120 lines
5.5 KiB
C#
120 lines
5.5 KiB
C#
using LiteDB;
|
|
|
|
namespace Campus_live_rewrite.api.dbs
|
|
{
|
|
// LiteDB v5 writes through a write-ahead log (`<name>-log.db`) and only merges
|
|
// it back into the main data file on Checkpoint() - which happens automatically
|
|
// ONLY on LiteDatabase.Dispose(), or once the log passes CheckpointSize
|
|
// (default 1000 pages / ~8MB).
|
|
//
|
|
// Every LiteDatabase in this project is a `static` field that is never disposed,
|
|
// and `systemctl stop campus` kills the process without unwinding statics. So no
|
|
// checkpoint ever ran, and the logs sat just under the 1000-page auto-threshold
|
|
// (573-825 pages) and never tripped it either.
|
|
//
|
|
// The result, as of 2026-08-21: Heartbeats.db and Settings.db were 8192 bytes -
|
|
// a bare header page - while Heartbeats-log.db was 5.6MB and Settings-log.db
|
|
// 6.4MB. 100% of that data lived in the WAL and none of it in the data file.
|
|
//
|
|
// That matters beyond tidiness because install.sh deliberately preserves
|
|
// Data/DBs/* across deploys, so those logs were copied forward verbatim into
|
|
// every new build. Deploying a known-good server could not fix a data-level
|
|
// problem, because the data came along with it. That is why swapping in a
|
|
// working server still broke.
|
|
public static class DbMaintenance
|
|
{
|
|
// Checkpoint every store. Each is wrapped separately so one bad file can't
|
|
// stop the rest from flushing.
|
|
private static Timer? _autoCheckpoint;
|
|
|
|
// Checkpoint on a schedule, not just at shutdown.
|
|
//
|
|
// Writes land in <name>-log.db and are only merged into <name>.db by a
|
|
// checkpoint. Until then the data is recoverable ONLY by replaying that log.
|
|
// Measured 2026-08-23: a setting written and read back survived a restart, but
|
|
// Settings.db was still 8,192 bytes (untouched since 12 Aug) while
|
|
// Settings-log.db had grown to 7.37 MB - i.e. every setting the server has ever
|
|
// stored lived in the log alone.
|
|
//
|
|
// That works until the log is lost or truncated, at which point everything
|
|
// written since the last checkpoint disappears at once. Shutdown-only
|
|
// checkpointing means a hard kill (crash, SIGKILL, power loss) never merges.
|
|
//
|
|
// A periodic checkpoint bounds that exposure to `minutes`. It only flushes data
|
|
// already committed - it cannot change any response or alter stored values.
|
|
public static void StartAutoCheckpoint(int minutes = 5)
|
|
{
|
|
if (_autoCheckpoint != null) return;
|
|
var period = TimeSpan.FromMinutes(minutes);
|
|
_autoCheckpoint = new Timer(_ =>
|
|
{
|
|
try { CheckpointAll(quiet: true); }
|
|
catch (Exception ex) { Console.WriteLine($"[DbMaintenance] auto-checkpoint failed: {ex.Message}"); }
|
|
}, null, period, period);
|
|
Console.WriteLine($"[DbMaintenance] auto-checkpoint every {minutes} min");
|
|
}
|
|
|
|
public static void StopAutoCheckpoint()
|
|
{
|
|
_autoCheckpoint?.Dispose();
|
|
_autoCheckpoint = null;
|
|
}
|
|
|
|
public static void CheckpointAll(bool quiet = false)
|
|
{
|
|
Flush("Players", () => PlayerDB.PlayerDBFile.Checkpoint(), quiet);
|
|
Flush("Rooms", () => RoomDB.rooms_db.Checkpoint(), quiet);
|
|
Flush("Chat", () => ChatDB.ChatDBFile.Checkpoint(), quiet);
|
|
Flush("Events", () => EventDB.EventDBFile.Checkpoint(), quiet);
|
|
Flush("PlayersFriends", () => FriendsDB.PlayerFriendDBFile.Checkpoint(), quiet);
|
|
Flush("Heartbeats", HeartbeatDB.Checkpoint, quiet);
|
|
Flush("Settings", SettingsDB.Checkpoint, quiet);
|
|
Flush("Leaderboard", LeaderboardDB.Checkpoint, quiet);
|
|
Flush("Support", SupportDB.Checkpoint, quiet);
|
|
Flush("Clubs", ClubDB.Checkpoint, quiet);
|
|
Flush("Inventions", InventionDB.Checkpoint, quiet);
|
|
}
|
|
|
|
private static void Flush(string name, Action action, bool quiet = false)
|
|
{
|
|
try
|
|
{
|
|
action();
|
|
if (!quiet) Console.WriteLine($"[DbMaintenance] checkpointed {name}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[DbMaintenance] FAILED to checkpoint {name}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// Print the data-file / log-file split at startup. A main file stuck at 8192
|
|
// bytes with a multi-megabyte log means the checkpoint path is broken again.
|
|
public static void ReportWalState()
|
|
{
|
|
string dir = Path.Combine(Environment.CurrentDirectory, "Data", "DBs");
|
|
if (!Directory.Exists(dir))
|
|
{
|
|
Console.WriteLine($"[DbMaintenance] no DB directory at {dir}");
|
|
return;
|
|
}
|
|
|
|
foreach (var db in Directory.GetFiles(dir, "*.db").OrderBy(f => f))
|
|
{
|
|
if (db.EndsWith("-log.db", StringComparison.OrdinalIgnoreCase)) continue;
|
|
|
|
var main = new FileInfo(db);
|
|
var log = new FileInfo(db[..^3] + "-log.db");
|
|
long logLen = log.Exists ? log.Length : 0;
|
|
|
|
string flag = (main.Length <= 8192 && logLen > 1024 * 1024)
|
|
? " <-- ALL DATA IS IN THE LOG, main file is empty"
|
|
: "";
|
|
|
|
Console.WriteLine(
|
|
$"[DbMaintenance] {main.Name,-24} data={main.Length,10:n0} log={logLen,12:n0}{flag}");
|
|
}
|
|
}
|
|
}
|
|
}
|