Skip to content

[amazonechocontrol] Improve handling of throttled notification polls - #21439

Open
ML19821 wants to merge 5 commits into
openhab:mainfrom
ML19821:fix/amazonechocontrol-notifications-throttling
Open

[amazonechocontrol] Improve handling of throttled notification polls#21439
ML19821 wants to merge 5 commits into
openhab:mainfrom
ML19821:fix/amazonechocontrol-notifications-throttling

Conversation

@ML19821

@ML19821 ML19821 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Description

On some accounts the hourly notifications poll (/api/notifications) fails on every cycle with 400: Bad Request (#21146, #19714). The real cause is throttling: Amazon answers with x-amzn-ErrorType: ThrottlingException and {"message":"Rate exceeded"}, but the binding never reads that header — and a failed poll was indistinguishable from "no notifications set" (#20018).

Changes

  1. Surface the real cause: x-amzn-ErrorType is appended to the exception message and logged at DEBUG; response bodies stay at TRACE.
  2. Fail fast on throttled responses (429, or a ThrottlingException header) instead of burning the generic retries — every retry is itself a counted request that keeps the account throttled. FailMode.NORMAL is unaffected.
  3. One poll at a time (added in the review round): the poll decision and its reservation are one atomic step with a unique in-flight token — concurrent triggers wait for the next cycle, a late result from a superseded poll is discarded, and a push trigger arriving during an in-flight poll schedules one catch-up poll instead of being lost.
  4. A failed poll is not an empty notification list: the last known state survives short outages; after three consecutive failures the next* channels go UNDEF. Retries back off exponentially (300 s doubling up to the hourly refresh), the push path honours the backoff, failures log once per streak.

Testing

148 unit tests (28 new): the backoff ladder, the UNDEF threshold, the throttling classification, no-second-request on a throttled fail-fast, and the token/serialization behavior of the review round. SAT report clean against main, re-established on the current revision. Verified on a production installation (build of 2026-08-19, before the review round's concurrency rework — that rework's behavior is pinned by the added unit tests): the ladder runs to the second, UNDEF appears after the third failure, no idle polling. Measured over two days: more than 100 attempts at spacings from seconds up to a deliberate 10.7-hour silence — every one answered 400 ThrottlingException. For such an account the endpoint is effectively permanently locked; the value of this PR there is the honest diagnosis, the strongly reduced request pressure, and channels that say UNDEF instead of pretending "no alarm set". Occasionally-throttled accounts additionally gain the transient-failure tolerance.

Fixes the misleading diagnosis of #21146 / #19714; related: #19781, #20018 (on a permanently throttled account the next* channels still end up UNDEF — but now UNDEF means what it says).

A follow-up PR will skip this poll entirely while no notification channel is linked.

Transparency: this patch was developed with AI assistance (Claude); every commit carries an AI-assisted-by trailer. All changes were built, tested and verified on a production system by the author.

Martin Littkovsky added 2 commits August 18, 2026 10:44
Amazon reports the actual failure cause in the x-amzn-ErrorType header
(for /api/notifications: "ThrottlingException", body {"message":"Rate
exceeded"}, see openhab#19781), while the status line only carries a generic
"Bad Request". The header is now part of the exception message, and the
cookie-free response body is logged at DEBUG, capped at 512 characters
and only while TRACE is off - previously the body was only visible at
TRACE, which also dumps session cookies.

A response identified as throttled - status 429, or an x-amzn-ErrorType
starting with "ThrottlingException" - additionally fails fast instead of
going through the generic three retries with a fixed 2 s pause: those
all land inside the same limit window, and every retry is itself a
counted request that keeps the account throttled. FailMode.NORMAL keeps
its behaviour, it never retried in the first place.

The classification and the assembly of the failure reason are extracted
into two package-private static methods, so both can be tested without a
live response. The reason phrase is treated as optional, because a
response is not required to carry one.

Also adds the missing return after the "Request aborted." completion:
the second completeExceptionally() on a status of 0 was a no-op.

Related to openhab#19781, openhab#19714, openhab#21146.

AI-assisted-by: Claude Code
Signed-off-by: Martin Littkovsky <2018turtle@proton.me>
A failed notifications poll was indistinguishable from "no
notifications set": getNotifications() swallowed the exception and
returned an empty list, which set every next* channel to UNDEF (openhab#20018)
and left nextRefreshNotifications at Long.MAX_VALUE, silently disabling
the event-driven refresh until the next hourly cycle.

The failure now propagates - getNotifications() is the only one of the
swallowing list getters around it whose empty result is published as a
statement about the device, the others feed command options and
per-device states - and the caller distinguishes transient from
sustained failure: the last known state survives short outages, and
from the third consecutive failure onwards the next* channels go UNDEF,
because at that point the binding genuinely does not know the state
anymore. The update is repeated on every further failure, so an echo
handler that registers during the outage is told as well.

Retries back off from 300 s, doubling up to the regular 3600 s refresh
interval and resetting on success; a flat retry interval measurably
keeps a throttled account throttled. The third failure is therefore
reached about 15 minutes after the first. refreshNotifications() also
returns early while the backoff is running, so the push path
(PUSH_NOTIFICATION_CHANGE) can no longer bypass it.

Failure count, delay and the resulting deadline are one state and live
together in NotificationPollBackoff behind one lock, because the poll is
reached both from the polling job and from a Jetty I/O thread: with the
deadline in a separate caller field, a poll succeeding on one thread and
a poll failing on the other could each write one half and leave a
backoff that never became due again. nextRefreshNotifications now
carries only the success path's wake-up time.

A re-login clears the backoff, so failures caused by the expired session
do not keep the poll silent after the user has fixed it.

Failures are logged once per streak at WARN, repeats at DEBUG, the
transition to UNDEF at WARN and recovery at INFO.

Related to openhab#20018, openhab#21146.

AI-assisted-by: Claude Code
Signed-off-by: Martin Littkovsky <2018turtle@proton.me>
@ML19821
ML19821 requested a review from mgeramb as a code owner August 19, 2026 10:41
@jaywiseman1971

jaywiseman1971 commented Aug 19, 2026

Copy link
Copy Markdown

@ML19821 , would love to try this JAR and be a beta tester for it.

I have a very heavy implementation of Amazon binding which has been broken for over 1.5 years now with many open bug reports from me on it.

What makes me unique is I have 15 Echos, 10 Tiles (grabs x/y locations from them) and 9 smart home devices hanging off this Amazon account.

A lot of the features of the Echo's don't work anymore such as LAST SPOKEN TTS and Tile Locations.

I worked a lot with @mgeramb when it was developed originally.

Best, Jay

@lsiepel

lsiepel commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

@ML19821 Thanks for picking this up. It was also on my todo list.
I will have some time tomorrow to review and follow-up. Before i do, it is highly appreciated to reduce/remove the excessive amounts of inline comments. Code should be self explanatory.

@lsiepel
lsiepel self-requested a review August 19, 2026 12:03
@lsiepel lsiepel added the bug An unexpected problem or unintended behavior of an add-on label Aug 19, 2026
Reduce the comments introduced by the two previous commits to short
notes on actual constraints, as requested in review. No functional
change; all changed lines are comments.

Signed-off-by: Martin Littkovsky <2018turtle@proton.me>
AI-assisted-by: Claude Code
@ML19821

ML19821 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Done before your review (same as on #21429/#21382): inline comments trimmed to the binding's norm (2446a475, −84 comment lines net) — what remains are one-sentence constraints such as the >= rationale in the backoff and the retry-counts-against-the-limit note. No executable line changed, 136/136 tests green. The PR description was already rewritten to the short format, including the AI-assistance disclosure.

@ML19821

ML19821 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

@jaywiseman1971 Happy to have a tester with a setup that size — here is a build: https://github.com/ML19821/openhab-addons/releases/tag/amazonechocontrol-throttling-test-1 (5.2.2-SNAPSHOT for openHAB 5.2.x; uninstall the official binding first, then drop the jar into your addons folder).

Honest expectations: this PR makes throttling visible (real cause in the log), stops the binding from hammering a throttled endpoint, and turns the next* channels UNDEF when the data is genuinely unknown. It does not unlock a throttled account — on my test account even 10+ hours of complete silence did not lift the limit — and it does not touch other features such as lastVoiceCommand or Tile locations; those are separate code paths worth their own issues. If the log noise changes shape on your system, a snippet here would be valuable.

@lsiepel

lsiepel commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

@ML19821 would you be interested in fixing some of the amazonechocontrol issues? I'm sure @jaywiseman1971 will be more then willing to test (many times). I'm sure there will be others too. The amazonechocontrol binding could use some love and i'm happy to assist where i can.

Response bodies stay at TRACE; the DEBUG line keeps the metadata that
matters, the x-amzn-ErrorType header and the status.

AI-assisted-by: Claude Code
Signed-off-by: Martin Littkovsky <2018turtle@proton.me>
@ML19821

ML19821 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Done proactively, same as on the netatmo PR: the failure body is out of the DEBUG line (bodies stay at TRACE), the x-amzn-ErrorType metadata remains. PR description updated accordingly.

@jaywiseman1971

Copy link
Copy Markdown

Happy to have a tester with a setup that size — here is a build: https://github.com/ML19821/openhab-addons/releases/tag/amazonechocontrol-throttling-test-1 (5.2.2-SNAPSHOT for openHAB 5.2.x; uninstall the official binding first, then drop the jar into your addons folder).

I uninstalled the GUI one from OH 5.1.4 and dropped in yours but it required this below, so I had an OLD version of this back in the day (velocity-engine-core-2.3.jar) and dropped it in and it came online.

Unresolved requirement: Import-Package: org.apache.velocity; version="[2.3.0,3.0.0)"

Will provide feedback soon along with a debug log so you can see what is happening.

abc

Thank you again!
Best, Jay

@jaywiseman1971

Copy link
Copy Markdown

@ML19821, did a clean OH startup and got this error. Will continue to monitor.

2026-08-19 16:01:36.913 [WARN ] [.amazonechocontrol.internal.handler.AccountHandler] - Failed to get notifications for amazonechocontrol:account:account2, next attempt in 300 s: https://na-api-alexa.amazon.com/api/notifications failed with code 400: Bad Request (x-amzn-ErrorType: ThrottlingException:http://internal.amazon.com/coral/com.amazon.coral.availability/)

@ML19821

ML19821 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

@jaywiseman1971 One question that decides what helps you most: do you have any of the nextAlarm / nextTimer / nextReminder / nextMusicAlarm channels linked to items?

  • If yes: the build you have (test-1) is the right one to run — it is exactly this PR.
  • If no: I just opened draft [amazonechocontrol] Poll notifications only while a channel is linked #21442, which stops these polls entirely while nothing is linked (zero requests, zero log lines — verified on my installation tonight). I can provide a combined test build once you have answered; keeping the two apart until then so your feedback stays attributable to one change.

Either way, to keep expectations honest: neither build touches lastVoiceCommand or Tile locations — different code paths, worth their own issues.

@jaywiseman1971

Copy link
Copy Markdown

One question that decides what helps you most: do you have any of the nextAlarm / nextTimer / nextReminder / nextMusicAlarm channels linked to items?

Yes, all my echo's have nextAlarm and nextReminder and I reference nextAlarm every night to remind you that you don't have an alarm set.

Best, Jay

@jaywiseman1971

Copy link
Copy Markdown

@ML19821, this is positive.

The last 2 tiles I linked to Amazon account (many months ago) where ALWAYS in an UNKNOWN state, now they are not. Not saying they are working w/o me leaving the house yet, but at least they are online now.

TILES

Best, Jay

@ML19821

ML19821 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

@lsiepel Happy to take you up on that. I'll start with two that scratch my own itches: #18085 (the volume-then-speak race — my own hallway announcement suffers from exactly that) and #21426 (the push-stream parse errors; same shape as the shelly NotifyEvent fix — one unreadable field should not take down the whole message).

Full disclosure, since you rightly asked for clarity about AI involvement: this is very much a human-plus-Claude-Code operation. Claude does the heavy lifting, I do the testing on a real installation and take the blame 😉 — my own Java would not survive your review. So far the combination seems to work, but please keep the review bar exactly where it is.

I'll open issues/PRs as things actually get ready rather than promising timelines — and with @jaywiseman1971 volunteering to test, the feedback loop is covered.

@jaywiseman1971

Copy link
Copy Markdown

@ML19821, the lastvoicecommand is still not working, see below, actually have 16 echo's, LOL

Will start a debug and see if that grabs anything worth while.

lastvoicecommand

Best, Jay

@jaywiseman1971

Copy link
Copy Markdown

@ML19821, is there a place I can drop 7 x 10 mb amazon trace logs zipped up to you vs. on this forum since the logs have confidential information about my setup in it?

I used these TRACE commands.

log:set TRACE org.openhab.binding.amazonechocontrol
log:set TRACE org.openhab.binding.amazonechocontrol.internal.util
log:set TRACE org.openhab.binding.amazonechocontrol.internal.util.HttpRequestBuilder
log:set TRACE org.openhab.binding.amazonechocontrol.internal.connection
log:set TRACE org.openhab.binding.amazonechocontrol.internal.handler

@jaywiseman1971

Copy link
Copy Markdown

@ML19821, the nextalarm hasn't worked either for quite some time. With this new build, same results.

Screenshot_20260819_191051_Chrome Beta

@ML19821

ML19821 commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

@jaywiseman1971 Good call not posting them publicly — one important reason on top of your own: TRACE logs of this binding contain your Amazon session cookies, so treat them like a password. You can send them to the address on my commits: 2018turtle@proton.me. Proton accepts up to 50 MB of attachments per incoming mail, and mail encoding inflates files somewhat in transit — so split the seven zips across three mails (2–3 zips each) to be safe. I will treat them as confidential and delete them after analysis; if you want to be thorough, log the binding account out and back in afterwards so the old session cookies in the logs become worthless.

On lastVoiceCommand: that is a separate code path from this PR (it never touched it), but it is on the list I agreed with @lsiepel — your traces are exactly what makes it workable. Two asks for the capture, if not too late: note roughly when you spoke a command (so I can find the window), and which Echo you spoke to.

@jaywiseman1971

Copy link
Copy Markdown

@ML19821, this confirms that the Tiles aren't getting updated still with new build.

Lower priority than last voice command. I'm away from home most of the day today, will send zipped logs to u later. They all zip up to 5 mb total.

Screenshot_20260820_084411_Chrome Beta

Best, Jay

@lsiepel lsiepel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an initial AI-assisted review.

Two comments to look at

if (!connection.isLoggedIn()) {
return;
}
if (notificationPollBackoff.shouldSkip(System.currentTimeMillis())) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The backoff check does not reserve an in-flight poll. checkLoginAndData() invokes this method on the scheduler while PUSH_NOTIFICATION_CHANGE invokes it directly on the Jetty callback thread, so both can pass shouldSkip() before either request reports a result. This can send multiple requests during the same throttling window and advance the failure counter several steps at once, or let a racing success reset a throttling failure. Please serialize notification polls or model an in-flight attempt atomically, and add a concurrent-trigger regression test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Polls are now reserved atomically (acdd539): NotificationPollBackoff.tryStart() admits a single running attempt and hands out a token identifying it, and onSuccess()/onFailure() end the attempt under the same lock. A caller refused while an attempt is running does not lose its trigger: the refusal is remembered under that lock and handed to whoever ends the attempt — on success the handler schedules an immediate catch-up poll (the finished attempt's response may predate the pushed change), on failure it expires, because a push must not undercut the backoff deadline. The timer checks in checkLoginAndData() only keep the once-per-second tick quiet; admission is decided solely by tryStart(). Deterministic tests cover the refusal, catch-up on success, expiry on failure, and idempotence of a double refusal.

nextLoginCheck = 0;
nextDataRefresh = 0;
// failures of the expired session must not delay polls on the new one
notificationPollBackoff.reset();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reset can be undone by an older request that is still in flight. setConnection() runs from the servlet thread, while a scheduled or push-triggered refreshNotifications() may already be waiting on the previous Connection; when that call later fails, its catch block invokes onFailure() and installs a backoff for the newly authenticated session. Please invalidate results belonging to the old connection, for example with a captured connection generation, and cover re-login during an active poll with a regression test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also fixed, via the same token (acdd539): setConnection() releases the running attempt so the new session may poll immediately, and a late result carrying the old token is discarded — its failure cannot throttle the new session, its success cannot confirm it or delay its first poll. Late failure, late success and the release-on-reset are each covered by a test.

@lsiepel

lsiepel commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

@jaywiseman1971 please continue in the right issue to troubleshoot. This PR is about improving the visibility, not the issue itself.

@jaywiseman1971

Copy link
Copy Markdown

TRACE logs

This has been sent to your email and I pasted the THING Echo that I asked the questions to cross reference the echo used.

Best, Jay

…a replaced connection

Signed-off-by: Martin Littkovsky <2018turtle@proton.me>
AI-assisted-by: Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug An unexpected problem or unintended behavior of an add-on

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants