Repository navigation
fix(apps): answer rate-limited calls with backoff instead of request hints - #2882
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the
Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2882 +/- ##
=======================================
Coverage 76.47% 76.47%
=======================================
Files 1156 1156
Lines 133878 133891 +13
=======================================
+ Hits 102379 102394 +15
+ Misses 23214 23213 -1
+ Partials 8285 8284 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…st hints The OpenAPI gateway answers throttled calls with code 99991400, which internal/errclass already classifies as api/rate_limit with retryable=true and no hint. withAppsHint then fills the empty hint with the command's request-shaped fallback, because its gate only withholds that fallback for failed_precondition. A rate-limited +db-execute therefore tells the caller to "verify table/column names with +db-table-get" and to "target the dev database with --environment dev"; +trace-get says to verify --app-id. None of that can explain a call the server never evaluated, and an agent that follows the advice sends more requests into the same limit. withDBSyncHint, used by the db-sync commands, has no subtype gate at all and does the same. These cases fail on the current code and are committed ahead of the fix, as hintExplainsFailure asks before its gate is widened: - TestAppsDBExecute_RateLimitHintIsBackoffNotRequestFix runs +db-execute against a 99991400 envelope (HTTP 200 and 429) through the real classification path and requires the hint to drop the table/column and --environment dev advice in favour of backoff guidance - TestAppsHints_RateLimitDoesNotInheritRequestHint drives a real 99991400 classification through withAppsHint (+trace-get, +db-execute, +session-messages-list), withObservabilityHint and withDBSyncHint, and requires the same error value, unchanged classification, no inherited request hint and backoff guidance - TestWithAppsHint_RateLimitKeepsUpstreamHint guards that a hint the upstream already supplied (e.g. a retry-after sentence) is kept; it passes today
…hints When the OpenAPI gateway throttles an apps call (code 99991400), the error is already classified as api/rate_limit with retryable=true, but it reaches withAppsHint with an empty hint and leaves carrying the command's fallback: "verify table/column names with +db-table-get ... target the dev database with --environment dev" on +db-execute, "verify --app-id" on +trace-get, "verify --session-id / --turn-id" on +session-messages-list. The server never evaluated those requests, so none of that advice can be right, and an agent that follows it changes a correct request and sends more calls into the same limit. The db-sync commands do the same through withDBSyncHint, which has no subtype gate. - Withhold the caller's hint for SubtypeRateLimit in hintExplainsFailure, next to failed_precondition. The gate stays per subtype: authentication, authorization and server errors keep their command hints as before - Fill the empty hint with a shared appRateLimitHint instead: the arguments are not the problem, stop immediate retries, rerun later with exponential backoff and jitter, and send fewer calls in parallel. The backoff wording follows the existing rate-limit hints in internal/client and internal/credential and the api/rate_limit row of errs/ERROR_CONTRACT.md - Apply the same answer in withDBSyncHint before its code table and fallback - Keep any hint the upstream already supplied, as for every other class Classification, code, retryable, log_id, the cause chain and the exit code are unchanged; only error.hint differs. All apps commands pick this up through the two helpers, so no command file changes.
7df6be4 to
2c108ef
Compare
🚀 PR Preview Install Guide🧰 CLI updatenpm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@c144f12985d71358619af7040ba6c192477bf7dc🧩 Skill updatenpx skills add larksuite/cli#fix/apps-rate-limit-backoff-hint -y -g |
Not every apps command goes through withAppsHint, withObservabilityHint or withDBSyncHint: +list returns the client error as is, so a throttle there keeps rate_limit with an empty hint. Say which commands the test covers instead of claiming all of them.
…t hints The hintExplainsFailure comment said the gateway's 99991400 reached every apps command through the request-hint fallback. Commands that return the client error as is, such as +list, never get a request hint, so say which commands it reached.
Summary
When the OpenAPI gateway throttles an
appscall (code99991400), lark-cli already classifies the error correctly asapi/rate_limit,retryable: true. But theappshint fallback then fills the empty hint with the command's request-shaped advice. A rate-limited+db-executetells the caller to "verify table/column names with+db-table-get… target the dev database with--environment dev".+trace-gettells it to "verify --app-id", and+session-messages-listtells it to "verify --session-id / --turn-id".The server never evaluated those requests, so that advice cannot be right. An agent that follows it changes a correct request and sends more calls into the same limit. This PR answers a rate limit with backoff guidance instead. It changes only
shortcuts/apps.Root cause
internal/errclass/codemeta.gomaps99991400toapi/rate_limit(retryable).APIHinthas no wording forrate_limit, so the error leaves the classifier withhint == "".withAppsHint(shortcuts/apps/common.go) fills an empty hint with the caller's fallback wheneverhintExplainsFailureis true. That gate only excludedfailed_precondition, sorate_limitgot the request-shaped hint.withDBSyncHint(shortcuts/apps/db_common.go), used by the+db-sync-*commands, has no subtype gate at all and did the same.errs/ERROR_CONTRACT.md), and this path carries noretry_after_seconds. The hint is therefore the caller's only guidance, and it pointed the wrong way.Before (
+db-execute, error envelope):{"type":"api","subtype":"rate_limit","code":99991400,"message":"request trigger frequency limit","hint":"verify table/column names with `lark-cli apps +db-table-get --app-id app_x --table <table>`; for day-to-day debugging target the dev database with `--environment dev`","retryable":true}After:
{"type":"api","subtype":"rate_limit","code":99991400,"message":"request trigger frequency limit","hint":"the request was rate limited, not rejected for its arguments, so do not change them; stop immediate retries, then rerun the same command later with exponential backoff and jitter, and send fewer calls in parallel","retryable":true}Changes
The PR has five commits:
appscommand goes through these helpers.hintExplainsFailurecomment.shortcuts/apps/apps_rate_limit_hint_test.go.TestAppsDBExecute_RateLimitHintIsBackoffNotRequestFixruns+db-executeagainst a99991400envelope (HTTP 200 and 429) through the real classification path. It asserts that the classification is unchanged, that the hint no longer contains+db-table-get,--environment devortable/column, and that it containsbackoff.TestAppsHints_RateLimitDoesNotInheritRequestHintdrives a real99991400classification throughwithAppsHint(with the+trace-get,+db-executeand+session-messages-listhints),withObservabilityHintandwithDBSyncHint. It asserts the same error value, unchanged classification, no inherited request hint, and backoff guidance.TestWithAppsHint_RateLimitKeepsUpstreamHintis a guard: a hint the upstream already supplied (e.g. a retry-after sentence) is kept. It passes before and after the fix.hintExplainsFailurenow also returns false forSubtypeRateLimit. The gate stays per subtype, so authentication, authorization and server errors keep their command hints, as the existing tests require. The comment above it names the failing tests, as it asks before the gate is widened.withAppsHintfills an empty hint on a rate limit with a new sharedappRateLimitHint.withDBSyncHintapplies the same hint after its "upstream hint wins" check and before its code table and fallback.internal/clientandinternal/credentialand theapi/rate_limitentry inerrs/ERROR_CONTRACT.md.withAppsHintandwithObservabilityHintrecognise some failures from message text (no database yet, a database subcode, no running container) and rewrite the message, the hint and, for database subcodes, the classification. They used to do that before looking at the subtype, so a throttle whose text happened to contain one of those markers was turned into "create a database" or "deploy the app" advice, and a database subcode even reclassified it asfailed_preconditionand droppedretryable.TestWithAppsHint_RateLimitWinsOverDatabaseMessageMarkersandTestWithObservabilityHint_RateLimitWinsOverContainerMessageMarkercover the three marker paths. The gateway's current throttle text does not contain these markers, so this closes an invariant gap rather than a reported case.The
appscommands that attach a request-shaped hint reach one of these helpers (withObservabilityHint,withRoleErrorHintand the icon helper delegate towithAppsHint), so no command file changes. Commands that return the client error as is, such as+list, keep the classifier'srate_limitwith an empty hint, which does not mislead. Commands with their own error helpers are not changed here.Not changed:
type,subtype,code,retryable,log_id, the cause chain and the exit code. Onlyerror.hintdiffers, and only forrate_limiterrors that arrive without a hint. Arate_limiterror is also no longer rewritten by the message heuristics, so its message and classification stay as the classifier produced them.internal/errclassand other domains are untouched. A globalAPIHintentry was considered and rejected:1063006(a daily, non-retryable quota) shares therate_limitsubtype and would get contradictory wording, and some drive/doc download helpers skip their own, more specific wording when the hint already mentions backoff.Test Plan
TestAppsDBExecute_RateLimitHintIsBackoffNotRequestFix: HTTP 200 and 429TestAppsHints_RateLimitDoesNotInheritRequestHint: all 5 helper pathsgo test ./shortcuts/apps/... ./internal/errclass/ -count=1passes. This includes the existing gate contracts:TestWithAppsHint_WithholdsHintForFailedPrecondition,TestWithAppsHint_FillsHintForOtherClasses(authentication 99991663 and 503 keep their hints), the no-database, db-subcode, db-sync, role and observability hint tests, andTestAppsFeatureEnable_RateLimit*.go test -count=1 ./cmd/... ./internal/... ./shortcuts/... ./extension/... ./errs/...passes: 135 packages ok and 0 failures. This is themake unit-testpackage set plus./errs/..., run without the race and-N -lflags.failed_preconditionwithretryable=false, the no-database message, the no-container message) and pass on the third.make vetandmake fmt-checkpass.go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6 run --new-from-rev=<base>with Go 1.23.0 (the CI toolchain): 0 issues. The diff-scopedlintmodule and its tests pass.make quality-gatewith this PR's title, body and branch as public-content metadata: no REJECT.tests/live e2e (needs live credentials) and any check against a real gateway throttle, which cannot be triggered on demand. In the unit tests the 99991400 envelope is simulated withhttpmock(HTTP 200 and 429 bodies).Agent trajectory check
This change is about what an agent does after a throttle, and unit tests cannot show that. So both binaries were also driven by clean agent sessions.
f31194b4), one from this branch (b09fbcb1).99991400envelope (HTTP 400,request trigger frequency limit). It does so for everyappscall in the first 45 seconds, and for any call that comes less than 2 seconds after the previous one. After that, it serves SQLite copies of an online database and a dev database.skills/;b09fbcb1; the two later commits only reword comments.+db-table-getright after a throttled+db-execute(what the old hint says to do)appscalls per run (mean)subtype: rate_limitand backs off with both binaries, so the difference only shows with the smaller model.--environment devadvice could change which data is read.Related Issues
Summary by CodeRabbit