Table of Contents

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

Lease

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

CancellationToken

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

long

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

string

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

cancellationToken CancellationToken

Cancels the request; the lease stays held.

Returns

Task<bool>

true if the lease was renewed, false if it has ended, also if it had before the call.

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

cancellationToken was cancelled. The lease is still held.