Getting Started โ
Get your first distributed lock running in under 5 minutes.
Installation โ
npm install syncguard @google-cloud/firestore
npm install syncguard ioredis
Install only the backend you need. Peer dependencies are optional.
Quick Start (Firestore) โ
import { createLock } from "syncguard/firestore";
import { Firestore } from "@google-cloud/firestore";
const db = new Firestore();
const lock = createLock(db);
// Prevent duplicate payment processing
await lock(
async () => {
const payment = await getPayment(paymentId);
if (payment.status === "pending") {
await chargeCard(payment.amount);
await updateStatus(paymentId, "completed");
}
},
{ key: `payment:${paymentId}` },
);
Firestore Index Required
For optimal performance, create a single-field ascending index on the lockId
field in your locks
collection. Firestore typically auto-creates this for equality queries, but verify in production.
Quick Start (Redis) โ
import { createLock } from "syncguard/redis";
import Redis from "ioredis";
const redis = new Redis();
const lock = createLock(redis);
await lock(
async () => {
// Your critical section
},
{ key: "resource:123" },
);
That's it. The lock()
function handles acquisition, retries, execution, and release automatically.
Configuration Basics โ
Customize lock behavior with inline options:
await lock(workFn, {
key: "job:daily-report", // Required: unique lock identifier
ttlMs: 60000, // Lock expires after 60s (default: 30s)
timeoutMs: 10000, // Give up acquisition after 10s (default: 5s)
maxRetries: 20, // Retry up to 20 times on contention (default: 10)
});
Key guidelines:
ttlMs
: Short enough to minimize impact of crashed processes, long enough for your worktimeoutMs
: How long to wait for contended locks before giving upmaxRetries
: Higher = more patient under load; uses exponential backoff with jitter
Backend-specific config (collection names, key prefixes):
const lock = createLock(db, {
collection: "app_locks", // Default: "locks"
fenceCollection: "app_fences", // Default: "fence_counters"
});
const lock = createLock(redis, {
keyPrefix: "my-app", // Default: "syncguard"
});
Manual Lock Control โ
For long-running tasks or custom retry logic, use the backend directly:
import { createRedisBackend } from "syncguard/redis";
const backend = createRedisBackend(redis);
// Acquire lock manually
const result = await backend.acquire({
key: "batch:daily-report",
ttlMs: 300000, // 5 minutes
});
if (result.ok) {
try {
const { lockId, fence } = result;
await generateReport(fence);
// Extend lock for long-running tasks
const extended = await backend.extend({
lockId,
ttlMs: 300000, // Another 5 minutes
});
if (!extended.ok) {
throw new Error("Failed to extend lock");
}
await sendReportEmail();
} finally {
await backend.release({ lockId: result.lockId });
}
} else {
console.log("Resource locked by another process");
}
Why manual mode?
- Extending locks during long-running work
- Custom retry strategies
- Conditional lock release
- Access to fencing tokens (see Fencing Tokens)
Error Handling โ
Lock operations throw LockError
for system failures:
import { LockError } from "syncguard";
try {
await lock(
async () => {
// Critical section
},
{ key: "resource:123" },
);
} catch (error) {
if (error instanceof LockError) {
console.error(`[${error.code}] ${error.message}`);
// Handle specific error codes
if (error.code === "AcquisitionTimeout") {
// Contention exceeded timeout
} else if (error.code === "ServiceUnavailable") {
// Backend unavailable, retry later
}
}
}
Common error codes:
AcquisitionTimeout
: Couldn't acquire lock withintimeoutMs
(contention)ServiceUnavailable
: Backend unavailable (network/connection issues)NetworkTimeout
: Operation timed out (client-side timeout)InvalidArgument
: Invalid parameters (malformed key/lockId)
TIP
See API Reference for all error codes. For backend error mapping specifications, see specs/interface.md ยง Error Handling.