diff --git a/src/GameCult.Networking/CultNetDatabase.cs b/src/GameCult.Networking/CultNetDatabase.cs index c5221359..74932da9 100644 --- a/src/GameCult.Networking/CultNetDatabase.cs +++ b/src/GameCult.Networking/CultNetDatabase.cs @@ -298,11 +298,6 @@ namespace GameCult.Networking /// Gets or sets the durable store for accepted shard mutation logs. /// public ICultNetShardMutationLogStore? MutationLogStore { get; set; } - /// - /// Gets or sets whether authoritative writes must occur inside ExecuteTransactionAsync. - /// Use this for state owners that forbid record-at-a-time mutation bypasses. - /// - public bool RequireTransactionsForAuthoritativeWrites { get; set; } } /// @@ -470,8 +465,6 @@ namespace GameCult.Networking private readonly Dictionary _nextLogSequences = new(StringComparer.Ordinal); private readonly Dictionary _appliedShardSequences = new(StringComparer.Ordinal); private readonly Subject _changes = new(); - private readonly AsyncLocal _ambientTransaction = new(); - private readonly bool _requireTransactionsForAuthoritativeWrites; private bool _disposed; /// @@ -484,7 +477,6 @@ namespace GameCult.Networking _runtimeId = options.RuntimeId; _documents = options.DocumentRegistry ?? new CultNetDocumentRegistry(cache.Registry); _mutationLogStore = options.MutationLogStore; - _requireTransactionsForAuthoritativeWrites = options.RequireTransactionsForAuthoritativeWrites; _shards = (options.Shards == null || options.Shards.Count == 0 ? new[] { CultNetShardDescriptor.PrimaryAll(options.RuntimeId) } : options.Shards) @@ -512,49 +504,6 @@ namespace GameCult.Networking /// public IReadOnlyList ClientAuthorityScopes => _clientAuthorityScopes; - /// - /// Runs one buffered authoritative database mutation. Cache state, durable state, - /// mutation logs, and database observers advance only after the complete stage succeeds. - /// - public async Task ExecuteTransactionAsync(Func stageAsync, bool soft = false) - { - ThrowIfDisposed(); - if (stageAsync == null) throw new ArgumentNullException(nameof(stageAsync)); - if (_ambientTransaction.Value != null) - { - await stageAsync().ConfigureAwait(false); - return; - } - - var transaction = new CultNetDatabaseTransaction(); - IReadOnlyList afterCommit; - _ambientTransaction.Value = transaction; - try - { - await _cache.ExecuteTransactionAsync(stageAsync, soft).ConfigureAwait(false); - afterCommit = transaction.AfterCommit.ToArray(); - } - finally - { - _ambientTransaction.Value = null; - } - - foreach (var publish in afterCommit) - publish(); - } - - /// Runs one value-producing buffered authoritative database mutation. - public async Task ExecuteTransactionAsync(Func> stageAsync, bool soft = false) - { - if (stageAsync == null) throw new ArgumentNullException(nameof(stageAsync)); - T result = default!; - await ExecuteTransactionAsync(async () => - { - result = await stageAsync().ConfigureAwait(false); - }, soft).ConfigureAwait(false); - return result; - } - /// Gets whether this database instance can authoritatively replace the document at a key. public bool CanWriteAuthoritatively(CultRecordKey key) where T : class { @@ -854,7 +803,6 @@ namespace GameCult.Networking public async Task> PutAsync(CultRecordKey key, T document) where T : class { ThrowIfDisposed(); - EnsureAuthoritativeTransaction(); if (document == null) throw new ArgumentNullException(nameof(document)); var descriptor = _cache.Registry.GetRequired(); @@ -880,10 +828,7 @@ namespace GameCult.Networking document, previous)); } - if (_ambientTransaction.Value is { } transaction) - transaction.AfterCommit.Add(PublishCommittedPut); - else - PublishCommittedPut(); + PublishCommittedPut(); return handle; } @@ -918,7 +863,6 @@ namespace GameCult.Networking public Task DeleteAsync(CultRecordKey key) where T : class { ThrowIfDisposed(); - EnsureAuthoritativeTransaction(); var descriptor = _cache.Registry.GetRequired(); var shard = ResolveShardInternal(descriptor, key); EnsurePrimary(shard, descriptor.SchemaId, key); @@ -944,10 +888,7 @@ namespace GameCult.Networking document: null, previousDocument: previous)); } - if (_ambientTransaction.Value is { } transaction) - transaction.AfterCommit.Add(PublishCommittedDelete); - else - PublishCommittedDelete(); + PublishCommittedDelete(); } return Task.CompletedTask; @@ -959,7 +900,6 @@ namespace GameCult.Networking public async Task ApplyPutAsync(CultNetDocumentPutRawMessage message) { ThrowIfDisposed(); - EnsureAuthoritativeTransaction(); if (message == null) throw new ArgumentNullException(nameof(message)); if (message.Document == null) { @@ -1005,10 +945,7 @@ namespace GameCult.Networking document, previous); } - if (_ambientTransaction.Value is { } transaction) - transaction.AfterCommit.Add(PublishCommittedPut); - else - PublishCommittedPut(); + PublishCommittedPut(); return document; } @@ -1018,7 +955,6 @@ namespace GameCult.Networking public Task ApplyDeleteAsync(CultNetDocumentDeleteMessage message) { ThrowIfDisposed(); - EnsureAuthoritativeTransaction(); if (message == null) throw new ArgumentNullException(nameof(message)); var key = new CultRecordKey(message.RecordKey); @@ -1055,10 +991,7 @@ namespace GameCult.Networking document: null, previousDocument: previous); } - if (_ambientTransaction.Value is { } transaction) - transaction.AfterCommit.Add(PublishCommittedDelete); - else - PublishCommittedDelete(); + PublishCommittedDelete(); return Task.CompletedTask; } @@ -1771,20 +1704,6 @@ namespace GameCult.Networking }; } - private sealed class CultNetDatabaseTransaction - { - public List AfterCommit { get; } = new(); - } - - private void EnsureAuthoritativeTransaction() - { - if (_requireTransactionsForAuthoritativeWrites && _ambientTransaction.Value == null) - { - throw new InvalidOperationException( - "This CultNet database requires authoritative writes to use ExecuteTransactionAsync."); - } - } - private void ThrowIfDisposed() { if (_disposed) diff --git a/tests/GameCult.Networking.Tests/NetworkingTests.cs b/tests/GameCult.Networking.Tests/NetworkingTests.cs index 56681839..99b54710 100644 --- a/tests/GameCult.Networking.Tests/NetworkingTests.cs +++ b/tests/GameCult.Networking.Tests/NetworkingTests.cs @@ -2957,60 +2957,6 @@ namespace GameCult.Networking.Tests Assert.That(changes[0].Document!.Username, Is.EqualTo("Raw")); } - [Test] - public async Task CultNetDatabase_RequiredTransaction_DefersRawPutPublicationUntilCommit() - { - var cache = new CultCache(); - var registry = new CultNetDocumentRegistry(cache.Registry) - .Register(CultNetDocumentBinding.ForDocument( - cache.Registry, - payloadSerializer: SerializePlayerDataPayload, - payloadDeserializer: DeserializePlayerDataPayload)); - var database = new CultNetDatabase(cache, new CultNetDatabaseOptions - { - DocumentRegistry = registry, - RequireTransactionsForAuthoritativeWrites = true - }); - var key = new CultRecordKey("player:transactional-raw"); - var message = registry.CreateRawDocumentPutMessage( - "put-transactional-raw", - new CultRecordHandle(key), - new PlayerData - { - PlayerId = Guid.NewGuid(), - Email = "transactional@example.test", - PasswordHash = "hash", - Username = "Transactional" - }); - var changes = new List>(); - using var subscription = database.WatchRecord(key) - .Subscribe(change => changes.Add(change)); - - Assert.That( - async () => await database.ApplyPutAsync(message), - Throws.TypeOf()); - - var staged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var transaction = database.ExecuteTransactionAsync(async () => - { - await database.ApplyPutAsync(message); - staged.SetResult(true); - await release.Task; - }); - await staged.Task; - - Assert.That(cache.Get(key), Is.Null); - Assert.That(changes, Is.Empty); - - release.SetResult(true); - await transaction; - - Assert.That(cache.Get(key)?.Username, Is.EqualTo("Transactional")); - Assert.That(changes, Has.Count.EqualTo(1)); - Assert.That(changes[0].Kind, Is.EqualTo(CultNetDatabaseChangeKind.Added)); - } - [Test] public async Task CultNetDatabase_ApplyRawPut_ResolvesForeignSchemaIdFromPayload() { @@ -4171,7 +4117,7 @@ namespace GameCult.Networking.Tests { Assert.That(new CultMeshRudpSocketOptions().BindHost, Is.EqualTo("0.0.0.0")); } - + [Test] public void CultMeshRudpFacade_CreatesAuthorizedClientFromPeerEndpoint() {