Server
This commit is contained in:
245
Persistence.cs
Normal file
245
Persistence.cs
Normal file
@@ -0,0 +1,245 @@
|
||||
namespace GeoSus.Server;
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
// Write-Ahead Log a Snapshoty pro persistenci
|
||||
public class Persistence
|
||||
{
|
||||
private readonly ServerConfig _config;
|
||||
private readonly ILogger<Persistence> _logger;
|
||||
private readonly object _walLock = new();
|
||||
|
||||
public Persistence(ServerConfig config, ILogger<Persistence> logger)
|
||||
{
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
|
||||
// Vytvoříme adresář pro data
|
||||
Directory.CreateDirectory(_config.DataPath);
|
||||
}
|
||||
|
||||
#region WAL
|
||||
|
||||
public void AppendToWal(string lobbyId, GameEvent evt)
|
||||
{
|
||||
var lobbyDir = GetLobbyDir(lobbyId);
|
||||
Directory.CreateDirectory(lobbyDir);
|
||||
|
||||
var walPath = GetCurrentWalPath(lobbyDir);
|
||||
var json = JsonSerializer.Serialize(evt, JsonOptions.Default);
|
||||
|
||||
lock (_walLock)
|
||||
{
|
||||
// Kontrola velikosti WAL
|
||||
if (File.Exists(walPath))
|
||||
{
|
||||
var fileInfo = new FileInfo(walPath);
|
||||
if (fileInfo.Length > _config.WalMaxSizeMb * 1024 * 1024)
|
||||
{
|
||||
// Rotace - nový soubor
|
||||
var newWalPath = Path.Combine(lobbyDir, $"wal_{DateTime.UtcNow:yyyyMMddHHmmss}.ndjson");
|
||||
walPath = newWalPath;
|
||||
}
|
||||
}
|
||||
|
||||
using var fs = new FileStream(walPath, FileMode.Append, FileAccess.Write, FileShare.Read);
|
||||
using var writer = new StreamWriter(fs, Encoding.UTF8);
|
||||
writer.WriteLine(json);
|
||||
writer.Flush();
|
||||
fs.Flush(true); // fsync
|
||||
}
|
||||
}
|
||||
|
||||
public List<GameEvent> ReadWal(string lobbyId, long fromEventId = 0)
|
||||
{
|
||||
var lobbyDir = GetLobbyDir(lobbyId);
|
||||
var events = new List<GameEvent>();
|
||||
|
||||
if (!Directory.Exists(lobbyDir))
|
||||
return events;
|
||||
|
||||
// Načteme všechny WAL soubory seřazené podle času
|
||||
var walFiles = Directory.GetFiles(lobbyDir, "wal_*.ndjson")
|
||||
.OrderBy(f => f)
|
||||
.ToList();
|
||||
|
||||
foreach (var walPath in walFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
var lines = File.ReadAllLines(walPath, Encoding.UTF8);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line)) continue;
|
||||
|
||||
var evt = JsonSerializer.Deserialize<GameEvent>(line, JsonOptions.Default);
|
||||
if (evt != null && evt.EventId > fromEventId)
|
||||
{
|
||||
events.Add(evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Chyba při čtení WAL {Path}", walPath);
|
||||
}
|
||||
}
|
||||
|
||||
return events.OrderBy(e => e.EventId).ToList();
|
||||
}
|
||||
|
||||
private string GetCurrentWalPath(string lobbyDir)
|
||||
{
|
||||
// Najdeme nejnovější WAL nebo vytvoříme nový
|
||||
var walFiles = Directory.GetFiles(lobbyDir, "wal_*.ndjson");
|
||||
if (walFiles.Length == 0)
|
||||
{
|
||||
return Path.Combine(lobbyDir, $"wal_{DateTime.UtcNow:yyyyMMddHHmmss}.ndjson");
|
||||
}
|
||||
return walFiles.OrderByDescending(f => f).First();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Snapshots
|
||||
|
||||
public void SaveSnapshot(string lobbyId, LobbySnapshot snapshot)
|
||||
{
|
||||
var lobbyDir = GetLobbyDir(lobbyId);
|
||||
Directory.CreateDirectory(lobbyDir);
|
||||
|
||||
// Serializujeme bez checksumu
|
||||
snapshot.Timestamp = DateTime.UtcNow;
|
||||
var json = JsonSerializer.Serialize(snapshot, JsonOptions.Indented);
|
||||
|
||||
// Spočítáme checksum
|
||||
var checksum = ComputeChecksum(json);
|
||||
snapshot.Checksum = checksum;
|
||||
|
||||
// Serializujeme znovu s checksumem
|
||||
json = JsonSerializer.Serialize(snapshot, JsonOptions.Indented);
|
||||
|
||||
var snapshotPath = Path.Combine(lobbyDir, $"snapshot_{snapshot.LastEventId}.json");
|
||||
var tempPath = snapshotPath + ".tmp";
|
||||
|
||||
// Atomic write - temp + rename
|
||||
File.WriteAllText(tempPath, json, Encoding.UTF8);
|
||||
File.Move(tempPath, snapshotPath, overwrite: true);
|
||||
|
||||
_logger.LogInformation("Snapshot uložen: {Path}, eventId: {EventId}", snapshotPath, snapshot.LastEventId);
|
||||
|
||||
// Čištění starých snapshotů
|
||||
CleanupOldSnapshots(lobbyDir);
|
||||
}
|
||||
|
||||
public LobbySnapshot? LoadLatestSnapshot(string lobbyId)
|
||||
{
|
||||
var lobbyDir = GetLobbyDir(lobbyId);
|
||||
|
||||
if (!Directory.Exists(lobbyDir))
|
||||
return null;
|
||||
|
||||
var snapshotFiles = Directory.GetFiles(lobbyDir, "snapshot_*.json")
|
||||
.OrderByDescending(f => f)
|
||||
.ToList();
|
||||
|
||||
foreach (var snapshotPath in snapshotFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(snapshotPath, Encoding.UTF8);
|
||||
var snapshot = JsonSerializer.Deserialize<LobbySnapshot>(json, JsonOptions.Default);
|
||||
|
||||
if (snapshot == null) continue;
|
||||
|
||||
// Validace checksumu
|
||||
var storedChecksum = snapshot.Checksum;
|
||||
snapshot.Checksum = "";
|
||||
var jsonWithoutChecksum = JsonSerializer.Serialize(snapshot, JsonOptions.Indented);
|
||||
var computedChecksum = ComputeChecksum(jsonWithoutChecksum);
|
||||
|
||||
if (storedChecksum != computedChecksum)
|
||||
{
|
||||
_logger.LogWarning("Checksum mismatch pro snapshot {Path}", snapshotPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
snapshot.Checksum = storedChecksum;
|
||||
return snapshot;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Chyba při čtení snapshot {Path}", snapshotPath);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void CleanupOldSnapshots(string lobbyDir, int keepCount = 5)
|
||||
{
|
||||
var snapshotFiles = Directory.GetFiles(lobbyDir, "snapshot_*.json")
|
||||
.OrderByDescending(f => f)
|
||||
.Skip(keepCount)
|
||||
.ToList();
|
||||
|
||||
foreach (var oldSnapshot in snapshotFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(oldSnapshot);
|
||||
_logger.LogDebug("Smazán starý snapshot: {Path}", oldSnapshot);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Nelze smazat snapshot {Path}", oldSnapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeChecksum(string content)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(content);
|
||||
var hash = SHA256.HashData(bytes);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Archive
|
||||
|
||||
public void ArchiveLobby(string lobbyId)
|
||||
{
|
||||
var lobbyDir = GetLobbyDir(lobbyId);
|
||||
if (!Directory.Exists(lobbyDir))
|
||||
return;
|
||||
|
||||
var archiveDir = Path.Combine(_config.DataPath, "archive");
|
||||
Directory.CreateDirectory(archiveDir);
|
||||
|
||||
var archivePath = Path.Combine(archiveDir, $"{lobbyId}_{DateTime.UtcNow:yyyyMMddHHmmss}");
|
||||
Directory.Move(lobbyDir, archivePath);
|
||||
|
||||
_logger.LogInformation("Lobby archivováno: {LobbyId} -> {ArchivePath}", lobbyId, archivePath);
|
||||
}
|
||||
|
||||
public void DeleteLobbyData(string lobbyId)
|
||||
{
|
||||
var lobbyDir = GetLobbyDir(lobbyId);
|
||||
if (Directory.Exists(lobbyDir))
|
||||
{
|
||||
Directory.Delete(lobbyDir, recursive: true);
|
||||
_logger.LogInformation("Data lobby smazána: {LobbyId}", lobbyId);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private string GetLobbyDir(string lobbyId)
|
||||
{
|
||||
return Path.Combine(_config.DataPath, "lobbies", lobbyId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user