Cut 4 Soul 4c: committed-unlogged wire writes, database serves only what it owns, typed unowned refusal
Q1: HandlePutAsync/HandleDeleteAsync catch a CultNetShardLogException (or an AggregateException holding one) before the generic catch and answer nothing: the write committed and was published, so the peer is not told it was refused. It is logged at warning level. The wire has no committed-with-degraded-log message. Q2: GetAsync/GetAll/GetByName/GetByIndex, the shard-less snapshot, the selection and the subscription snapshot serve only rows a shard owns, so read and watch agree; the CultMesh collections follow through GetAll, and the Mesh untyped variant fallback checks ownership. The raw cache still holds everything. Q3: a replica log apply skips a row its shard does not own, as the snapshot path does, and still records the entry so a chained replica sees no gap. API: rowFilter is internal (public CreateSelectionResponse and CreateRawSnapshotResponse signatures restored, internal overloads added); exception tags on the write methods; Commit's unwrap rule is XML doc; the unowned refusal answers on the wire with code unowned_schema (schema enum in both copies, Rust CultNetErrorCode, CultNetErrorMessage.ForUnownedSchema) and put and delete log it at warning level. Dispose keeps journal-first; a test now fails if the observer goes first. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
396cccafd4
commit
557e70fd03
12 changed files with 467 additions and 37 deletions
|
|
@ -112,10 +112,13 @@ namespace GameCult.Networking
|
|||
request);
|
||||
}
|
||||
|
||||
// A database serves only what its shards own.
|
||||
return _database.Documents.CreateRawSnapshotResponse(
|
||||
_database.Cache,
|
||||
string.IsNullOrWhiteSpace(request.MessageId) ? Guid.NewGuid().ToString("N") : request.MessageId,
|
||||
request);
|
||||
request,
|
||||
options: null,
|
||||
rowFilter: _database.Owns);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -136,6 +139,7 @@ namespace GameCult.Networking
|
|||
// 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.
|
||||
options: null,
|
||||
rowFilter: _database.Owns,
|
||||
shardIdOf: (schemaId, key) => _database.ResolveShard(schemaId, key).ShardId,
|
||||
cursorKey: _database.CursorKey,
|
||||
|
|
@ -314,6 +318,16 @@ namespace GameCult.Networking
|
|||
peer.SendCultNet(CreateRoutingError(ex));
|
||||
}
|
||||
}
|
||||
catch (CultNetUnownedSchemaException ex)
|
||||
{
|
||||
_server.Logger.LogWarning($"CultNet raw put refused: {ex.Message}");
|
||||
peer.SendCultNet(CultNetErrorMessage.ForUnownedSchema(ex));
|
||||
}
|
||||
catch (Exception ex) when (CommittedWithLogFailure(ex))
|
||||
{
|
||||
// The write committed and was published; only its shard log failed, so the peer is not told it was refused.
|
||||
_server.Logger.LogWarning($"CultNet raw put committed but its shard log failed: {ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_server.Logger.LogError($"CultNet raw put failed: {ex.Message}");
|
||||
|
|
@ -334,6 +348,15 @@ namespace GameCult.Networking
|
|||
peer.SendCultNet(CreateRoutingError(ex));
|
||||
}
|
||||
}
|
||||
catch (CultNetUnownedSchemaException ex)
|
||||
{
|
||||
_server.Logger.LogWarning($"CultNet raw delete refused: {ex.Message}");
|
||||
peer.SendCultNet(CultNetErrorMessage.ForUnownedSchema(ex));
|
||||
}
|
||||
catch (Exception ex) when (CommittedWithLogFailure(ex))
|
||||
{
|
||||
_server.Logger.LogWarning($"CultNet raw delete committed but its shard log failed: {ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
peer.SendCultNet(new CultNetErrorMessage { Error = ex.Message });
|
||||
|
|
@ -606,6 +629,12 @@ namespace GameCult.Networking
|
|||
: messageId;
|
||||
}
|
||||
|
||||
// A CultNetShardLogException means the write committed and was published (the writer hears of a failed log afterwards);
|
||||
// the cache wraps it in an AggregateException when something else failed in the same hold.
|
||||
private static bool CommittedWithLogFailure(Exception exception) =>
|
||||
exception is CultNetShardLogException ||
|
||||
exception is AggregateException aggregate && aggregate.Flatten().InnerExceptions.Any(inner => inner is CultNetShardLogException);
|
||||
|
||||
private static CultNetErrorMessage CreateRoutingError(CultNetShardAuthorityException exception)
|
||||
{
|
||||
return new CultNetErrorMessage
|
||||
|
|
|
|||
Loading…
Reference in a new issue