75 lines
2.7 KiB
C#
75 lines
2.7 KiB
C#
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; }
|
||
|
|
|
||
|
|
/// <summary>Takes the lock for what is said, or null when someone else holds it; disposing the result frees it.</summary>
|
||
|
|
public static async Task<Held> 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<MaintenanceLock>()
|
||
|
|
.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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>Who holds it now and for what, or null.</summary>
|
||
|
|
public static async Task<MaintenanceLock> Current(CancellationToken token)
|
||
|
|
{
|
||
|
|
var held = await DB.Default.Find<MaintenanceLock>().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<MaintenanceLock>().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<MaintenanceLock>().Match(l => l.ID == Name && l.Owner == _owner)
|
||
|
|
.Modify(l => l.Owner, null).Modify(l => l.Heartbeat, DateTime.MinValue).ExecuteAsync();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|