Coordinate a singleton job with a distributed lock
In this tutorial, you use a distributed lock to make sure a job runs on only one instance at a time, even when several copies of your app try to run it together. You build a small console app that acquires a lock before doing work and skips the work when another instance already holds it. You need the .NET SDK.
A lock is backed by a lease, so it cannot be held forever by a crashed process: when the backing lease expires without renewal, the lock releases automatically and another instance can take over. The lock surface lives on IZaris, so you cast the client to IZaris before using it.
1. Create a project and add the SDK
Create a console project and add the Zaris SDK.
dotnet new console -o zaris-lock
cd zaris-lock
dotnet add package Clustron.Zaris.SDK
2. Get an IZaris client
In Program.cs, register an in-process store and resolve the client as IZaris. The coordination features — locks, counters, leases, and watch — are exposed on IZaris, which extends the key-value client, so you cast the provider's result to it.
using Clustron.Zaris.Client;
using Clustron.Zaris.Client.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection()
.AddClustronZaris("demo", "zaris://inproc/demo")
.BuildServiceProvider();
var client = (IZaris)await services
.GetRequiredService<IZarisClientProvider>()
.GetAsync("demo");
3. Acquire the lock
Call Locks.AcquireAsync with a lock name and a time-to-live. The call does not block waiting for the lock: it returns a lock handle if you acquired it, or null if another instance already holds it. Always check for null before doing the protected work.
var handle = await client.Locks.AcquireAsync(
"lock:nightly-report",
TimeSpan.FromSeconds(30));
if (handle is null)
{
Console.WriteLine("Another instance holds the lock. Skipping.");
return;
}
4. Do the work and release
The handle is an IAsyncDisposableLock, which implements IAsyncDisposable, so wrap it in await using to release the lock when the work finishes — including when an exception unwinds the block. Releasing explicitly frees the lock immediately instead of waiting for the time-to-live to lapse.
await using (handle)
{
Console.WriteLine("Lock acquired. Running the job...");
await client.PutAsync("report:status", "running");
// do the exclusive work here
await Task.Delay(TimeSpan.FromSeconds(2));
await client.PutAsync("report:status", "done");
Console.WriteLine("Job complete. Lock will be released.");
}
5. Renew for long-running work
The time-to-live bounds how long the lock stays held without contact. If your job can run longer than the window you chose, renew the lock while you hold it. RenewAsync extends the lock and returns true when it succeeds, or false when the lock was already lost — treat a false return as a signal to stop, because another instance may now hold the lock.
var renewed = await handle.RenewAsync(TimeSpan.FromSeconds(30));
if (!renewed)
{
Console.WriteLine("Lost the lock. Stopping work.");
return;
}
6. Prove exclusivity
To see the lock in action, run the program in two terminals at once. Because each run uses its own in-process store, first make the store shared so both processes contend for the same lock: swap the connection string for a remote one that points at a running node.
.AddClustronZaris("demo", "zaris://localhost:7861/demo")
Start two instances against that node within the same time window. One prints Lock acquired, the other prints Another instance holds the lock. Skipping. Only the connection string changed; every line that acquires, renews, and releases the lock stayed the same.
What you built
You gated a job behind a distributed lock so that only one instance runs it at a time. You learned the three rules that make lock code correct: treat a null return from AcquireAsync as "someone else has it" and skip the work, release the handle with await using so a crash or exception cannot strand the lock, and renew when the work outlasts the time-to-live. Because the lock is lease-backed, a process that dies mid-job stops renewing and the lock frees itself, so the system never deadlocks on a lost owner.