CultNetDatabaseServer: a selection skips cached rows no shard owns instead of failing
ResolveShard now throws for an unowned schema, so one such cached row would have failed the whole selection. The server passes a row filter (CultNetDatabase.Owns) to CreateSelectionResponse, which gains an optional rowFilter that SelectPage already took, so those rows are not part of what the database serves and the rest is answered. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e79882a8ec
commit
dac712b09c
4 changed files with 29 additions and 2 deletions
|
|
@ -1922,6 +1922,8 @@ namespace GameCult.Networking
|
|||
private CultNetShardDescriptor ResolveShardInternal(CultDocumentDescriptor descriptor, CultRecordKey key) =>
|
||||
FindShard(descriptor, key) ?? throw new CultNetUnownedSchemaException(descriptor.SchemaId, key);
|
||||
|
||||
internal bool Owns(CultDocumentDescriptor descriptor, CultRecordKey key) => FindShard(descriptor, key) != null;
|
||||
|
||||
private CultNetShardDescriptor? FindShard(CultDocumentDescriptor descriptor, CultRecordKey key) =>
|
||||
_shards.FirstOrDefault(shard => shard.Matches(descriptor, key));
|
||||
|
||||
|
|
|
|||
|
|
@ -135,6 +135,8 @@ namespace GameCult.Networking
|
|||
// R-Q: asOf is one shard-log watermark, so a selection whose matched rows span more than
|
||||
// one shard's log is refused rather than answered against a watermark that is not
|
||||
// exact for all of them.
|
||||
// A row whose schema no shard owns is not part of what this database serves: it is left out, not an error.
|
||||
rowFilter: _database.Owns,
|
||||
shardIdOf: (schemaId, key) => _database.ResolveShard(schemaId, key).ShardId,
|
||||
cursorKey: _database.CursorKey,
|
||||
// S-9: once the matched rows resolve to one shard, asOf is that shard's own watermark,
|
||||
|
|
|
|||
|
|
@ -338,6 +338,7 @@ namespace GameCult.Networking
|
|||
/// row's ordinal (the reference: <see cref="CultNetDatabase.LastWriteSequence"/>); <paramref name="asOf"/>
|
||||
/// is the snapshot the page is exact for when the selection is not shard-scoped, or the fallback
|
||||
/// used only when <paramref name="asOfForShard"/> is null or the selection matches no rows (S-9).
|
||||
/// <paramref name="rowFilter"/>, when supplied, leaves out the cached rows it rejects before evaluation.
|
||||
/// </summary>
|
||||
public CultNetSnapshotResponseRawV1Message CreateSelectionResponse(
|
||||
CultCache cache,
|
||||
|
|
@ -348,13 +349,14 @@ namespace GameCult.Networking
|
|||
CultNetDocumentMessageOptions? options = null,
|
||||
Func<string, CultRecordKey, string>? shardIdOf = null,
|
||||
CultNetSelectionCursorKey? cursorKey = null,
|
||||
Func<string, ulong>? asOfForShard = null)
|
||||
Func<string, ulong>? asOfForShard = null,
|
||||
Func<CultDocumentDescriptor, CultRecordKey, bool>? rowFilter = null)
|
||||
{
|
||||
if (cache == null) throw new ArgumentNullException(nameof(cache));
|
||||
if (selection == null) throw new ArgumentNullException(nameof(selection));
|
||||
if (ordinalOf == null) throw new ArgumentNullException(nameof(ordinalOf));
|
||||
|
||||
var page = SelectPage(cache, selection, ordinalOf, asOf, options, shardIdOf: shardIdOf, cursorKey: cursorKey, asOfForShard: asOfForShard);
|
||||
var page = SelectPage(cache, selection, ordinalOf, asOf, options, rowFilter, shardIdOf, cursorKey, asOfForShard);
|
||||
return new CultNetSnapshotResponseRawV1Message
|
||||
{
|
||||
MessageId = RequireNonEmpty(messageId, nameof(messageId)),
|
||||
|
|
|
|||
|
|
@ -745,6 +745,27 @@ namespace GameCult.Networking.Tests
|
|||
Assert.That(cache.Get<MeshQuickstartNote>(Unowned), Is.Null);
|
||||
}
|
||||
|
||||
// One cached row of a schema no shard owns is not part of what this database serves: the selection answers with the
|
||||
// rest, it does not fail as a whole.
|
||||
[Test]
|
||||
public async Task ASelectionSkipsACachedRowNoShardOwnsAndAnswersWithTheRest()
|
||||
{
|
||||
var (database, cache, _, _) = OwnedNothingOfNote();
|
||||
await database.PutAsync(One, Note("owned"));
|
||||
await cache.UpsertAsync(new MeshQuickstartNote { NoteId = "u", Body = "unowned" }, new CultRecordHandle<MeshQuickstartNote>(Unowned));
|
||||
using var server = new Server(cache, ServerSecurityOptions.Development());
|
||||
using var databaseServer = new CultNetDatabaseServer(server, database);
|
||||
|
||||
var response = databaseServer.CreateSelectionResponse(new CultNetSnapshotRequestV1Message
|
||||
{
|
||||
MessageId = "select-all",
|
||||
Selection = new CultNetSelection { Projection = CultNetSelectionProjections.Document }
|
||||
});
|
||||
|
||||
Assert.That(response.Documents!.Select(document => document.RecordKey), Is.EqualTo(new[] { One.Value }));
|
||||
Assert.That(response.Matched, Is.EqualTo(1u));
|
||||
}
|
||||
|
||||
private sealed class CapturingLogger : GameCult.Logging.ILogger
|
||||
{
|
||||
public List<string> Errors { get; } = new();
|
||||
|
|
|
|||
Loading…
Reference in a new issue