The server backs itself up: every collection byte for byte, the media linked
A backup is a directory <stamp>-<kind> under Backups:Root (/var/lib/privapub/backups, 2770, files 0640), written as .partial and renamed once whole: a manifest (host, build, newest migration, each collection's count, size, sha256 and indexes, what was left out and why, the media list), each collection as gzipped canonical Extended JSON read raw, and hard links to the files of untrashed media rows (copies where a link can't be made). On a replica set every collection is read in one snapshot session. Never in a backup: the statistics salt, jobs, recovery codes, sessions, the maintenance lock and the configuration's copy with its SMTP password. One backup or restore at a time (MaintenanceLock, a heartbeat document), and the janitor purges nothing meanwhile. BackupScheduler backs up nightly at 03:30 UTC (or at once after missing a night); rotation keeps 7 daily, 4 weekly, 3 pre-deploy and 3 pre-restore backups. CLI: admin backup [--kind] [--db-only], admin backups, admin backup verify; these run before migrations, so the deploy's own pre-deploy backup, which replaces mongodump, is of the database as the live build left it. EntityMaps.Warm runs once under a lock, since test hosts now boot side by side. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
37b12c5fee
commit
12bb75809f
20 files changed
+1132
-22
No files matched your search
@@ -0,0 +1,75 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user