using MongoDB.Driver; using MongoDB.Entities; namespace PrivaPub.Infrastructure.Backup { // One backup or restore at a time, whoever starts it (the CLI, the nightly schedule, the administrator's page): a // document held with a heartbeat. A holder that died (no heartbeat for two minutes) is taken over. The collection is // never backed up, nor dropped by a restore. public class MaintenanceLock : Entity { public const string Name = "maintenance"; static readonly TimeSpan Stale = TimeSpan.FromMinutes(2); static readonly TimeSpan Beat = TimeSpan.FromSeconds(30); public string Owner { get; set; } public string What { get; set; } public DateTime Since { get; set; } public DateTime Heartbeat { get; set; } /// Takes the lock for what is said, or null when someone else holds it; disposing the result frees it. public static async Task Take(string what, CancellationToken token) { var owner = $"{Environment.MachineName}:{Environment.ProcessId}:{Guid.NewGuid():N}"; var now = DateTime.UtcNow; var stale = now - Stale; try { var taken = await DB.Default.UpdateAndGet() .Match(l => l.ID == Name && (l.Heartbeat < stale || l.Owner == null)) .Modify(l => l.Owner, owner) .Modify(l => l.What, what) .Modify(l => l.Since, now) .Modify(l => l.Heartbeat, now) .Option(o => o.IsUpsert = true) .ExecuteAsync(token); return taken?.Owner == owner ? new Held(owner) : default; } catch (MongoCommandException ex) when (ex.Code == 11000) { return default;//held: the upsert met the live lock's id } catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) { return default; } } /// Who holds it now and for what, or null. public static async Task Current(CancellationToken token) { var held = await DB.Default.Find().Match(l => l.ID == Name).ExecuteFirstAsync(token); return held?.Owner != null && held.Heartbeat >= DateTime.UtcNow - Stale ? held : default; } public sealed class Held : IAsyncDisposable { readonly string _owner; readonly Timer _heartbeat; public Held(string owner) { _owner = owner; _heartbeat = new Timer(_ => _ = DB.Default.Update().Match(l => l.ID == Name && l.Owner == owner) .Modify(l => l.Heartbeat, DateTime.UtcNow).ExecuteAsync(), default, Beat, Beat); } public async ValueTask DisposeAsync() { await _heartbeat.DisposeAsync(); await DB.Default.Update().Match(l => l.ID == Name && l.Owner == _owner) .Modify(l => l.Owner, null).Modify(l => l.Heartbeat, DateTime.MinValue).ExecuteAsync(); } } } }