fix(audible): tell a failed catalog lookup apart from a confirmed zero-match - #910
Open
m4bard wants to merge 1 commit into
Open
fix(audible): tell a failed catalog lookup apart from a confirmed zero-match#910m4bard wants to merge 1 commit into
m4bard wants to merge 1 commit into
Conversation
…o-match An Audible call that times out comes back as an empty result set that is identical to Audible answering "no such book". Listenarrs#877 has the trace: the per-call CancellationTokenSource fires, AudibleApiClient logs a warning and returns null, and SearchProductsCoreAsync turns that null into new SearchProductsDirectResponse(). From there nothing can tell the two apart, and a caller reasonably reads an empty list as an absence. This is the half of Listenarrs#877 that does not depend on its open questions. The retry half is deliberately left alone: it interacts with Listenarrs#635's planned refactor of AudibleApiClient, and the durations need Audible latency figures I do not have. Worth saying because the obvious patch is wrong: adding .Or<TaskCanceledException>() to the existing Polly policy does not retry these. AudibleApiClient passes its own token into SendAsync and PolicyHttpMessageHandler hands that token to policy.ExecuteAsync, so the retry engine holds an already-cancelled token. Measured at the message handler in Listenarrs#877, not reasoned about. Nothing in this PR touches Polly. SearchProductsDirectResponse gains ProviderUnavailable, set where the null is currently discarded. It rides through both ToSearchResponse copies onto AudibleSearchResponse, which is additive, so a client that ignores the field sees what it saw before. The /search/audible endpoint answers 503 rather than a 200 that makes a false claim about the catalogue. A flag rather than an exception, deliberately. Throwing from SearchProductsCoreAsync would reach fifteen call sites across series lookup, author-catalog paging and several fallback cascades inside this same workflow, all of which currently degrade to an empty page and carry on. That is a much larger behaviour change than the contract fix needs, and not one to make while Listenarrs#635 is open. One thing fell out of reading it. The diacritics retry at SearchProductsDirectAsync fires on any empty result, so a timeout spent the caller's remaining budget on a second request that failed the same way. It now checks the flag first. NOT fixed here: POST /api/v1/search still collapses the two, because carrying the signal through SearchService.IntelligentSearchAsync means changing its return type. That is the endpoint the report in Listenarrs#877 came from, so this is a partial answer to it and I would rather say so than imply otherwise. Tests: the failure case, a control on a promptly-answered empty catalogue so the flag cannot be set unconditionally, and the diacritics budget. Each was checked by reverting its own half. The first version of the diacritics test was worthless, passing with the guard removed because the query I picked had no diacritics in it; it uses an accented title now. Full suite 3121 passed, 0 failed, 130 skipped.
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.
Half of #877, and the half that does not wait on anything.
What it fixes
An Audible call that times out returns an empty result set identical to Audible answering "no such book". The per-call
CancellationTokenSourcefires,AudibleApiClientlogs a warning and returns null, andSearchProductsCoreAsyncturns that null intonew SearchProductsDirectResponse(). From there nothing can tell the two apart, and a caller reasonably reads an empty list as an absence. The report behind #877 was an external script doing exactly that.SearchProductsDirectResponsenow carriesProviderUnavailable, set where the null is currently discarded. It rides through bothToSearchResponsecopies ontoAudibleSearchResponse, andGET /search/audibleanswers 503 instead of a 200 that makes a false claim about the catalogue. The field is additive, so a client that ignores it sees what it saw before.The obvious patch does not work, and it is worth saying why
Adding
.Or<TaskCanceledException>()to the existing Polly policy looks like the fix and changes nothing.AudibleApiClientpasses its own token intoSendAsync, andPolicyHttpMessageHandlerhands that token topolicy.ExecuteAsync, so the retry engine holds an already-cancelled token before it can make a second attempt. That is measured at the message handler in #877 rather than reasoned about.Nothing here touches Polly. The retry half of #877 is left alone deliberately: it interacts with #635's planned refactor of
AudibleApiClient, and picking per-attempt durations needs Audible latency figures I do not have.A flag rather than an exception
Readarr throws at this boundary, and I looked at doing the same.
BookInfoProxyrethrows asBookInfoExceptionandSearchController.Searchhas no try/catch, so a provider failure reaches the error handler.Throwing from
SearchProductsCoreAsynchere would reach fifteen call sites across series lookup, author-catalog paging and several fallback cascades inside this same workflow, all of which currently degrade to an empty page and carry on. That is a much larger behaviour change than the contract fix needs, and not one I would make while #635 is open. If you would rather have the exception, it is a small change from here and the flag goes away.One thing that fell out of reading it
SearchProductsDirectAsyncretries with diacritics stripped whenever a result comes back empty. A timeout comes back empty, so it was spending the caller's remaining budget on a second request that failed the same way, inside a 10 second budget the existing 2, 4 and 8 second backoff already overruns. It checks the flag first now.What this does not fix
POST /api/v1/searchstill collapses the two. Carrying the signal throughSearchService.IntelligentSearchAsyncmeans changing its return type, which is a larger contract decision than this. That is the endpoint the original report came from, so this is a partial answer to #877 and I would rather say so than let it read as closed.Tests
Three: the failure case, a control on a promptly answered empty catalogue so the flag cannot be set unconditionally, and the diacritics budget. Each was checked by reverting its own half of the production change.
The first version of the diacritics test was worthless. It passed with the guard removed, because the title I picked had no diacritics in it, so the retry never fired either way. It uses an accented title now and fails as it should. Mentioning it because a test that cannot fail looks exactly like one that passes.
Full unfiltered suite: 3121 passed, 0 failed, 130 skipped.
Checked against canary
a630572e.