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();
}
}
}
}