保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。 保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。 验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。 包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
350 lines
23 KiB
C#
350 lines
23 KiB
C#
using System.Net;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using game.Account;
|
|
using game.PlayerDataStorage;
|
|
using Newtonsoft.Json;
|
|
|
|
static partial class Tests
|
|
{
|
|
static readonly Uri Api = new("http://synthetic.invalid/");
|
|
static readonly string RunId = DateTime.UtcNow.ToString("yyyyMMddHHmmss") + "_" + Guid.NewGuid().ToString("N");
|
|
static readonly string Root = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../runs", RunId));
|
|
static readonly List<object> Results = new();
|
|
static int assertions, failures;
|
|
static async Task<int> Main(string[] args)
|
|
{
|
|
if(args.Length==1 && args[0]=="--audit-precompiled") return PrecompiledAudit.Run();
|
|
if(args.Length==2 && args[0]=="--cleanup-failure") return await IsolatedCleanupFailure(args[1]);
|
|
Directory.CreateDirectory(Root);
|
|
await Run("B2-C1-metadata-one-auth-shared-cookie", SingleAuthentication);
|
|
await Run("B2-C1-lease-once-before-after-close", Admission);
|
|
await Run("B2-C1-denied-routes-zero-send", Routes);
|
|
await Run("CP2B-T-formal-lease-route-and-cookie-isolation", FormalBusinessLease);
|
|
await Run("B2-C1-real-handler-rejected", RealHandlerRejected);
|
|
await Run("B2-C1-cyclic-handler-rejected-without-recursive-dispose", CyclicHandlerRejected);
|
|
await Run("B2-C1-active-send-drains-before-session", Drain);
|
|
await Run("B2-C1-reentrant-cancel-same-task", Reentry);
|
|
await Run("B2-C1-dispose-failure-retains-session", DisposeFailure);
|
|
await Run("B2-C1-issuance-close-race", IssuanceRace);
|
|
await Run("B2-C1-factory-revokes-context", FactoryRevocation);
|
|
await Run("B2-C1-stale-success-body-discarded", StaleResponse);
|
|
await Run("B2-C1-401-and-redirect-evidence", Responses);
|
|
await Run("B2-C1-registry-timeout-then-real-drain", RegistryTimeout);
|
|
await Run("B2-C1-registry-failure-retained", RegistryFailure);
|
|
await Run("B2-C1-registry-fails-before-other-drains", RegistryEarlyFailure);
|
|
await Run("B2-C1-new-failure-wakes-existing-observer", RegistryNewFailure);
|
|
await Run("B2-C1-registry-registration-during-await", RegistryRegistration);
|
|
await Run("B2-C1-registry-invalid-cancel", RegistryCancellation);
|
|
await Run("B2-C1-process-registry-cross-caller", ProcessBarrier);
|
|
await RunSchemaTests();
|
|
await RunD3Tests();
|
|
await RunCollectionSchemaTests();
|
|
await RunClubPackSchemaTests();
|
|
await RunCampEventSchemaTests();
|
|
await RunOfferSchemaTests();
|
|
await RunBuffTriggerSchemaTests();
|
|
await RunActivitySchemaTests();
|
|
await RunPinataSchemaTests();
|
|
await RunCubeSchemaTests();
|
|
await RunBattlePassSchemaTests();
|
|
await RunPotionSchemaTests();
|
|
await RunPlayerSchemaTests();
|
|
await RunShopSchemaTests();
|
|
await RunEventBaseSchemaTests();
|
|
await RunFishSchemaTests();
|
|
await RunPartnerSchemaTests();
|
|
await RunLoginSchemaTests();
|
|
await RunAggregateTests();
|
|
await RunCoordinatorTests();
|
|
await RunCP2ARuntimeSelectionTests();
|
|
await RunCP2AExistingArchiveTests();
|
|
var report = new { runId = RunId, checkpoint = "4-validation", failures, assertions, accessedFormalServer = false, tests = Results };
|
|
await File.WriteAllTextAsync(Path.Combine(Root, "result.json"), JsonConvert.SerializeObject(report, Formatting.Indented));
|
|
Console.WriteLine($"RESULT tests={Results.Count} failed={failures} assertions={assertions} run={RunId}");
|
|
return failures == 0 ? 0 : 1;
|
|
}
|
|
static async Task Run(string name, Func<Task> test)
|
|
{
|
|
var before = assertions;
|
|
try { await test().WaitAsync(TimeSpan.FromSeconds(30)); Results.Add(new { name, passed = true, assertions = assertions - before }); Console.WriteLine("PASS " + name); }
|
|
catch (Exception ex) { failures++; Results.Add(new { name, passed = false, error = ex.GetType().Name }); Console.WriteLine("FAIL " + name + " " + ex); }
|
|
}
|
|
static void Check(bool value, string message) { Interlocked.Increment(ref assertions); if (!value) throw new InvalidOperationException(message); }
|
|
sealed class CyclicHandler : DelegatingHandler { }
|
|
static async Task CyclicHandlerRejected()
|
|
{
|
|
var first = new CyclicHandler();
|
|
var second = new CyclicHandler();
|
|
first.InnerHandler = second;
|
|
second.InnerHandler = first;
|
|
var capability = PlayerKVControlledLoadCapability.CreateForValidation(_ => first);
|
|
await Throws<InvalidOperationException>(() =>
|
|
{
|
|
capability.CreateHandler(new CookieContainer());
|
|
return Task.CompletedTask;
|
|
});
|
|
// No send occurred. Break the rejected graph before its owner disposes it.
|
|
second.InnerHandler = new Handler((_, _) => throw new InvalidOperationException("Must not send."));
|
|
first.Dispose();
|
|
}
|
|
static TaskCompletionSource<bool> Signal() => new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
static HttpResponseMessage Json(string text, HttpStatusCode code = HttpStatusCode.OK) => new(code) { Content = new StringContent(text, Encoding.UTF8, "application/json") };
|
|
static async Task Throws<T>(Func<Task> action) where T : Exception
|
|
{ try { await action(); } catch (T) { Check(true, "expected exception"); return; } throw new InvalidOperationException("Expected " + typeof(T).Name); }
|
|
sealed class Handler : HttpMessageHandler
|
|
{
|
|
readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> send;
|
|
readonly Action dispose;
|
|
internal Handler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> send, Action dispose = null)
|
|
{ this.send = send; this.dispose = dispose; }
|
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage r, CancellationToken t) => send(r, t);
|
|
protected override void Dispose(bool disposing) { if (disposing) dispose?.Invoke(); base.Dispose(disposing); }
|
|
}
|
|
sealed class Fixture
|
|
{
|
|
internal readonly List<string> Order = new();
|
|
internal int Auth, Gets, BorrowedSends, SessionDisposes, LeaseDisposes;
|
|
internal bool Current = true, FailLeaseDispose;
|
|
internal Action DuringFactory;
|
|
internal CookieContainer Jar;
|
|
internal Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> Borrowed = (_, _) => Task.FromResult(Json("{}"));
|
|
internal PlayerKVLoginContext Context;
|
|
internal void Record(string x) { lock (Order) Order.Add(x); }
|
|
internal Fixture()
|
|
{
|
|
Context = new PlayerKVLoginContext("101", Path.Combine(Root, "cache", Guid.NewGuid().ToString("N")), 7,
|
|
_ => Current, Api,
|
|
() => new S101AccountSession(Api, jar =>
|
|
{
|
|
Jar = jar;
|
|
return new Handler((r, t) =>
|
|
{
|
|
Check(r.RequestUri.AbsolutePath == "/account/Login", "one synthetic auth route");
|
|
Auth++;
|
|
jar.Add(Api, new Cookie("sid", "synthetic"));
|
|
return Task.FromResult(Json("{\"state\":0,\"internalId\":17,\"newCreate\":true,\"createTime\":\"2026-01-01T00:00:00Z\",\"svrTime\":\"2026-01-02T00:00:00Z\"}"));
|
|
}, () => { SessionDisposes++; Record("session"); });
|
|
}),
|
|
() => new PlayerKVGameSessionBridge(jar =>
|
|
{
|
|
Check(ReferenceEquals(Jar, jar), "bridge shares cookie identity");
|
|
return new Handler((r, t) =>
|
|
{
|
|
Check(r.RequestUri.AbsolutePath == "/playerdata/Get", "get only, zero KV mutation");
|
|
Gets++; return Task.FromResult(Json("{\"data\":{}}"));
|
|
}, () => Record("bridge"));
|
|
}));
|
|
}
|
|
internal PlayerKVControlledLoadCapability Capability() => PlayerKVControlledLoadCapability.CreateForValidation(jar =>
|
|
{
|
|
Check(ReferenceEquals(Jar, jar), "borrower shares exact cookie identity");
|
|
DuringFactory?.Invoke();
|
|
return new Handler((r, t) => { BorrowedSends++; return Borrowed(r, t); }, () =>
|
|
{ LeaseDisposes++; Record("lease"); if (FailLeaseDispose) throw new IOException("synthetic cleanup fault"); });
|
|
});
|
|
internal PlayerKVControlledLoadCapability FormalCapability() => PlayerKVControlledLoadCapability.CreateForGame(jar =>
|
|
{
|
|
Check(ReferenceEquals(Jar, jar), "formal lease shares exact cookie identity");
|
|
return new Handler((r, t) => { BorrowedSends++; return Borrowed(r, t); }, () =>
|
|
{ LeaseDisposes++; Record("lease"); if (FailLeaseDispose) throw new IOException("synthetic cleanup fault"); });
|
|
});
|
|
internal async Task<PlayerKVAccountLease> PreparedLease()
|
|
{
|
|
Check((await Context.PrepareAsync()).Prepared, "prepared");
|
|
Check(Context.TryAcquireAccountLease(Capability(), out var lease), "lease admitted");
|
|
return lease;
|
|
}
|
|
}
|
|
static async Task SingleAuthentication()
|
|
{
|
|
var f = new Fixture(); var lease = await f.PreparedLease();
|
|
var d = f.Context.AccountDescriptor;
|
|
Check(d.PlayerId == "101" && d.Epoch == 7 && d.InternalId == 17 && d.NewlyCreated, "metadata belongs to authentication");
|
|
Check(d.CreateTimeUtc.Year == 2026 && d.ServerTimeUtc.Day == 2, "metadata time");
|
|
Check((await lease.SendAsync(HttpMethod.Get, "__kv_validation/ok")).IsCurrent, "borrowed send");
|
|
Check(f.Auth == 1 && f.Gets == 1 && f.BorrowedSends == 1, "no second login");
|
|
await f.Context.CloseAsync();
|
|
Check(string.Join(",", f.Order) == "lease,bridge,session", "borrower drains before bridge and session");
|
|
Check(f.Context.AccountDescriptor == null && !lease.IsCurrent, "closed admission revoked");
|
|
}
|
|
static async Task Admission()
|
|
{
|
|
var f = new Fixture(); Check(!f.Context.TryAcquireAccountLease(f.Capability(), out _), "before prepared rejected");
|
|
var lease = await f.PreparedLease();
|
|
Check(!f.Context.TryAcquireAccountLease(f.Capability(), out _), "one lease only");
|
|
Check(!f.Context.TryAcquireAccountLease(null, out _), "capability required");
|
|
var close = f.Context.CloseAsync(); Check(!f.Context.TryAcquireAccountLease(f.Capability(), out _), "close synchronous");
|
|
await close;
|
|
Check(!(await lease.SendAsync(HttpMethod.Get, "__kv_validation/ok")).IsCurrent && f.BorrowedSends == 0, "after close no transport");
|
|
}
|
|
static async Task Routes()
|
|
{
|
|
var f = new Fixture(); var l = await f.PreparedLease();
|
|
foreach (var route in new[] { "account/Login", "playerdata/Get", "playerdata/Update", "http://synthetic.invalid/__kv_validation/ok", "//synthetic.invalid/x", "__kv_validation/../ok", "__kv_validation/ok?x=1", "%5f%5fkv_validation/ok", "__kv_validation/ok#f" })
|
|
await Throws<ArgumentException>(() => l.SendAsync(HttpMethod.Get, route));
|
|
Check(f.BorrowedSends == 0, "rejected routes never sent"); await f.Context.CloseAsync();
|
|
}
|
|
static async Task FormalBusinessLease()
|
|
{
|
|
var f = new Fixture(); Check((await f.Context.PrepareAsync()).Prepared, "formal prepared");
|
|
Check(f.Context.TryAcquireAccountLease(f.FormalCapability(), out var lease), "formal lease admitted");
|
|
Check(lease.IsFormal && ReferenceEquals(lease.FormalCookies, f.Jar), "immutable formal mode and shared jar");
|
|
var formalClient=(HttpClient)typeof(PlayerKVAccountLease).GetField("client", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(lease)!;
|
|
Check(formalClient.Timeout == TimeSpan.FromSeconds(15), "formal transport retains old 15 second timeout");
|
|
f.Borrowed = async (request, _) =>
|
|
{
|
|
Check(request.RequestUri.AbsolutePath == "/mail/fetch", "canonical business path");
|
|
Check(request.Method == HttpMethod.Post, "POST method retained");
|
|
Check(request.Content.Headers.ContentType.MediaType == "application/json", "JSON content type retained");
|
|
Check(await request.Content.ReadAsStringAsync() == "{}", "body retained");
|
|
return Json("business");
|
|
};
|
|
var response = await lease.SendAsync(HttpMethod.Post, "mail/fetch", "{}");
|
|
Check(response.IsCurrent && response.StatusCode == 200 && response.Body == "business", "formal response retained");
|
|
foreach (var route in new[] { "__kv_validation/ok", "__kv_validation/unauthorized", "account/Login", "playerdata/Get", "/mail/fetch", "//other/x", "http://other/x", "mail/fetch?x=1", "mail/%66etch", "mail/../fetch", "mail\\fetch", "mail//fetch" })
|
|
await Throws<ArgumentException>(() => lease.SendAsync(HttpMethod.Get, route));
|
|
Check(f.BorrowedSends == 1, "invalid formal routes zero send");
|
|
await f.Context.CloseAsync();
|
|
var v = new Fixture(); var validation = await v.PreparedLease();
|
|
Check(!validation.IsFormal && validation.FormalCookies == null, "validation never exposes formal cookie");
|
|
var validationClient=(HttpClient)typeof(PlayerKVAccountLease).GetField("client", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(validation)!;
|
|
Check(validationClient.Timeout == TimeSpan.FromSeconds(100), "validation timeout unchanged");
|
|
await Throws<ArgumentException>(() => validation.SendAsync(HttpMethod.Get, "mail/fetch"));
|
|
Check(v.BorrowedSends == 0, "validation ordinary path zero send");
|
|
await v.Context.CloseAsync();
|
|
var broken = new Fixture { FailLeaseDispose = true };
|
|
Check((await broken.Context.PrepareAsync()).Prepared, "failure case prepared");
|
|
Check(broken.Context.TryAcquireAccountLease(broken.FormalCapability(), out var failedLease), "failure case formal lease");
|
|
var failedClose = broken.Context.CloseAsync();
|
|
await Throws<IOException>(() => failedClose);
|
|
Check(!failedLease.IsCurrent && failedLease.FormalCookies == null &&
|
|
ReferenceEquals(failedClose, broken.Context.CloseAsync()), "formal close failure remains closed");
|
|
}
|
|
static async Task RealHandlerRejected()
|
|
{
|
|
var f = new Fixture(); await f.Context.PrepareAsync();
|
|
await Throws<InvalidOperationException>(() =>
|
|
{ f.Context.TryAcquireAccountLease(PlayerKVControlledLoadCapability.CreateForValidation(_ => new HttpClientHandler()), out _); return Task.CompletedTask; });
|
|
Check(f.BorrowedSends == 0, "real handler never sends"); await f.Context.CloseAsync();
|
|
}
|
|
static async Task Drain()
|
|
{
|
|
var f = new Fixture(); var entered = Signal(); var release = Signal(); var cancelled = Signal();
|
|
f.Borrowed = async (_, token) => { token.Register(() => cancelled.TrySetResult(true)); entered.TrySetResult(true); await release.Task; f.Record("send-ended"); return Json("late"); };
|
|
var l = await f.PreparedLease(); var send = l.SendAsync(HttpMethod.Get, "__kv_validation/ok"); await entered.Task;
|
|
var close = f.Context.CloseAsync(); await cancelled.Task;
|
|
Check(!close.IsCompleted && f.SessionDisposes == 0 && f.LeaseDisposes == 0, "no premature dispose despite cancellation");
|
|
release.TrySetResult(true); var response = await send; await close;
|
|
Check(!response.IsCurrent && response.Body == null, "late response withheld");
|
|
Check(string.Join(",", f.Order) == "send-ended,lease,bridge,session", "full resource order");
|
|
}
|
|
static async Task Reentry()
|
|
{
|
|
var f = new Fixture(); var entered = Signal(); var release = Signal(); var callback = Signal(); Task reentered = null;
|
|
f.Borrowed = async (_, token) => { token.Register(() => { reentered = f.Context.CloseAsync(); callback.TrySetResult(true); }); entered.TrySetResult(true); await release.Task; return Json("{}"); };
|
|
var l = await f.PreparedLease(); var send = l.SendAsync(HttpMethod.Get, "__kv_validation/ok"); await entered.Task;
|
|
var close = f.Context.CloseAsync(); await callback.Task;
|
|
Check(ReferenceEquals(close, reentered), "cancellation reentry same task, no cycle");
|
|
release.TrySetResult(true); await send; await close;
|
|
}
|
|
static async Task DisposeFailure()
|
|
{
|
|
var f = new Fixture { FailLeaseDispose = true }; var l = await f.PreparedLease();
|
|
var close = f.Context.CloseAsync(); await Throws<IOException>(() => close);
|
|
Check(f.SessionDisposes == 0 && !f.Order.Contains("bridge"), "failed borrower cleanup prevents owner disposal");
|
|
Check(ReferenceEquals(close, f.Context.CloseAsync()) && !f.Context.TryAcquireAccountLease(f.Capability(), out _), "failure cannot reopen");
|
|
var registry = new PlayerKVShutdownRegistry(); registry.Register(close, f.Context);
|
|
Check(registry.Status == PlayerKVShutdownStatus.Failed, "fault retained");
|
|
var retained = (Dictionary<Task, PlayerKVLoginContext>)typeof(PlayerKVShutdownRegistry)
|
|
.GetField("pending", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(registry)!;
|
|
Check(ReferenceEquals(retained[close], f.Context), "failed owner retained beyond caller lifetime");
|
|
}
|
|
static async Task IssuanceRace()
|
|
{
|
|
for (int i = 0; i < 24; i++)
|
|
{
|
|
var f = new Fixture(); await f.Context.PrepareAsync(); var go = Signal();
|
|
var issue = Task.Run(async () => { await go.Task; return f.Context.TryAcquireAccountLease(f.Capability(), out _); });
|
|
var close = Task.Run(async () => { await go.Task; await f.Context.CloseAsync(); });
|
|
go.TrySetResult(true); await Task.WhenAll(issue, close);
|
|
Check(f.SessionDisposes == 1 && !f.Context.IsCurrent, "race closes session once");
|
|
if (issue.Result) Check(f.LeaseDisposes == 1, "published borrower observed by close");
|
|
}
|
|
}
|
|
static async Task FactoryRevocation()
|
|
{
|
|
var f = new Fixture(); await f.Context.PrepareAsync(); f.DuringFactory = () => { _ = f.Context.CloseAsync(); };
|
|
Check(!f.Context.TryAcquireAccountLease(f.Capability(), out var l) && l == null, "reentrant factory cannot publish");
|
|
await f.Context.CloseAsync(); Check(f.LeaseDisposes == 1 && f.SessionDisposes == 1, "candidate still cleaned");
|
|
}
|
|
static async Task StaleResponse()
|
|
{
|
|
var f = new Fixture(); f.Borrowed = (_, _) => { f.Current = false; return Task.FromResult(Json("should-not-escape")); };
|
|
var l = await f.PreparedLease(); var r = await l.SendAsync(HttpMethod.Get, "__kv_validation/ok");
|
|
Check(!r.IsCurrent && r.Body == null && r.StatusCode == null, "200 does not restore old epoch"); await f.Context.CloseAsync();
|
|
}
|
|
static async Task Responses()
|
|
{
|
|
var f = new Fixture(); var l = await f.PreparedLease();
|
|
f.Borrowed = (_, _) => Task.FromResult(Json("{}", HttpStatusCode.Unauthorized));
|
|
Check((await l.SendAsync(HttpMethod.Get, "__kv_validation/unauthorized")).StatusCode == 401, "401 evidence for version-aware caller");
|
|
f.Borrowed = (_, _) => Task.FromResult(Json("{}", HttpStatusCode.Redirect));
|
|
await Throws<InvalidOperationException>(() => l.SendAsync(HttpMethod.Get, "__kv_validation/ok")); await f.Context.CloseAsync();
|
|
}
|
|
static async Task RegistryTimeout()
|
|
{
|
|
var registry = new PlayerKVShutdownRegistry(); var closing = Signal(); var deadline = Signal(); registry.Register(closing.Task);
|
|
var observe = registry.ObserveAsync(10000, delay: (_, _) => deadline.Task); deadline.TrySetResult(true);
|
|
Check(await observe == PlayerKVShutdownStatus.Pending && !closing.Task.IsCompleted, "timeout not force-close");
|
|
closing.TrySetResult(true); await closing.Task;
|
|
Check(registry.Status == PlayerKVShutdownStatus.Ready, "real completion opens barrier");
|
|
}
|
|
static async Task RegistryFailure()
|
|
{
|
|
var registry = new PlayerKVShutdownRegistry(); var fail = Signal(); registry.Register(fail.Task); fail.TrySetException(new IOException("synthetic"));
|
|
Check(await registry.ObserveAsync(10000) == PlayerKVShutdownStatus.Failed, "fault classified");
|
|
registry.Register(Task.CompletedTask); Check(registry.Status == PlayerKVShutdownStatus.Failed, "new task cannot erase failure");
|
|
Check(!typeof(PlayerKVShutdownRegistry).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic).Any(m => m.Name == "Clear" || m.Name == "Reset"), "no reset API");
|
|
}
|
|
static async Task RegistryRegistration()
|
|
{
|
|
var registry = new PlayerKVShutdownRegistry(); var first = Signal(); var second = Signal(); var deadline = Signal();
|
|
registry.Register(first.Task); var observe = registry.ObserveAsync(10000, delay: (_, _) => deadline.Task);
|
|
registry.Register(second.Task); first.TrySetResult(true); deadline.TrySetResult(true);
|
|
Check(await observe == PlayerKVShutdownStatus.Pending, "newly registered work not missed"); second.TrySetResult(true);
|
|
}
|
|
static async Task RegistryEarlyFailure()
|
|
{
|
|
var registry = new PlayerKVShutdownRegistry(); var fail = Signal(); var slow = Signal(); var deadline = Signal();
|
|
registry.Register(fail.Task); registry.Register(slow.Task);
|
|
var observe = registry.ObserveAsync(10000, delay: (_, _) => deadline.Task);
|
|
fail.TrySetException(new IOException("synthetic"));
|
|
Check(await observe == PlayerKVShutdownStatus.Failed && !slow.Task.IsCompleted && !deadline.Task.IsCompleted,
|
|
"failure reported without waiting for unrelated cleanup or observation deadline");
|
|
slow.TrySetResult(true);
|
|
}
|
|
static async Task RegistryNewFailure()
|
|
{
|
|
var registry = new PlayerKVShutdownRegistry(); var slow = Signal(); var deadline = Signal();
|
|
registry.Register(slow.Task); var observe = registry.ObserveAsync(10000, delay: (_, _) => deadline.Task);
|
|
registry.Register(Task.FromException(new IOException("synthetic")));
|
|
Check(await observe == PlayerKVShutdownStatus.Failed && !slow.Task.IsCompleted && !deadline.Task.IsCompleted,
|
|
"new root failure wakes existing observer"); slow.TrySetResult(true);
|
|
}
|
|
static async Task RegistryCancellation()
|
|
{
|
|
var registry = new PlayerKVShutdownRegistry(); var close = Signal(); registry.Register(close.Task);
|
|
Check(await registry.ObserveAsync(0) == PlayerKVShutdownStatus.Input, "invalid timeout Input");
|
|
using var cts = new CancellationTokenSource(); var observed = registry.ObserveAsync(10000, cts.Token); cts.Cancel();
|
|
Check(await observed == PlayerKVShutdownStatus.Cancelled && !close.Task.IsCompleted, "observer cancellation does not cancel cleanup"); close.TrySetResult(true);
|
|
}
|
|
static async Task ProcessBarrier()
|
|
{
|
|
var firstRoot = PlayerKVShutdownRegistry.Process; var close = Signal(); firstRoot.Register(close.Task);
|
|
var secondRoot = PlayerKVShutdownRegistry.Process;
|
|
Check(ReferenceEquals(firstRoot, secondRoot) && secondRoot.Status == PlayerKVShutdownStatus.Pending, "new caller same process barrier");
|
|
close.TrySetResult(true); Check(await secondRoot.ObserveAsync(1000) == PlayerKVShutdownStatus.Ready, "drained across caller replacement");
|
|
}
|
|
}
|