Extract JSON snapshot persistence helpers

This commit is contained in:
Jacob Dubin
2026-05-17 06:58:12 -05:00
parent d37521281e
commit 785dc2b48b
3 changed files with 131 additions and 131 deletions

View File

@@ -18,7 +18,7 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
private readonly ConcurrentDictionary<string, CloudSession> _sessionsByToken = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, CloudSession> _sessionsByToken = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, string> _symmetricKeys = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, string> _symmetricKeys = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, KeyRequestRecord> _keyRequests = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, KeyRequestRecord> _keyRequests = new(StringComparer.OrdinalIgnoreCase);
private readonly string? _persistencePath; private readonly JsonSnapshotStore _snapshotStore;
private readonly Lock _syncRoot = new(); private readonly Lock _syncRoot = new();
private readonly List<UpdateManifest> _updates; private readonly List<UpdateManifest> _updates;
private readonly List<MediaRecord> _media = []; private readonly List<MediaRecord> _media = [];
@@ -33,7 +33,7 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
public InMemoryCloudStateStore(string? persistencePath = null) public InMemoryCloudStateStore(string? persistencePath = null)
{ {
_persistencePath = persistencePath; _snapshotStore = new JsonSnapshotStore(persistencePath, PersistenceJsonOptions);
_robot = new DeviceRegistration _robot = new DeviceRegistration
{ {
HostMappings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) HostMappings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
@@ -104,111 +104,88 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
public void LoadPersistedState() public void LoadPersistedState()
{ {
if (string.IsNullOrWhiteSpace(_persistencePath) || !File.Exists(_persistencePath)) var snapshot = _snapshotStore.Load<PersistentStateSnapshot>();
if (snapshot is null)
{ {
return; return;
} }
try _account = snapshot.Account ?? _account;
_robot = snapshot.Robot ?? _robot;
_robotProfile = snapshot.RobotProfile ?? _robotProfile;
_devices.Clear();
foreach (var device in snapshot.Devices ?? [])
{ {
var snapshot = JsonSerializer.Deserialize<PersistentStateSnapshot>(File.ReadAllText(_persistencePath), PersistenceJsonOptions); _devices[device.DeviceId] = device;
if (snapshot is null)
{
return;
}
_account = snapshot.Account ?? _account;
_robot = snapshot.Robot ?? _robot;
_robotProfile = snapshot.RobotProfile ?? _robotProfile;
_devices.Clear();
foreach (var device in snapshot.Devices ?? [])
{
_devices[device.DeviceId] = device;
}
if (_devices.IsEmpty || !_devices.ContainsKey(_robot.DeviceId))
{
_devices[_robot.DeviceId] = _robot;
}
_sessionsByToken.Clear();
foreach (var session in snapshot.Sessions ?? [])
{
if (!string.IsNullOrWhiteSpace(session.Token))
{
_sessionsByToken[session.Token] = session.ToRecord();
}
}
_symmetricKeys.Clear();
foreach (var pair in snapshot.SymmetricKeys ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase))
{
_symmetricKeys[pair.Key] = pair.Value;
}
_keyRequests.Clear();
foreach (var keyRequest in snapshot.KeyRequests ?? [])
{
_keyRequests[keyRequest.RequestId] = keyRequest;
}
_updates.Clear();
_updates.AddRange(snapshot.Updates ?? []);
_media.Clear();
_media.AddRange(snapshot.Media ?? []);
_backups.Clear();
_backups.AddRange(snapshot.Backups ?? []);
_loops.Clear();
_loops.AddRange(snapshot.Loops ?? []);
_people.Clear();
_people.AddRange(snapshot.People ?? []);
if (_robotProfile is null || !string.Equals(_robotProfile.RobotId, _robot.RobotId, StringComparison.OrdinalIgnoreCase))
{
_robotProfile = new RobotProfile
{
RobotId = _robot.RobotId,
Payload = new Dictionary<string, object?>
{
["SSID"] = "my-ssid",
["connectedAt"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
["platform"] = _robot.FirmwareVersion ?? "12.10.0",
["serialNumber"] = _robot.DeviceId
},
UpdatedUtc = DateTimeOffset.UtcNow
};
}
Interlocked.Exchange(ref _revision, snapshot.Revision);
_lastLoadedUtc = snapshot.LastLoadedUtc ?? DateTimeOffset.UtcNow;
_lastSavedUtc = snapshot.LastSavedUtc;
} }
catch
if (_devices.IsEmpty || !_devices.ContainsKey(_robot.DeviceId))
{ {
// Ignore corrupt state and continue with the in-memory defaults. _devices[_robot.DeviceId] = _robot;
} }
_sessionsByToken.Clear();
foreach (var session in snapshot.Sessions ?? [])
{
if (!string.IsNullOrWhiteSpace(session.Token))
{
_sessionsByToken[session.Token] = session.ToRecord();
}
}
_symmetricKeys.Clear();
foreach (var pair in snapshot.SymmetricKeys ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase))
{
_symmetricKeys[pair.Key] = pair.Value;
}
_keyRequests.Clear();
foreach (var keyRequest in snapshot.KeyRequests ?? [])
{
_keyRequests[keyRequest.RequestId] = keyRequest;
}
_updates.Clear();
_updates.AddRange(snapshot.Updates ?? []);
_media.Clear();
_media.AddRange(snapshot.Media ?? []);
_backups.Clear();
_backups.AddRange(snapshot.Backups ?? []);
_loops.Clear();
_loops.AddRange(snapshot.Loops ?? []);
_people.Clear();
_people.AddRange(snapshot.People ?? []);
if (_robotProfile is null || !string.Equals(_robotProfile.RobotId, _robot.RobotId, StringComparison.OrdinalIgnoreCase))
{
_robotProfile = new RobotProfile
{
RobotId = _robot.RobotId,
Payload = new Dictionary<string, object?>
{
["SSID"] = "my-ssid",
["connectedAt"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
["platform"] = _robot.FirmwareVersion ?? "12.10.0",
["serialNumber"] = _robot.DeviceId
},
UpdatedUtc = DateTimeOffset.UtcNow
};
}
Interlocked.Exchange(ref _revision, snapshot.Revision);
_lastLoadedUtc = snapshot.LastLoadedUtc ?? DateTimeOffset.UtcNow;
_lastSavedUtc = snapshot.LastSavedUtc;
} }
public void SavePersistedState() public void SavePersistedState()
{ {
if (string.IsNullOrWhiteSpace(_persistencePath))
{
return;
}
lock (_syncRoot) lock (_syncRoot)
{ {
var directory = Path.GetDirectoryName(_persistencePath);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
var now = DateTimeOffset.UtcNow; var now = DateTimeOffset.UtcNow;
var snapshot = new PersistentStateSnapshot var snapshot = new PersistentStateSnapshot
{ {
@@ -229,8 +206,7 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
Loops = _loops.ToArray(), Loops = _loops.ToArray(),
People = _people.ToArray() People = _people.ToArray()
}; };
_snapshotStore.Save(snapshot);
File.WriteAllText(_persistencePath, JsonSerializer.Serialize(snapshot, PersistenceJsonOptions));
_lastSavedUtc = now; _lastSavedUtc = now;
} }
} }

View File

@@ -13,7 +13,7 @@ public sealed class InMemoryPersonalMemoryStore : IPersonalMemoryStore
}; };
private readonly ConcurrentDictionary<string, TenantMemoryRecord> _tenantMemory = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, TenantMemoryRecord> _tenantMemory = new(StringComparer.OrdinalIgnoreCase);
private readonly string? _persistencePath; private readonly JsonSnapshotStore _snapshotStore;
private readonly Lock _syncRoot = new(); private readonly Lock _syncRoot = new();
private long _revision; private long _revision;
private DateTimeOffset? _lastLoadedUtc; private DateTimeOffset? _lastLoadedUtc;
@@ -21,7 +21,7 @@ public sealed class InMemoryPersonalMemoryStore : IPersonalMemoryStore
public InMemoryPersonalMemoryStore(string? persistencePath = null) public InMemoryPersonalMemoryStore(string? persistencePath = null)
{ {
_persistencePath = persistencePath; _snapshotStore = new JsonSnapshotStore(persistencePath, PersistenceJsonOptions);
LoadPersistedState(); LoadPersistedState();
} }
@@ -36,50 +36,27 @@ public sealed class InMemoryPersonalMemoryStore : IPersonalMemoryStore
public void LoadPersistedState() public void LoadPersistedState()
{ {
if (string.IsNullOrWhiteSpace(_persistencePath) || !File.Exists(_persistencePath)) var snapshot = _snapshotStore.Load<PersistentStateSnapshot>();
if (snapshot is null)
{ {
return; return;
} }
try _tenantMemory.Clear();
foreach (var tenant in snapshot.Tenants ?? [])
{ {
var snapshot = JsonSerializer.Deserialize<PersistentStateSnapshot>(File.ReadAllText(_persistencePath), PersistenceJsonOptions); _tenantMemory[tenant.TenantKey] = tenant.ToRecord();
if (snapshot is null)
{
return;
}
_tenantMemory.Clear();
foreach (var tenant in snapshot.Tenants ?? [])
{
_tenantMemory[tenant.TenantKey] = tenant.ToRecord();
}
Interlocked.Exchange(ref _revision, snapshot.Revision);
_lastLoadedUtc = snapshot.LastLoadedUtc ?? DateTimeOffset.UtcNow;
_lastSavedUtc = snapshot.LastSavedUtc;
}
catch
{
// Ignore corrupt state and continue with the in-memory defaults.
} }
Interlocked.Exchange(ref _revision, snapshot.Revision);
_lastLoadedUtc = snapshot.LastLoadedUtc ?? DateTimeOffset.UtcNow;
_lastSavedUtc = snapshot.LastSavedUtc;
} }
public void SavePersistedState() public void SavePersistedState()
{ {
if (string.IsNullOrWhiteSpace(_persistencePath))
{
return;
}
lock (_syncRoot) lock (_syncRoot)
{ {
var directory = Path.GetDirectoryName(_persistencePath);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
var now = DateTimeOffset.UtcNow; var now = DateTimeOffset.UtcNow;
var snapshot = new PersistentStateSnapshot var snapshot = new PersistentStateSnapshot
{ {
@@ -103,8 +80,7 @@ public sealed class InMemoryPersonalMemoryStore : IPersonalMemoryStore
}) })
.ToArray() .ToArray()
}; };
_snapshotStore.Save(snapshot);
File.WriteAllText(_persistencePath, JsonSerializer.Serialize(snapshot, PersistenceJsonOptions));
_lastSavedUtc = now; _lastSavedUtc = now;
} }
} }

View File

@@ -0,0 +1,48 @@
using System.Text.Json;
namespace Jibo.Cloud.Infrastructure.Persistence;
internal sealed class JsonSnapshotStore
{
private readonly string? _persistencePath;
private readonly JsonSerializerOptions _options;
public JsonSnapshotStore(string? persistencePath, JsonSerializerOptions options)
{
_persistencePath = persistencePath;
_options = options;
}
public TSnapshot? Load<TSnapshot>()
{
if (string.IsNullOrWhiteSpace(_persistencePath) || !File.Exists(_persistencePath))
{
return default;
}
try
{
return JsonSerializer.Deserialize<TSnapshot>(File.ReadAllText(_persistencePath), _options);
}
catch
{
return default;
}
}
public void Save<TSnapshot>(TSnapshot snapshot)
{
if (string.IsNullOrWhiteSpace(_persistencePath))
{
return;
}
var directory = Path.GetDirectoryName(_persistencePath);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
File.WriteAllText(_persistencePath, JsonSerializer.Serialize(snapshot, _options));
}
}