Ruvio.AspNetCore.DistributedLock
0.1.0
dotnet add package Ruvio.AspNetCore.DistributedLock --version 0.1.0
NuGet\Install-Package Ruvio.AspNetCore.DistributedLock -Version 0.1.0
<PackageReference Include="Ruvio.AspNetCore.DistributedLock" Version="0.1.0" />
<PackageVersion Include="Ruvio.AspNetCore.DistributedLock" Version="0.1.0" />
<PackageReference Include="Ruvio.AspNetCore.DistributedLock" />
paket add Ruvio.AspNetCore.DistributedLock --version 0.1.0
#r "nuget: Ruvio.AspNetCore.DistributedLock, 0.1.0"
#:package Ruvio.AspNetCore.DistributedLock@0.1.0
#addin nuget:?package=Ruvio.AspNetCore.DistributedLock&version=0.1.0
#tool nuget:?package=Ruvio.AspNetCore.DistributedLock&version=0.1.0
Ruvio.AspNetCore.DistributedLock
Distributed mutual exclusion on Ruvio. A lock is one key holding a random owner token, and every step is a single server command that only the owner can win:
| Step | Command |
|---|---|
| acquire | SET key token NX PX lease |
| renew | SET key token IFEQ token PX lease |
| release | DELEX key IFEQ token |
Release and renewal can never touch a lock another owner took after yours expired, which is
the bug a plain DEL has. This needs a Ruvio server with SET … IFEQ and DELEX.
services.AddRuvioDistributedLock(options => options.KeyPrefix = "myapp:lock:");
await using IRuvioLock? held = await locks.TryAcquireAsync(
"nightly-report", lease: TimeSpan.FromSeconds(30), wait: TimeSpan.FromSeconds(5));
if (held is null) return; // someone else runs it
await RunReportAsync(held.LockLost); // stops if the lock is lost mid-run
TryAcquireAsyncreturns null when the lock stayed taken;AcquireAsyncthrowsTimeoutException.- With
AutoRenew(default) the lease is renewed every third of its length. If a whole lease passes without a successful renewal, or a renewal finds another owner,IsHeldturns false andLockLostis cancelled; pass that token to the guarded work. - Disposing releases the lock; a failed release is swallowed because the lease expires on its own.
ASP.NET Core: [RuvioLock] for endpoints
Why. Some endpoints must never run twice at once for the same thing: charging an order,
numbering an invoice, importing a file, closing a day. A lock or SemaphoreSlim only protects
one instance. With two pods, or a double click that reaches a different pod, both requests run.
[RuvioLock] moves the guard into Ruvio so it holds across every instance, and removes the
acquire / try-finally / renew / map-to-HTTP boilerplate.
builder.Services.AddRuvioDistributedLock(); // needs an IRuvioClient registered
app.UseRouting();
app.UseAuthentication();
app.UseRuvioLock(); // after routing and auth
// minimal API
app.MapPost("/orders/{id}/pay", PayAsync).WithMetadata(new RuvioLockAttribute("order:{id}"));
// controller
[RuvioLock("nightly-import", leaseSeconds: 120, WaitMilliseconds = 0)]
[HttpPost("import")]
public Task<IActionResult> Import(CancellationToken ct) => ...; // ct is cancelled if the lock is lost
| Behaviour | Detail |
|---|---|
| Name | Fixed ("nightly-import") or template: {id} = route value, then query value; {user} = user name. An unresolved placeholder answers 400. |
| Taken | Waits WaitMilliseconds (default 0), then answers RejectStatusCode (default 409) with Retry-After; your code does not run. |
| Running | The lease is renewed automatically; the lock is released when the response finishes, also when the endpoint throws or the client disconnects. |
| Lock lost | HttpContext.RequestAborted is replaced by a token that also fires when the lock is lost, so EF Core, HttpClient and your code stop instead of running next to the new owner. |
| Server unreachable | Fails closed with 503 instead of running unguarded. |
| Cost | Endpoints without the attribute return the next delegate's task directly: no allocation. |
Telemetry
Meter and ActivitySource Ruvio.AspNetCore.DistributedLock (same shape as the rate-limiting
package): counters ruvio.lock.acquired, ruvio.lock.rejected (contention), ruvio.lock.lost
(unhealthy server link) and ruvio.lock.errors, plus histogram ruvio.lock.wait_duration (ms).
Fixed names are tagged resource; templated names are not, to keep cardinality bounded.
builder.Services.AddOpenTelemetry().WithMetrics(m => m.AddMeter(RuvioLockTelemetry.Name));
Limits
- One lock lives on one server. If a failover loses the key, two owners can overlap for up to a lease. Work that must never overlap should also check a version or fencing value on the resource it writes. This package does not issue fencing tokens: handing out an increasing number and taking the lock are two commands, so they cannot be made atomic here.
- Locks are not reentrant. Acquiring a held resource again from the same process waits like any caller.
- Lease times are wall-clock server TTLs; keep the lease well above the longest pause your process can have.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Ruvio.Client (>= 0.3.2)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0 | 50 | 10/12/2026 |