Class Lease
- Namespace
- DynaSharp.Coordination
- Assembly
- DynaSharp.Coordination.dll
A held lock: the lease that TryAcquireAsync(string, TimeSpan, CancellationToken) or AcquireAsync(string, TimeSpan, CancellationToken) handed out. It renews itself every third of
its duration until it is disposed or lost, and Ended says when it ends. Dispose it to release the lock: await using var lease = ....
public sealed class Lease : IAsyncDisposable
- Inheritance
-
Lease
- Implements
- Inherited Members
Examples
await using var lease = await locks.AcquireAsync("nightly-report", TimeSpan.FromSeconds(30), ct);
using var stop = CancellationTokenSource.CreateLinkedTokenSource(ct, lease.Ended);
await foreach (var placed in byStatus.Query(Order.Keys.ByStatusPk.Of(OrderStatus.Placed)).ItemsAsync(stop.Token))
{
await client.TransactWrite().IsHeld(lease).Put(orders, placed with { Status = OrderStatus.Shipped }).SendAsync(stop.Token);
}
Remarks
Timing. The lease is held from the moment its acquire was sent, for its duration on the monotonic clock of the client, and each renewal extends it by the duration from the moment that renewal was sent. The heartbeat sends the next renewal a third of the duration after the last one that succeeded was sent, so a slow answer does not delay the one after it. A contender starts its wait only once it has read the item, after the write, so on the holder's clock the lease ends before a contender may take it over, whatever offset the clocks of the two machines have. That assumes both clocks run at about the same rate. A host that is suspended, or a virtual machine that freezes, stops the holder but not the contender: it may wake up after its lease ended without knowing it yet. That case is what IsHeld<TChain>(TChain, Lease) and FencingToken are for.
Losing it. It is lost when a renewal finds another owner in the item (a contender took it over, or someone wrote the item), when it finds the item deleted, which starts the fencing token at 1 again for the next acquire, when DynamoDB answers a renewal with ResourceNotFoundException, and when the lease runs out on the monotonic clock without a renewal that succeeded. A renewal of the heartbeat that fails with any other error, after the retries of the client, is tried again with a growing wait, up to a sixth of the duration, until one succeeds or the lease runs out. Losing it is final: acquire the lock again for a new lease.
Fencing. A lease guards only what checks it. Code that writes to DynamoDB adds IsHeld<TChain>(TChain, Lease) to the transaction of the write, and a system outside compares FencingToken with the highest one it has seen.
A lease that is never disposed is renewed for as long as the process runs: the heartbeat keeps it alive, and with it the lock.
Properties
Current
Gets the lease of the term of RunAsLeaderAsync(string, TimeSpan, Func<Lease, CancellationToken, Task>, CancellationToken) that the calling code runs in, or null outside one. It is what IsHeld<TChain>(TChain) checks, so that work deep in a call graph fences its writes without the lease passed down to it.
public static Lease? Current { get; }
Property Value
Remarks
The leader sets it before it calls the work and clears it when the term ends. It flows as an AsyncLocal<T> does: into everything the work calls, awaits or starts with Run(Func<Task>) during the term, and never back to the caller of RunAsLeaderAsync(string, TimeSpan, Func<Lease, CancellationToken, Task>, CancellationToken). There is no setter: a lease from TryAcquireAsync(string, TimeSpan, CancellationToken) or AcquireAsync(string, TimeSpan, CancellationToken) is passed to IsHeld<TChain>(TChain, Lease) as it is.
Code that started before the term, in a constructor or a StartAsync, or that runs under SuppressFlow(), sees
null, and so does the heartbeat of a lease. Work that outlives its term, such as a task the work started and did not wait for, keeps the lease
that has ended, and a transaction that checks it fails at DynamoDB, which is the fencing it is for. In a RunAsLeaderAsync(string, TimeSpan, Func<Lease, CancellationToken, Task>, CancellationToken) inside the
work of another, the inner lease is current; pass the outer one explicitly.
Ended
Gets a token that is cancelled once the lease ends: when it is lost, see the remarks of Lease, or released by DisposeAsync(). Pass it to the work that needs the lock, so that the work stops when the lease does. An acquire whose answer came after a whole duration hands out a lease that has ended already.
public CancellationToken Ended { get; }
Property Value
FencingToken
Gets the fencing token: a number that each acquire of this lock raises by one, so a later holder always has a higher one. A system that a holder writes to keeps the highest token it has seen and refuses a lower one; that stops a holder that lost its lease, but does not know yet, from overwriting the work of the next.
public long FencingToken { get; }
Property Value
Remarks
The token goes back only if the item of the lock goes back: when it is deleted, which starts the count at 1 again, and when the table is restored from a backup or to a point in time, which brings an older token back. A global table that takes writes in more than one region can grant the lock twice, with the same token, because each region applies its conditional write before the other one's arrives: keep the lock table to a single region that writes.
Name
Gets the name of the lock.
public string Name { get; }
Property Value
Methods
DisposeAsync()
Stops the heartbeat and releases the lock if this lease still holds it: one UpdateItem that removes the owner and the holder label, on condition that the owner is
this lease, so a lock that someone else took over is left alone. The fencing token stays in the item. A release that fails is logged, not thrown,
and the lock is then free once the lease runs out. Ended is cancelled. Calling it again does nothing.
public ValueTask DisposeAsync()
Returns
- ValueTask
A task that completes when the lock is released.
RenewAsync(CancellationToken)
Renews the lease now, outside the heartbeat, which renews it every third of its duration anyway. Useful before a step that must not start on a lease that is about to run out. It sends one renewal, with the retries of the client, and a failure leaves the lease to the heartbeat.
public Task<bool> RenewAsync(CancellationToken cancellationToken = default)
Parameters
cancellationTokenCancellationTokenCancels the request; the lease stays held.
Returns
Exceptions
- ResourceNotFoundException
The table does not exist. The lease is lost.
- DynamoDbException
DynamoDB failed the request, after the retries of the client, with any other error. The lease is still held until it runs out, and the heartbeat goes on renewing it.
- HttpRequestException
The request did not reach DynamoDB, after the retries of the client. The lease is still held, as above.
- TimeoutException
An attempt timed out, after the retries of the client. The lease is still held, as above.
- CredentialsUnavailableException
The client has no credentials. The lease is still held, as above.
- OperationCanceledException
cancellationTokenwas cancelled. The lease is still held.