Repository navigation
Name a failure once: the taxonomy, and the one place a class is assigned - #191
Merged
Merged
Conversation
`superplayer-resilience`'s first code, and the vocabulary every issue after it reads. `FailureClass` is PRD.md §3.3's sealed taxonomy — public, naming no Media3 type — and each class answers the three questions ADR-0011 rule 1 asks of it: whether retrying the same bytes can help, the highest `FallbackRung` the ladder may climb for it, and the stable name a log line, a bug report and a warehouse row share. A ceiling rather than an itinerary, because which lower rungs are worth attempting is the ladder's (#181); `FallbackRung` is a type rather than a number because "3" at a call site says nothing about which rung 3 is. The one-to-one row deriving telemetry's coarse `FailureCategory` is on the class (rule 3), so nothing keeps a second taxonomy; wiring it into `PlaybackFailure` is #183's. `ErrorClassifier.classify` is total and has no unknown class to fall into. It reads, strongest evidence first: what core already concluded, mapping `StaleLivePlaylistException` by the `likelyCause` it names and `LiveWindowTooShortException` by its verdict, re-deriving neither (rule 4); then the load that failed, where the `LoadKind` stamp a player with resilience carries is what tells a refused segment from a refused manifest, which is the whole of `Transient.CdnEdge`; then the engine's error-code band, the only `when` over `errorCode` the repository may contain. The fall-through is `Transient.Network` and never `Fatal.Unsupported`, which is reached only from a code that says *unsupported*: an unrecognised code is likelier to be a transfer that can be retried than content that can never play, and calling it fatal would end sessions the ladder could rescue. The test is pure — there is nothing to play to classify a failure — and asserts totality over the whole code space rather than over a corpus, which is what "zero unclassified errors" means as a property of the function. Robolectric only for the two Android types the evidence is made of. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HeTWwmK1KcFMrfSAre23GS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #177.
superplayer-resilience's first code:FailureClass, the sealed taxonomyPRD.md§3.3 names, andErrorClassifier, the single place a failure acquires a meaning (ADR-0011 rules 1–4). Nothing acts on a classification yet — the retry policy (#178), the ladder (#181) and telemetry'sclassificationfield (#183) are the issues that read it.CI (not attached to PRs in this repo —
ci.ymlisworkflow_dispatch:only): https://github.com/ramesh130/superplayer/actions/runs/35049408550The decisions, argued
Why the rung ceiling is shaped the way it is. Rule 1 asks each class for "which rung it may reach", so the class carries a
FallbackRung— a type, in the ladder's own declaration order, because3at a call site says nothing about which rung 3 is — and it is a ceiling, not an itinerary. Which rungs below it are worth attempting is the ladder's to decide (#181), which is how rule 7'sDevice.DecoderTransient"goes to rung 5 without trying a host" while still having rung 5 as its ceiling. The one rung a class settles by itself is rung 1, and that is exactly whatretryableis: a failure that is not retryable is not retried, however much budget the decision in force allows.The ceilings then say something rather than repeating each other:
Fatal.Unsupported→TYPED_ERROR: no remedy is attempted at all. Rule 7's "rung 6 at once".Device.DecoderTransient→RECREATE_DECODER: the one class whose remedy is the top rung.Drm.Provisioning,Drm.LicenceAcquisition→RETRY_SAME_URL: a licence exchange between the device and the licence service is not something another host, another variant or another source has any bearing on.NEXT_SOURCE: rungs 2–4 are all plausible remedies for a transfer, a description or a device that could not play this rung, and rung 5 is not — a decoder is not implicated in bytes that never reached one.How totality was kept without an unknown class.
classifytakes aThrowableand returns a class for every input, including one that is noPlaybackExceptionat all (the load-error path #178 will hold a bareIOException). Three layers of evidence, strongest first, each allowed to decline: what core already concluded, then the load that failed, then the engine's band. The band is a singlewhenwhose every range carries its own fall-through, so a code a later Media3 invents lands with its neighbours, and whose finalelsecovers the miscellaneous band, Media3's negative session codes, an app's custom codes and no band at all. That default isTransient.Networkand neverFatal.Unsupported: rule 2 reserves that class for a code that says unsupported, and an unrecognised code is far likelier to be a transfer that can be retried than content that can never play — calling it fatal would end sessions the ladder could have rescued. The test asserts this over the whole code space (−200..8000, plusCUSTOM_ERROR_CODE_BASEand both ends ofInt), a looping cause chain and a 200-deep one, rather than over a corpus that happens to be at hand.Core detects, this maps (rule 4).
StaleLivePlaylistExceptionis read by thelikelyCausecore filled in —INTERMEDIARY_CACHEis aTransient.CdnEdge, because an edge holding a frozen copy is an edge defect and another edge may hold a live one;ORIGINis aContent.SegmentGap, because segments that were promised are not being produced and asking again finds the same hole.LiveWindowTooShortExceptionis aContent.ManifestInvalid. No playlist age and no manifest attribute is re-read, and core's cache-bypassing reload is not a rung.What separates
Transient.CdnEdgefrom a plain transfer failure is theLoadKindstamp #176 put on every request of a player with resilience attached: 401/403/404 on a media load is the expired token or edge missPRD.md§3.3 names; the same status on a manifest, or on an unstamped request, says nothing and falls through to the band. 416 on a media load is aContent.SegmentGap— the object is shorter than the description promised.Two known disagreements with the error-code band, both foreseen by ADR-0011 rule 3 and both #183's to act on when the field is wired up: a frozen origin is band
NETWORKand classSOURCE, and the renderer band is bandRENDERERand classDECODER. The renderer band maps onto theDeviceleaves deliberately — initialising an audio track or a frame processor is a decoder init in every way the ladder cares about — which leavesFailureCategory.RENDERERandUNKNOWNwith no row in the table, said out loud inFailureClass's KDoc rather than left to be discovered.Drm.*is declared and no DRM is plumbed (rule 6). Its three leaves are not placeholders: Media3's DRM band reaches players today and rule 2 leaves nothing unclassified, so each leaf is a code the engine can already raise. Phase 6 adds a leaf in a change that says why.Review, two axes
Standards. ADR-0011 rules 1–4 followed as written; the taxonomy names no Media3 type and
api/superplayer-resilience.api— the module's first entries — confirms it (verifyNoUnstableMedia3InPublicApipasses). OnewhenovererrorCodein the repository, which is this one. Every non-obvious constant carries a// ref:or// spec:citation (RFC 9110 for the statuses, Media3's documented band ranges,MediaCodec.CodecException's two flags). No new dependency artifact —media3-datasource,media3-test-utils-robolectricandrobolectricare catalog entries already recorded inTHIRD_PARTY.md— so no row was needed.docs/testing.md: the test drives the module's public API, asserts past no facade, and touches no device and no network. Smell pass: the Repeated Switches rule is the one this change is most exposed to and rule 1 is the answer to it;Speculative Generalitywas weighed for theDrmleaves and forFallbackRung's six values (both required — one by rule 2's totality, one by rule 7's fixed order) and no other accessor was added on spec.CLAUDE.md's orientation gained the paragraph and the module gained a line in the test-sources sentence.Spec. Against #177's body and rules 1–4: the taxonomy is public and Media3-free, each class carries retryability, ceiling and stable name, the classifier is total with no unknown class, core's two exceptions are mapped by their fields, a 403 on a segment with a healthy manifest is
Transient.CdnEdgeand on a manifest is not, the class →FailureCategorytable is built and no telemetry code is touched, and the.apidiff is committed. Two of #177's acceptance boxes are deliberately not ticked here: "everyFaultScriptfault kind classifies to a named class, asserted through the harness" and "each hostile corpus entry that fails today classifies to a named class". This unit was scoped to the pure mapping — nothing acts on a classification yet, so a harness run would only catch an exception and hand it to the same function this test calls directly — and driving the fault corpus and the hostile corpus through a player is the phase's exit test, #184, where it is a criterion rather than a duplicate.What was skipped, and why
No emulator or demo run. This unit has no playback path: the classifier is a pure function, so the Robolectric unit test is the end-to-end evidence.
./gradlew assemble checkis green on the full repository.🤖 Generated with Claude Code
https://claude.ai/code/session_01HeTWwmK1KcFMrfSAre23GS