fix: backpack thumbnails exception handling & improvement on cancelled operations - #8824
Conversation
|
Windows and Mac build successful in Unity Cloud! You can find a link to the downloadable artifact below. |
|
✅ PR reviewed and approved by QA on both platforms following the PR test instructions. Build: Test results:
Caveats:
Unrelated errors noted (do not affect verdict):
Verdict: PASS ✅ Player 8824 windows.log Mac 19.05.2026_18.44.15_REC.-.PR.8824.mp4Windows 19.05.2026_18.51.07_REC.PR-8824.mp4 |
|
@claude review |
This comment has been minimized.
This comment has been minimized.
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Signed-off-by: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Signed-off-by: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com>
|
I'll analyze this and get back to you. |
This comment has been minimized.
This comment has been minimized.
|
Claude finished @lorux0's task in 3m 12s —— View job Re-review Complete
Root CauseThe grid cell input was gated on thumbnail completion, but three independent failure modes could block that gate permanently: cancelled thumbnail intentions resolving with no signal to the consumer, no consumer-side timeout, and no retry path after a page-switch cancellation. The diff addresses all three correctly: a three-state result ( Previous Blocking Issue — Fixed ✅The unconditional if (wearable.ThumbnailAssetResult is not { IsInitialized: true })
wearable.ThumbnailAssetResult = StreamableLoadingResult<SpriteData>.WithFallback.CancelledResult();The race condition is resolved. The duplicate comment in Non-Blocking Warning
Not blocking given the bounded lifetime, but worth a follow-up. REVIEW_RESULT: PASS ✅ |
|
🔍 Claude reviewed this PR and found no blocking issues, but assessed it as complex — human DEV review is still required before merging. |
|
PR #8824, run #26231363611 Builds: Windows change, Windows baseline, macOS change, macOS baseline Framework 13 i7
|
…ethrowing forever ## Problem One transient thumbnail failure poisons that item's thumbnail for the whole session: `ECSThumbnailProvider.GetAsync` rethrows `ThumbnailLoadFailedException` from cache on every subsequent request. The emote wheel's spinner sticks forever (unobserved `UniTaskVoid` chain), and backpack grids replay-log the exception per page render. Sentry family PAD/P9R/PD5 ≈ 260 events/week of pure cached replays (~396 events / ~33 users with the one-shot timeout signals PA1/P9Y included). Introduced by #8824. ## Root cause An initialized non-Succeeded `ThumbnailAssetResult` slot was treated as a permanent terminal state: the only producer of the sticky `Failed` state is the provider's own 30 s consumer-timeout catch, and there was no recovery path until restart. Separately, `EmotesWheelController.WaitForThumbnailAsync` had no catch, so the exception vanished into a `.Forget()` chain with the loading spinner still active. ## Fix 1. `ECSThumbnailProvider`: any initialized non-Succeeded slot (Failed as well as Cancelled) is now clear-and-retry — reset the slot and spawn a fresh promise. The timeout path still writes `Failed()` (it remains the release signal for concurrent waiters); it just stops being permanent. 2. `EmotesWheelController.WaitForThumbnailAsync`: backpack-style catch — OCE returns; other exceptions log via `ReportHub` (THUMBNAILS), fall back to the default thumbnail, and release the spinner (mirrors `BackpackEmoteGridController`). 3. Comment/doc updates stating the new per-attempt invariant (no behavior change). Contract change: `GetAsync` moves from "throws instantly forever after first failure" to "retries per explicit call". All 8 call sites are UI-render-bounded and concurrent waiters are deduped by the in-flight slot signal; worst case a persistently-failing item costs one 30 s promise per grid/wheel open. ## Test New EditMode `ECSThumbnailProviderShould`: `RetryAfterFailedSlotInsteadOfRethrowing` (pin: instant rethrow, 0 promises spawned), `MarkFailedOnTimeoutAndRetryOnNextCall` (pin: cached rethrow without spawning), `ReturnCachedSuccessWithoutSpawningPromise` (guards against an always-retry regression). ## Validation Windows Unity 6000.4 EditMode lane at the pin: RED FAIL 2/3 as intended (retry test expected 1 promise got 0; timeout test expected 2 got 1; success guard passed) / GREEN PASS 3/3. Fixes #8902 Fixes #8891
…ethrowing forever ## Problem One transient thumbnail failure poisons that item's thumbnail for the whole session: `ECSThumbnailProvider.GetAsync` rethrows `ThumbnailLoadFailedException` from cache on every subsequent request. The emote wheel's spinner sticks forever (unobserved `UniTaskVoid` chain), and backpack grids replay-log the exception per page render. Sentry family PAD/P9R/PD5 ≈ 260 events/week of pure cached replays (~396 events / ~33 users with the one-shot timeout signals PA1/P9Y included). Introduced by #8824. ## Root cause An initialized non-Succeeded `ThumbnailAssetResult` slot was treated as a permanent terminal state: the only producer of the sticky `Failed` state is the provider's own 30 s consumer-timeout catch, and there was no recovery path until restart. Separately, `EmotesWheelController.WaitForThumbnailAsync` had no catch, so the exception vanished into a `.Forget()` chain with the loading spinner still active. ## Fix 1. `ECSThumbnailProvider`: any initialized non-Succeeded slot (Failed as well as Cancelled) is now clear-and-retry — reset the slot and spawn a fresh promise. The timeout path still writes `Failed()` (it remains the release signal for concurrent waiters); it just stops being permanent. 2. `EmotesWheelController.WaitForThumbnailAsync`: backpack-style catch — OCE returns; other exceptions log via `ReportHub` (THUMBNAILS), fall back to the default thumbnail, and release the spinner (mirrors `BackpackEmoteGridController`). 3. Comment/doc updates stating the new per-attempt invariant (no behavior change). Contract change: `GetAsync` moves from "throws instantly forever after first failure" to "retries per explicit call". All 8 call sites are UI-render-bounded and concurrent waiters are deduped by the in-flight slot signal; worst case a persistently-failing item costs one 30 s promise per grid/wheel open. ## Test New EditMode `ECSThumbnailProviderShould`: `RetryAfterFailedSlotInsteadOfRethrowing` (pin: instant rethrow, 0 promises spawned), `MarkFailedOnTimeoutAndRetryOnNextCall` (pin: cached rethrow without spawning), `ReturnCachedSuccessWithoutSpawningPromise` (guards against an always-retry regression). ## Validation Windows Unity 6000.4 EditMode lane at the pin: RED FAIL 2/3 as intended (retry test expected 1 promise got 0; timeout test expected 2 got 1; success guard passed) / GREEN PASS 3/3. Fixes #8902 Fixes #8891 Includes inspection-warning cleanup in all touched files.
…ethrowing forever ## Problem One transient thumbnail failure poisons that item's thumbnail for the whole session: `ECSThumbnailProvider.GetAsync` rethrows `ThumbnailLoadFailedException` from cache on every subsequent request. The emote wheel's spinner sticks forever (unobserved `UniTaskVoid` chain), and backpack grids replay-log the exception per page render. Sentry family PAD/P9R/PD5 ≈ 260 events/week of pure cached replays (~396 events / ~33 users with the one-shot timeout signals PA1/P9Y included). Introduced by #8824. ## Root cause An initialized non-Succeeded `ThumbnailAssetResult` slot was treated as a permanent terminal state: the only producer of the sticky `Failed` state is the provider's own 30 s consumer-timeout catch, and there was no recovery path until restart. Separately, `EmotesWheelController.WaitForThumbnailAsync` had no catch, so the exception vanished into a `.Forget()` chain with the loading spinner still active. ## Fix 1. `ECSThumbnailProvider`: any initialized non-Succeeded slot is now clear-and-retry — reset the slot and spawn a fresh promise. The timeout path still writes `Failed()` (it remains the release signal for concurrent waiters); it just stops being permanent. 2. `EmotesWheelController.WaitForThumbnailAsync`: backpack-style catch — OCE returns; other exceptions log via `ReportHub` (THUMBNAILS), fall back to the default thumbnail, and release the spinner (mirrors `BackpackEmoteGridController`). 3. With the retry in place `WithFallback.Cancelled` lost its only in-lane reader, but the cancelled-vs-failed distinction is retained: the API pre-exists on dev and an in-flight sibling PR's disposal tests consume `CancelledResult()`; the resolver keeps stamping `Cancelled` for in-flight cancellation and `Failed` for terminal failures (see the Cross-PR note below). 4. Comment/doc updates stating the new per-attempt invariant (no behavior change). Contract change: `GetAsync` moves from "throws instantly forever after first failure" to "retries per explicit call". All 11 call sites are bounded per explicit call and concurrent waiters are deduped per attachment while an attempt is in flight (the signal is per-attachment, not per-attempt — a same-frame stale-entity overlap window exists and is tracked as a follow-up); worst case a persistently-failing item costs one 30 s promise per grid/wheel open. ## Test New EditMode `ECSThumbnailProviderShould`: `RetryAfterFailedSlotInsteadOfRethrowing` (pin: instant rethrow, 0 promises spawned), `MarkFailedOnTimeoutAndRetryOnNextCall` (pin: cached rethrow without spawning), `ReturnCachedSuccessWithoutSpawningPromise` (guards against an always-retry regression). ## Validation Windows Unity 6000.4 EditMode lane at the pin: RED FAIL 2/3 as intended (retry test expected 1 promise got 0; timeout test expected 2 got 1; success guard passed) / GREEN PASS 3/3. Fixes #8902 Fixes #8891 ## Cross-PR note `WithFallback.Cancelled` / `CancelledResult()` are **retained** — an earlier revision of this branch deleted them (Fix item 3 above describes that revision and is superseded; `2b1374f8d3` reverted the deletion). The pair pre-exists this PR (present at the base commit, `StreamableLoadingResult.cs:58`), and while this branch no longer reads the `Cancelled` discriminator — `GetAsync` now treats every initialized non-Succeeded slot, cancelled or failed, as clear-and-retry — it is not dead API: the in-flight sibling PR `bugsweep/unload-thumbnail-nre-blocks-memory-release` consumes `WithFallback.CancelledResult()` in its thumbnail-disposal test (`StorageThumbnailDisposalShould.cs`), and the resolver's cancellation stamps remain the waiter-release signal via `IsInitialized`. Removing a pre-existing shared-struct member out from under a sibling in-flight PR is out of scope for this bugfix; if the discriminator is still unread once both PRs land, collapsing `Cancelled` into `Failed` can ship as its own cleanup. Includes inspection-warning cleanup in all touched files.

Fixes #8241
Problem
The grid's cell-input layer is gated behind thumbnail load completion. The path from "cell unblocked" back to "thumbnail ready" had several failure points:
Combined, a stalled or cancelled thumbnail load could leave the grid cell input gated forever, hence the symptoms.
Solution
Test plan
Open the backpack either wearables or emotes. Switch fast through pages. Check that thumbnails load as expected and you can interact with the items.