Add durable shard mutation log storage
This commit is contained in:
parent
d73c3d26e8
commit
2ff82365cb
8 changed files with 367 additions and 52 deletions
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using GameCult.Caching;
|
||||
using R3;
|
||||
|
|
@ -225,6 +226,10 @@ namespace GameCult.Networking
|
|||
/// Gets or sets the document registry used to create and apply raw CultNet document messages.
|
||||
/// </summary>
|
||||
public CultNetDocumentRegistry? DocumentRegistry { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the durable store for accepted shard mutation logs.
|
||||
/// </summary>
|
||||
public ICultNetShardMutationLogStore? MutationLogStore { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -385,6 +390,7 @@ namespace GameCult.Networking
|
|||
private readonly string _runtimeId;
|
||||
private readonly List<CultNetShardDescriptor> _shards;
|
||||
private readonly List<CultNetClientAuthorityScope> _clientAuthorityScopes;
|
||||
private readonly ICultNetShardMutationLogStore? _mutationLogStore;
|
||||
private readonly Dictionary<string, object> _predictedDocuments = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, List<CultNetShardMutationLogEntry>> _mutationLogs =
|
||||
new(StringComparer.Ordinal);
|
||||
|
|
@ -402,11 +408,13 @@ namespace GameCult.Networking
|
|||
options ??= new CultNetDatabaseOptions();
|
||||
_runtimeId = options.RuntimeId;
|
||||
_documents = options.DocumentRegistry ?? new CultNetDocumentRegistry(cache.Registry);
|
||||
_mutationLogStore = options.MutationLogStore;
|
||||
_shards = (options.Shards == null || options.Shards.Count == 0
|
||||
? new[] { CultNetShardDescriptor.PrimaryAll(options.RuntimeId) }
|
||||
: options.Shards)
|
||||
.ToList();
|
||||
_clientAuthorityScopes = (options.ClientAuthorityScopes ?? Array.Empty<CultNetClientAuthorityScope>()).ToList();
|
||||
InitializeLogSequencesFromStore();
|
||||
_cache.OnUpdate += PublishCacheUpdate;
|
||||
}
|
||||
|
||||
|
|
@ -452,6 +460,26 @@ namespace GameCult.Networking
|
|||
return query.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets replica catch-up log entries for a shard after the supplied sequence.
|
||||
/// </summary>
|
||||
public IReadOnlyList<CultNetShardLogEntryMessage> GetMutationLogMessages(
|
||||
string shardId,
|
||||
long afterSequence = 0,
|
||||
int? limit = null)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (string.IsNullOrWhiteSpace(shardId)) throw new ArgumentException("Value must be non-empty.", nameof(shardId));
|
||||
if (_mutationLogStore != null)
|
||||
{
|
||||
return _mutationLogStore.Read(shardId, afterSequence, limit);
|
||||
}
|
||||
|
||||
return GetMutationLog(shardId, afterSequence, limit)
|
||||
.Select(ToLogEntryMessage)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last replicated sequence applied for a shard.
|
||||
/// </summary>
|
||||
|
|
@ -639,7 +667,12 @@ namespace GameCult.Networking
|
|||
key,
|
||||
descriptor.SchemaId,
|
||||
document,
|
||||
previous);
|
||||
previous,
|
||||
new CultNetShardLogEntryMessage
|
||||
{
|
||||
ChangeKind = previous == null ? "added" : "updated",
|
||||
Put = message
|
||||
});
|
||||
PublishUntyped(
|
||||
descriptor.DocumentType,
|
||||
kind,
|
||||
|
|
@ -680,7 +713,12 @@ namespace GameCult.Networking
|
|||
key,
|
||||
descriptor.SchemaId,
|
||||
document: null,
|
||||
previousDocument: previous);
|
||||
previousDocument: previous,
|
||||
wireEntry: new CultNetShardLogEntryMessage
|
||||
{
|
||||
ChangeKind = "removed",
|
||||
Delete = message
|
||||
});
|
||||
PublishUntyped(
|
||||
descriptor.DocumentType,
|
||||
CultNetDatabaseChangeKind.Removed,
|
||||
|
|
@ -877,22 +915,30 @@ namespace GameCult.Networking
|
|||
CultRecordKey key,
|
||||
string schemaId,
|
||||
object? document,
|
||||
object? previousDocument)
|
||||
object? previousDocument,
|
||||
CultNetShardLogEntryMessage? wireEntry = null)
|
||||
{
|
||||
var sequence = NextMutationLogSequence(shard.ShardId);
|
||||
RecordMutationLogEntry(new CultNetShardMutationLogEntry(
|
||||
var committedAt = DateTimeOffset.UtcNow.ToString("O");
|
||||
var entry = new CultNetShardMutationLogEntry(
|
||||
shard.ShardId,
|
||||
shard.Epoch,
|
||||
sequence,
|
||||
DateTimeOffset.UtcNow.ToString("O"),
|
||||
committedAt,
|
||||
kind,
|
||||
schemaId,
|
||||
key,
|
||||
document,
|
||||
previousDocument));
|
||||
previousDocument);
|
||||
var storedWireEntry = _mutationLogStore == null
|
||||
? null
|
||||
: wireEntry ?? ToLogEntryMessage(entry);
|
||||
RecordMutationLogEntry(entry, storedWireEntry);
|
||||
}
|
||||
|
||||
private void RecordMutationLogEntry(CultNetShardMutationLogEntry entry)
|
||||
private void RecordMutationLogEntry(
|
||||
CultNetShardMutationLogEntry entry,
|
||||
CultNetShardLogEntryMessage? wireEntry = null)
|
||||
{
|
||||
if (!_mutationLogs.TryGetValue(entry.ShardId, out var entries))
|
||||
{
|
||||
|
|
@ -906,6 +952,11 @@ namespace GameCult.Networking
|
|||
{
|
||||
_nextLogSequences[entry.ShardId] = entry.Sequence + 1;
|
||||
}
|
||||
|
||||
if (_mutationLogStore != null && wireEntry != null)
|
||||
{
|
||||
_mutationLogStore.Append(entry.ShardId, NormalizeWireLogEntry(entry, wireEntry));
|
||||
}
|
||||
}
|
||||
|
||||
private long NextMutationLogSequence(string shardId)
|
||||
|
|
@ -917,6 +968,26 @@ namespace GameCult.Networking
|
|||
return sequence;
|
||||
}
|
||||
|
||||
private void InitializeLogSequencesFromStore()
|
||||
{
|
||||
if (_mutationLogStore == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var shard in _shards)
|
||||
{
|
||||
var highest = _mutationLogStore.Read(shard.ShardId)
|
||||
.Select(entry => entry.Sequence)
|
||||
.DefaultIfEmpty(0)
|
||||
.Max();
|
||||
if (highest > 0)
|
||||
{
|
||||
_nextLogSequences[shard.ShardId] = highest + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyCommittedShardLogEntryAsync(
|
||||
CultNetShardDescriptor shard,
|
||||
CultNetShardLogEntryMessage entry)
|
||||
|
|
@ -983,7 +1054,8 @@ namespace GameCult.Networking
|
|||
descriptor.SchemaId,
|
||||
key,
|
||||
document,
|
||||
previous));
|
||||
previous),
|
||||
entry);
|
||||
PublishUntyped(
|
||||
descriptor.DocumentType,
|
||||
kind,
|
||||
|
|
@ -1029,7 +1101,8 @@ namespace GameCult.Networking
|
|||
descriptor.SchemaId,
|
||||
key,
|
||||
document: null,
|
||||
previousDocument: previous));
|
||||
previousDocument: previous),
|
||||
entry);
|
||||
PublishUntyped(
|
||||
descriptor.DocumentType,
|
||||
CultNetDatabaseChangeKind.Removed,
|
||||
|
|
@ -1040,6 +1113,93 @@ namespace GameCult.Networking
|
|||
previousDocument: previous);
|
||||
}
|
||||
|
||||
private CultNetShardLogEntryMessage ToLogEntryMessage(CultNetShardMutationLogEntry entry)
|
||||
{
|
||||
if (entry.Kind == CultNetDatabaseChangeKind.Removed || entry.Document == null)
|
||||
{
|
||||
return new CultNetShardLogEntryMessage
|
||||
{
|
||||
Sequence = entry.Sequence,
|
||||
CommittedAt = entry.CommittedAt,
|
||||
ChangeKind = "removed",
|
||||
Delete = new CultNetDocumentDeleteMessage
|
||||
{
|
||||
MessageId = Guid.NewGuid().ToString("N"),
|
||||
SchemaId = entry.SchemaId,
|
||||
RecordKey = entry.Key.Value,
|
||||
ShardId = entry.ShardId,
|
||||
ShardEpoch = entry.ShardEpoch
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return new CultNetShardLogEntryMessage
|
||||
{
|
||||
Sequence = entry.Sequence,
|
||||
CommittedAt = entry.CommittedAt,
|
||||
ChangeKind = entry.Kind == CultNetDatabaseChangeKind.Added ? "added" : "updated",
|
||||
Put = new CultNetDocumentPutRawMessage
|
||||
{
|
||||
MessageId = Guid.NewGuid().ToString("N"),
|
||||
Document = CreateRawDocumentRecord(entry.Key, entry.Document),
|
||||
ShardId = entry.ShardId,
|
||||
ShardEpoch = entry.ShardEpoch
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private CultNetShardLogEntryMessage NormalizeWireLogEntry(
|
||||
CultNetShardMutationLogEntry entry,
|
||||
CultNetShardLogEntryMessage wireEntry)
|
||||
{
|
||||
return new CultNetShardLogEntryMessage
|
||||
{
|
||||
Sequence = entry.Sequence,
|
||||
CommittedAt = string.IsNullOrWhiteSpace(entry.CommittedAt) ? wireEntry.CommittedAt : entry.CommittedAt,
|
||||
ChangeKind = entry.Kind == CultNetDatabaseChangeKind.Removed
|
||||
? "removed"
|
||||
: entry.Kind == CultNetDatabaseChangeKind.Added ? "added" : "updated",
|
||||
Put = wireEntry.Put == null
|
||||
? null
|
||||
: new CultNetDocumentPutRawMessage
|
||||
{
|
||||
MessageId = string.IsNullOrWhiteSpace(wireEntry.Put.MessageId)
|
||||
? Guid.NewGuid().ToString("N")
|
||||
: wireEntry.Put.MessageId,
|
||||
Document = wireEntry.Put.Document,
|
||||
ShardId = entry.ShardId,
|
||||
ShardEpoch = entry.ShardEpoch
|
||||
},
|
||||
Delete = wireEntry.Delete == null
|
||||
? null
|
||||
: new CultNetDocumentDeleteMessage
|
||||
{
|
||||
MessageId = string.IsNullOrWhiteSpace(wireEntry.Delete.MessageId)
|
||||
? Guid.NewGuid().ToString("N")
|
||||
: wireEntry.Delete.MessageId,
|
||||
SchemaId = wireEntry.Delete.SchemaId,
|
||||
RecordKey = wireEntry.Delete.RecordKey,
|
||||
ShardId = entry.ShardId,
|
||||
ShardEpoch = entry.ShardEpoch
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private CultNetRawDocumentRecord CreateRawDocumentRecord(CultRecordKey key, object document)
|
||||
{
|
||||
var method = typeof(CultNetDocumentRegistry)
|
||||
.GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Single(candidate => candidate.Name == nameof(CultNetDocumentRegistry.CreateRawDocumentPutMessage) &&
|
||||
candidate.IsGenericMethodDefinition);
|
||||
var documentType = document.GetType();
|
||||
var handleType = typeof(CultRecordHandle<>).MakeGenericType(documentType);
|
||||
var handle = Activator.CreateInstance(handleType, new object[] { key });
|
||||
var message = method
|
||||
.MakeGenericMethod(documentType)
|
||||
.Invoke(_documents, new[] { Guid.NewGuid().ToString("N"), handle, document, null });
|
||||
return ((CultNetDocumentPutRawMessage)message!).Document;
|
||||
}
|
||||
|
||||
private static CultNetDatabaseChangeKind ChangeKindFromWire(string changeKind)
|
||||
{
|
||||
return changeKind switch
|
||||
|
|
|
|||
Loading…
Reference in a new issue