Distributed Locks
A distributed lock is a mechanism that ensures that a specific resource is accessed by only one node or process at a time in a distributed environment. This is crucial when:
- Preventing race conditions: Ensuring that multiple processes do not modify shared resources simultaneously.
- Coordinating tasks: Managing access to shared databases, files, or services across different nodes.
- Maintaining data durability: Guaranteeing that concurrent operations do not result in inconsistent states.
By partitioning locks among nodes controlled by Raft Groups, Kahuna offers:
- Reliability: Raft consensus ensures that partition data remains consistent even in the face of network failures.
- Simplicity: A straightforward API based on leases makes it easy to integrate distributed locking into your applications.
Persistent lock acquire, extend, and release operations are batched per partition before Raft proposal. Bursty workloads that touch locks on the same partition can therefore share fewer WAL appends and quorum round trips while keeping the same fencing-token and lease semantics.
Use Cases
- Leader Election: Elect a single leader in a cluster of services. Only one node should act as the leader at any time (e.g., for scheduling, replication). A distributed lock ensures only one process "wins" and holds the leadership.
- Preventing Double Execution of Scheduled Jobs: Ensure a cron job or background worker is executed only once across multiple nodes. In a horizontally scaled system, multiple nodes might try to run the same job. A distributed lock prevents multiple executions of the same scheduled task.
- Safe Deployment CI/CD Coordination: Prevent multiple CI/CD pipelines from deploying the same environment simultaneously. Two deployment jobs could conflict and cause downtime. Use a lock to serialize deployments.
- Database Migration Coordination: Ensure that only one service performs schema migration at startup. If multiple services run migrate up concurrently, it may corrupt the schema. Use a lock to ensure only the first instance runs migrations.
- Session Control / Login Exclusivity: Allow only one active session per user. Used in banking apps, gaming, admin consoles, etc.
- Distributed Queue Consumer Coordination: Ensure that a message from a queue is only processed once, even with multiple consumers.
- Throttling or Rate-Limiting Across Services: Enforce a global rate limit across many service instances.
- External System Coordination: Ensure only one node writes to an external system (e.g., shared database, billing API, payment gateway) to avoid double charges or inconsistent writes.
API
Kahuna exposes a simple API for acquiring, releasing and extending locks. The main functions are:
Lock
- API
- CLI
- C#
- TypeScript
- Rest
Task<KahunaLock> GetOrCreateLock(
string resource,
TimeSpan expiry,
LockDurability durability = LockDurability.Persistent
);
- resource: The identifier for the resource you want to lock.
- expiry: The duration for the lock lease.
- durability: Defines whether the lock durability is Ephemeral or Persistent.
Returns:
- IsAcquired:
trueif the lock was successfully acquired. - FencingToken: A monotonically increasing token for that lock resource. Downstream systems can reject operations with an older token.
Acquire a lease on the resource locks/tasks/123 for 10 seconds in the interactive shell:
kahuna-cli> lock locks/tasks/123 10000
f1 acquired 786f947d8a6643f0b939865f72aa512a
Acquire a lease using command line arguments:
~> kahuna-cli --lock locks/tasks/123 --expires 10000
f4 acquired 4e3c5d8ee76a4a1db268c81b048a002a
Below is a basic example to demonstrate how to acquire a distributed lock in a C# project:
await using KahunaLock myLock = await client.GetOrCreateLock(
"balance-" + userId,
TimeSpan.FromSeconds(5)
);
if (myLock.IsAcquired)
{
Console.WriteLine("Lock acquired!");
// implement exclusive logic here
}
const lock = await client.acquireLock("balance-" + userId, {
expiry: 5000,
durability: "persistent"
});
if (lock.acquired) {
console.log("Lock acquired");
// implement exclusive logic here
}
Acquiring a lock from the Rest API:
curl --location 'https://kahuna-dev.company.internal/v1/locks/try-lock' \
--header 'Content-Type: application/json' \
--data '{"resource":"locks/tasks/123", "owner":"e5943062358144b4b0bbff8868f7063d", "expiresMs":10000, "durability":0 }'
Response:
{"type": 0, "fencingToken": 0}
Unlock
- API
- CLI
- C#
- TypeScript
- Rest
Task<bool> Unlock(
string resource,
byte[] owner,
LockDurability durability = LockDurability.Persistent
);
- resource: The identifier for the resource to unlock.
- owner: The unique identifier for the lock previously used to acquire the lock.
- durability: Defines whether the lock durability is Ephemeral or Persistent.
Returns:
- Unlocked:
trueif the resource was successfully unlocked.
Unlock the acquired lease on the resource locks/tasks/123 in the interactive shell:
kahuna-cli> lock locks/tasks/123 10000
f1 acquired 786f947d8a6643f0b939865f72aa512a
kahuna-cli> unlock locks/tasks/123
f1 unlocked
Unlock the acquired lease using command line arguments:
$ kahuna-cli --lock locks/tasks/123 --expires 60000
f4 acquired 4e3c5d8ee76a4a1db268c81b048a002a
$ kahuna-cli --unlock-lock locks/tasks/123 --owner 4e3c5d8ee76a4a1db268c81b048a002a
f4 unlocked
Below is a basic example to demonstrate how to acquire a distributed lock in a C# project.
KahunaLock myLock = await client.GetOrCreateLock(
"balance-" + userId,
TimeSpan.FromSeconds(5)
);
if (myLock.IsAcquired)
{
Console.WriteLine("Lock acquired!");
// implement exclusive logic here
}
// Manually release the lock
await myLock.DisposeAsync();
const lock = await client.acquireLock("balance-" + userId, {
expiry: 5000
});
if (lock.acquired) {
console.log("Lock acquired");
// implement exclusive logic here
}
await lock.release();
curl --location 'http://kahuna-dev.company.internal/v1/locks/try-unlock' \
--header 'Content-Type: application/json' \
--data '{"resource":"locks/tasks/123", "owner":"e5943062358144b4b0bbff8868f7063d", "durability":0}'
Response:
{"type": 0, "fencingToken": 0}
Extend
- API
- CLI
- C#
- TypeScript
- Rest
Task<(bool Extended, long FencingToken)> TryExtendLock(
string resource,
byte[] owner,
TimeSpan duration,
LockDurability durability = LockDurability.Persistent
);
- resource: The identifier for the resource you want to extend.
- owner: A unique identifier for the lock, usually associated with the client or process requesting the lock. It must be the current owner of the lock.
- duration: The new lock lease duration.
- durability: Defines whether the lock durability is Ephemeral or Persistent.
Returns:
- Extended:
trueif the lock was successfully extended. - FencingToken: The current fencing token for that lock resource. Extending a lock keeps the same owner and token.
Extends the acquired lease on the resource locks/tasks/123 in the interactive shell:
kahuna-cli> lock locks/tasks/123 10000
f1 acquired 449941a969f74f8eb3d81cc70e5ab2ae
kahuna-cli> extend-lock locks/tasks/123 60000
f1 extended 449941a969f74f8eb3d81cc70e5ab2ae
Extend the acquired lease using command line arguments:
$ kahuna-cli --lock locks/tasks/123 --expires 60000
f4 acquired 4e3c5d8ee76a4a1db268c81b048a002a
$ kahuna-cli --extend-lock locks/tasks/123 --owner 4e3c5d8ee76a4a1db268c81b048a002a --expires 60000
f4 extended
How to acquire a distributed lock and then extend it in a c# project:
await using KahunaLock myLock = await client.GetOrCreateLock(
"balance-" + userId,
TimeSpan.FromSeconds(5)
);
if (myLock.IsAcquired)
{
Console.WriteLine("Lock acquired!");
// implement exclusive logic here
// Extend the lock
await myLock.TryExtend(TimeSpan.FromSeconds(60));
}
await using lock = await client.acquireLock("balance-" + userId, {
expiry: 5000
});
if (lock.acquired) {
// implement exclusive logic here
await lock.extend(60_000);
}
curl --location 'http://kahuna-dev.company.internal/v1/locks/try-extend' \
--header 'Content-Type: application/json' \
--data '{"resource": "locks/tasks/123", "owner": "e5943062358144b4b0bbff8868f7063d", "expiresMs": 60000, "durability": 0}'
Response:
{"type": 0, "fencingToken": 0}
Get
- API
- CLI
- C#
- TypeScript
- Rest
Task<KahunaLockInfo?> GetLockInfo(
string resource,
LockDurability durability = LockDurability.Persistent
);
- resource: The identifier for the resource you want to get information.
- durability: Defines whether the lock durability is Ephemeral or Persistent.
Returns:
- Owner: The current owner of the lock.
- FencingToken: The current fencing token for that lock resource.
Obtains information about the lock locks/tasks/123 in the interactive shell:
kahuna-cli> lock locks/tasks/123 10000
f1 acquired 449941a969f74f8eb3d81cc70e5ab2ae
kahuna-cli> get-lock locks/tasks/123
f9 fa020530242546798d7c349437665071
Obtain information about the lock using command line arguments:
$ kahuna-cli --lock locks/tasks/123 --expires 60000
f4 acquired 4e3c5d8ee76a4a1db268c81b048a002a
$ kahuna-cli --get-lock locks/tasks/123
f4 4e3c5d8ee76a4a1db268c81b048a002a
Obtain information about a lock in a c# project:
await using KahunaLock myLock = await client.GetOrCreateLock(
"group-leader-" + groupId,
expiry: TimeSpan.FromSeconds(5)
);
if (!myLock.IsAcquired)
{
Console.WriteLine("Lock not acquired!");
var lockInfo = await myLock.GetInfo();
Console.WriteLine($"Lock owner: {lockInfo.Owner}");
Console.WriteLine($"Expires: {lockInfo.Expires}");
}
await using lock = await client.acquireLock("group-leader-" + groupId, {
expiry: 5000
});
if (!lock.acquired) {
const lockInfo = await client.getLockInfo("group-leader-" + groupId);
console.log(`Lock owner: ${lockInfo?.owner}`);
console.log(`Expires: ${lockInfo?.expires}`);
}
curl --location 'https://kahuna-dev.company.internal/v1/locks/get-info' \
--header 'Content-Type: application/json' \
--data '{"resource": "locks/tasks/123", "durability": 0}'
Response:
{"type": 0, "owner": "4e3c5d8ee76a4a1db268c81b048a002a", "fencingToken": 4}