From 002fbdf3570f533a8728d2a8f808025065d99532 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 20 Jul 2026 00:04:45 +0100 Subject: [PATCH] Retry transient provider bootstrap connections --- .../EveUnityCultMeshLiveProviderTransport.cs | 59 ++++++++++----- .../EveUnitySceneLiveProviderBridge.cs | 14 +++- .../EveUnitySceneSurfaceLowererTests.cs | 71 +++++++++++++++++-- .../org.gamecult.eve.unity-scene/package.json | 4 +- 4 files changed, 119 insertions(+), 29 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 80a4539..404701b 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Collections.Concurrent; +using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using GameCult.Caching; @@ -190,16 +191,27 @@ namespace GameCult.Eve.UnityScene if (_bootstrapped) return; - var elapsed = Stopwatch.StartNew(); - ResolveAdvertisement(forceRefresh: true); - TraceStartup("advertisement", elapsed); - RefreshSurface(); - TraceStartup("surface", elapsed); - RefreshAssetCatalog(); - TraceStartup("asset-catalog-and-bundles", elapsed); - EnsureLiveSubscriptions(); - TraceStartup("subscriptions", elapsed); - _bootstrapped = true; + try + { + _snapshot ??= CreateSnapshotSession(); + var elapsed = Stopwatch.StartNew(); + ResolveAdvertisement(forceRefresh: true); + TraceStartup("advertisement", elapsed); + RefreshSurface(); + TraceStartup("surface", elapsed); + RefreshAssetCatalog(); + TraceStartup("asset-catalog-and-bundles", elapsed); + EnsureLiveSubscriptions(); + TraceStartup("subscriptions", elapsed); + _bootstrapped = true; + } + catch (Exception error) when (error is IOException || error is SocketException || error is TimeoutException) + { + ResetSnapshotSession(); + throw new InvalidOperationException( + $"CultMesh provider '{_providerId}' bootstrap transport failed; the snapshot lane was reset for retry.", + error); + } } private static void TraceStartup(string phase, Stopwatch elapsed) @@ -510,7 +522,19 @@ namespace GameCult.Eve.UnityScene }) .GetAwaiter() .GetResult(); - _snapshot = CultMesh.SnapshotSession( + _snapshot = CreateSnapshotSession(); + _meshClient = new CultMeshClient(new CultMeshClientOptions + { + RendezvousEndpoints = new[] { _endpoint }, + RealtimeConnectors = new ICultMeshRealtimeTransportConnector[] + { + new CultMeshNativeQuicRealtimeTransportConnector() + } + }); + } + + private CultMeshSnapshotSession CreateSnapshotSession() => + CultMesh.SnapshotSession( _endpoint, new CultMeshSnapshotRequestOptions { @@ -523,14 +547,11 @@ namespace GameCult.Eve.UnityScene RudpMaxFragmentBytes = 1024 }, _networkRegistry); - _meshClient = new CultMeshClient(new CultMeshClientOptions - { - RendezvousEndpoints = new[] { _endpoint }, - RealtimeConnectors = new ICultMeshRealtimeTransportConnector[] - { - new CultMeshNativeQuicRealtimeTransportConnector() - } - }); + + private void ResetSnapshotSession() + { + _snapshot?.Dispose(); + _snapshot = null; } private void ResolveAdvertisement(bool forceRefresh = false) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneLiveProviderBridge.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneLiveProviderBridge.cs index 59814ca..7ccaea0 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneLiveProviderBridge.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneLiveProviderBridge.cs @@ -239,8 +239,18 @@ namespace GameCult.Eve.UnityScene _transport.SurfaceDocumentAvailable += OnSurfaceDocumentAvailable; _transport.AssetManifestDocumentAvailable += OnAssetManifestDocumentAvailable; _transport.CommandReceiptAvailable += OnCommandReceiptAvailable; - _transport.Connect(); - _connected = true; + try + { + _transport.Connect(); + _connected = true; + } + catch + { + _transport.SurfaceDocumentAvailable -= OnSurfaceDocumentAvailable; + _transport.AssetManifestDocumentAvailable -= OnAssetManifestDocumentAvailable; + _transport.CommandReceiptAvailable -= OnCommandReceiptAvailable; + throw; + } } public void Disconnect() diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 9a25ac0..bd12229 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; using GameCult.Caching; @@ -981,6 +982,37 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(transport.RefreshCount, Is.EqualTo(0)); } + [Test] + public void LiveProviderBridgeRollsBackSubscriptionsWhenTransportConnectFails() + { + var transport = new FakeLiveProviderTransport( + new EveUnitySceneProviderSurfaceDocument( + PlayableArpgDocument(), + Advertisement("aetheria.daemon.game"), + "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", + 1), + new EveUnityPlayableWorldAssetManifestDocument( + "cultmesh://aetheria/assets/manifest", + Array.Empty(), + "aetheria")) + { + FailNextConnect = true + }; + using var bridge = new EveUnitySceneLiveProviderBridge(transport); + + Assert.Throws(() => bridge.Connect()); + Assert.That(transport.SurfaceSubscriberCount, Is.Zero); + Assert.That(transport.AssetManifestSubscriberCount, Is.Zero); + Assert.That(transport.ReceiptSubscriberCount, Is.Zero); + + bridge.Connect(); + + Assert.That(transport.ConnectCount, Is.EqualTo(2)); + Assert.That(transport.SurfaceSubscriberCount, Is.EqualTo(1)); + Assert.That(transport.AssetManifestSubscriberCount, Is.EqualTo(1)); + Assert.That(transport.ReceiptSubscriberCount, Is.EqualTo(1)); + } + [Test] public void PlayableWorldClientHostMountsInterfaceProviderWithoutProviderTypes() { @@ -2817,6 +2849,10 @@ namespace GameCult.Eve.UnityScene.Tests private sealed class FakeLiveProviderTransport : IEveUnitySceneLiveProviderTransport { + private Action? _surfaceDocumentAvailable; + private Action? _assetManifestDocumentAvailable; + private Action? _commandReceiptAvailable; + public FakeLiveProviderTransport( EveUnitySceneProviderSurfaceDocument surfaceDocument, EveUnityPlayableWorldAssetManifestDocument assetManifestDocument) @@ -2843,15 +2879,38 @@ namespace GameCult.Eve.UnityScene.Tests public int RefreshCount { get; private set; } - public event Action? SurfaceDocumentAvailable; + public bool FailNextConnect { get; set; } - public event Action? AssetManifestDocumentAvailable; + public int SurfaceSubscriberCount => _surfaceDocumentAvailable?.GetInvocationList().Length ?? 0; - public event Action? CommandReceiptAvailable; + public int AssetManifestSubscriberCount => _assetManifestDocumentAvailable?.GetInvocationList().Length ?? 0; + + public int ReceiptSubscriberCount => _commandReceiptAvailable?.GetInvocationList().Length ?? 0; + + public event Action? SurfaceDocumentAvailable + { + add => _surfaceDocumentAvailable += value; + remove => _surfaceDocumentAvailable -= value; + } + + public event Action? AssetManifestDocumentAvailable + { + add => _assetManifestDocumentAvailable += value; + remove => _assetManifestDocumentAvailable -= value; + } + + public event Action? CommandReceiptAvailable + { + add => _commandReceiptAvailable += value; + remove => _commandReceiptAvailable -= value; + } public void Connect() { ConnectCount++; + if (!FailNextConnect) return; + FailNextConnect = false; + throw new IOException("transient transport failure"); } public void Disconnect() @@ -2872,18 +2931,18 @@ namespace GameCult.Eve.UnityScene.Tests public void PublishSurface(EveUnitySceneProviderSurfaceDocument document) { CurrentSurfaceDocument = document; - SurfaceDocumentAvailable?.Invoke(document); + _surfaceDocumentAvailable?.Invoke(document); } public void PublishAssetManifest(EveUnityPlayableWorldAssetManifestDocument document) { CurrentAssetManifestDocument = document; - AssetManifestDocumentAvailable?.Invoke(document); + _assetManifestDocumentAvailable?.Invoke(document); } public void PublishReceipt(EveUnitySceneCommandReceipt receipt) { - CommandReceiptAvailable?.Invoke(receipt); + _commandReceiptAvailable?.Invoke(receipt); } } diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index bff0809..a0b18db 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,11 +1,11 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.97", + "version": "0.3.98", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { - "org.gamecult.cultlib": "1.0.36", + "org.gamecult.cultlib": "1.0.38", "org.gamecult.eve.plugin-fields": "0.2.3", "org.gamecult.eve.surface": "0.2.4", "com.unity.mathematics": "1.3.2",