Introduce pluggable snapshot storage for persistence

This commit is contained in:
Jacob Dubin
2026-05-17 07:02:49 -05:00
parent 785dc2b48b
commit 888f472f69
5 changed files with 67 additions and 8 deletions

View File

@@ -0,0 +1,7 @@
namespace Jibo.Cloud.Infrastructure.Persistence;
public interface ISnapshotStore
{
TSnapshot? Load<TSnapshot>() where TSnapshot : class;
void Save<TSnapshot>(TSnapshot snapshot) where TSnapshot : class;
}

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 JsonSnapshotStore _snapshotStore; private readonly ISnapshotStore _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 = [];
@@ -32,8 +32,13 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
private DateTimeOffset? _lastSavedUtc; private DateTimeOffset? _lastSavedUtc;
public InMemoryCloudStateStore(string? persistencePath = null) public InMemoryCloudStateStore(string? persistencePath = null)
: this(new JsonFileSnapshotStore(persistencePath, PersistenceJsonOptions))
{ {
_snapshotStore = new JsonSnapshotStore(persistencePath, PersistenceJsonOptions); }
public InMemoryCloudStateStore(ISnapshotStore snapshotStore)
{
_snapshotStore = snapshotStore;
_robot = new DeviceRegistration _robot = new DeviceRegistration
{ {
HostMappings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) HostMappings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)

View File

@@ -13,15 +13,20 @@ 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 JsonSnapshotStore _snapshotStore; private readonly ISnapshotStore _snapshotStore;
private readonly Lock _syncRoot = new(); private readonly Lock _syncRoot = new();
private long _revision; private long _revision;
private DateTimeOffset? _lastLoadedUtc; private DateTimeOffset? _lastLoadedUtc;
private DateTimeOffset? _lastSavedUtc; private DateTimeOffset? _lastSavedUtc;
public InMemoryPersonalMemoryStore(string? persistencePath = null) public InMemoryPersonalMemoryStore(string? persistencePath = null)
: this(new JsonFileSnapshotStore(persistencePath, PersistenceJsonOptions))
{ {
_snapshotStore = new JsonSnapshotStore(persistencePath, PersistenceJsonOptions); }
public InMemoryPersonalMemoryStore(ISnapshotStore snapshotStore)
{
_snapshotStore = snapshotStore;
LoadPersistedState(); LoadPersistedState();
} }

View File

@@ -2,18 +2,18 @@ using System.Text.Json;
namespace Jibo.Cloud.Infrastructure.Persistence; namespace Jibo.Cloud.Infrastructure.Persistence;
internal sealed class JsonSnapshotStore internal sealed class JsonFileSnapshotStore : ISnapshotStore
{ {
private readonly string? _persistencePath; private readonly string? _persistencePath;
private readonly JsonSerializerOptions _options; private readonly JsonSerializerOptions _options;
public JsonSnapshotStore(string? persistencePath, JsonSerializerOptions options) public JsonFileSnapshotStore(string? persistencePath, JsonSerializerOptions options)
{ {
_persistencePath = persistencePath; _persistencePath = persistencePath;
_options = options; _options = options;
} }
public TSnapshot? Load<TSnapshot>() public TSnapshot? Load<TSnapshot>() where TSnapshot : class
{ {
if (string.IsNullOrWhiteSpace(_persistencePath) || !File.Exists(_persistencePath)) if (string.IsNullOrWhiteSpace(_persistencePath) || !File.Exists(_persistencePath))
{ {
@@ -30,7 +30,7 @@ internal sealed class JsonSnapshotStore
} }
} }
public void Save<TSnapshot>(TSnapshot snapshot) public void Save<TSnapshot>(TSnapshot snapshot) where TSnapshot : class
{ {
if (string.IsNullOrWhiteSpace(_persistencePath)) if (string.IsNullOrWhiteSpace(_persistencePath))
{ {

View File

@@ -5,6 +5,33 @@ namespace Jibo.Cloud.Tests.Infrastructure;
public sealed class PersistenceStoreTests public sealed class PersistenceStoreTests
{ {
[Fact]
public void PersonalMemoryStore_CanUseAlternateSnapshotBackend()
{
var backend = new RecordingSnapshotStore();
var store = new InMemoryPersonalMemoryStore(backend);
var scope = new PersonalMemoryTenantScope("acct-b", "loop-b", "device-b", "person-b");
store.SetName(scope, "Alt Backend");
Assert.Single(backend.Saves);
Assert.Equal("Alt Backend", store.GetName(scope));
Assert.Equal("1", store.GetPersistenceStateInfo().SchemaVersion);
}
[Fact]
public void CloudStateStore_CanUseAlternateSnapshotBackend()
{
var backend = new RecordingSnapshotStore();
var store = new InMemoryCloudStateStore(backend);
store.CreateMedia("openjibo-default-loop", "backend-photo", "image", "photo-ref", false, null);
Assert.Single(backend.Saves);
Assert.Contains(store.ListMedia(), item => item.Path == "backend-photo");
Assert.Equal("1", store.GetPersistenceStateInfo().SchemaVersion);
}
[Fact] [Fact]
public void PersonalMemoryStore_RoundTripsStateAndRevision() public void PersonalMemoryStore_RoundTripsStateAndRevision()
{ {
@@ -84,4 +111,19 @@ public sealed class PersistenceStoreTests
} }
} }
} }
private sealed class RecordingSnapshotStore : ISnapshotStore
{
public List<object> Saves { get; } = [];
public TSnapshot2? Load<TSnapshot2>() where TSnapshot2 : class
{
return default;
}
public void Save<TSnapshot2>(TSnapshot2 snapshot) where TSnapshot2 : class
{
Saves.Add(snapshot);
}
}
} }