Repository navigation
fix(batch): read the stopped run, retry without duplicate jobs, and recover every unfinished task - #48
Draft
emirlan-zen wants to merge 2 commits into
Draft
fix(batch): read the stopped run, retry without duplicate jobs, and recover every unfinished task#48emirlan-zen wants to merge 2 commits into
emirlan-zen wants to merge 2 commits into
Conversation
`POST /jobs/{id}/stop` answers with the refreshed run, not the job. The
client typed it as a job and read `latest_run`, so `zenrows batch cancel`
printed "status: unknown" and `--json` dropped the status and stats.
Type the response as a run and print its status and stats under the job id
the caller passed. The test fixtures now use the run shape the API returns.
The MCP batch-jobs skill now tells agents to put per-task scrape overrides
under `zenrows_params`, where the MCP server reads them; some versions
ignore the same keys set flat on a task.
…nd price runs as submitted Every existing error code, exit code, JSON key, flag and the --timeout unit stay as they were; new detail comes in new fields and flags. Retries and unknown results - Status reads retry 429/502/503/504 with backoff and Retry-After, so a transient error no longer aborts `wait` or a paginated `results`. - `create` and `retry-failed` send an Idempotency-Key (fresh, or the new `--idempotency-key`) and reuse it for their own automatic retries on 502/503/504 and connection errors. Within the key's 24 hours a retry under it never starts a second job or run, though its answer is not guaranteed to be the original (a replay, a 409, or a refusal made before the key is checked, such as a 402). - `--idempotency-key` must be 1 to 128 characters with no surrounding whitespace or control characters, since HTTP would strip a blank key to none at all. - A write's result is unknown when no attempt got a usable answer, when the last answer is a 5xx, when any refusal follows an attempt that got no usable answer, or when a 2xx answer is empty, not JSON, or has no job_id. Then the error keeps its code, adds `idempotency_key`, says not to resubmit with a new key, and prints the exact command that repeats the request with it (key option first, shell-quoted). - With a workspace, `create` and `retry-failed` record the key in their run artifact. If `create --follow` fails after the job exists, the error names the job and says to follow it with `wait`. Recovery and errors - New `retry-failed --include-pending` reruns failed, pending and processing tasks, so a stopped or credit-failed run can be finished. - 409 answers keep BATCH_FAILED and get specific guidance. - Errors add the API's code as `server_code` and a finer `reason` (insufficient_credits, api_key_cap_reached, too_many_active_jobs, too_many_scheduled_jobs, run_paused, wait_timeout) next to the unchanged `code`. The 429 advice says how each kind of slot is freed. - `cancel` reads the run that stop returns and adds its status and stats; a successful stop still exits 0 with ok:true. Estimate and submitted params - The estimate prices the params as submitted: job params, then the task's `zenrows_params`, then its flat keys; a task key set to null keeps the job's value, for the estimate and the proxy_country check alike. - The policy credit cap keeps checking the previous estimate, so a run that passed before still passes; when the corrected estimate is over the cap, `create` warns with the `zenrows policy set` command. - Boolean params are sent as "true"/"false"; values the API rejects (1, "1", "yes") and non-string metadata are refused before sending. Smaller fixes - `results --status processing` is accepted. - `wait --timeout` stays in milliseconds; `600s` and `600000ms` also work. A non-numeric value is still ignored (default 600000 ms), with a warning. - `wait` on a paused run fails right away with BATCH_FAILED (reason run_paused) and exit 1, as the timeout it would reach did. - Policy cap errors name the exact `zenrows policy set ...` command; the defaults are documented in the batch skill and README. - Skills and README: Batch is the reliable path at scale at Fetch's credits (not the cheapest), Extract adds no charge of its own, `batch_wait` is a listed MCP tool, and the flag names match the CLI.
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.
This PR fixes several ways
zenrows batchmisreports a run, prices it, retries it, or gets stuck. It touchescancel,retry-failed,wait,resultsandcreate.Nothing that works today changes. The existing error codes, exit codes, JSON keys, flags and
--timeoutunits all stay as they are. New detail comes in new fields (server_code,reason,idempotency_key,paused) and new flags. The only differences in existing behaviour are a few values the Batch API already rejects, which are now refused locally before sending. The machine diff againstmainunder Compatibility shows this.batch cancelreads the run that stop returnsPOST /jobs/{id}/stopreturns the refreshed run, not the job. The client read the response as a job and looked forlatest_run, so the run's status and stats never showed.A successful stop call still exits 0 with
ok: true, whatever the run's status, as before. The status, stats and failure fields are additions. The test fixtures mocked/stopwith a job-shaped body, which hid the bug; they now use the run shape.Recovering a run that did not finish
--include-pendingonretry-failed. It reruns?status=failed,pending,processing, so it covers every task that did not finish.pendingand can leave tasks cut off mid-flight asprocessing.processingwould carry those rows over unfinished, and the new run could never complete.retry-failed(failed only) is unchanged.statuspoints at this flag.BATCH_FAILED, with specific advice. Instead of "fix the problem and retry":no_matching_tasks,partial_rerun_window_expired(10 days) andrun_not_stoppableeach get their own next step.run_not_terminalseparates a run that is still in progress from a run whose task ingestion never completed.idempotency_key_conflictis covered below.Automatic retries never start a second job
GETs retry 429/502/503/504 up to 3 times, with backoff andRetry-After, the same rulecrawluses. A transient 502 no longer abortswaitor loses the pages a paginatedresultshad already fetched.createandretry-failedsend anIdempotency-Key. That's a fresh one by default, or your own with the new--idempotency-key. They reuse it for their own automatic retries on 502/503/504 and on connection errors. A 429 on a POST is an account limit and is not retried.--idempotency-keymust be 1 to 128 characters, with no surrounding whitespace and no control characters. HTTP strips a header's surrounding whitespace, so a blank key would reach the API as no key at all. This is a new flag, so nothing that worked before is refused.When a write ends with an unknown result
Only
create(POST /jobs) andretry-failed(POST /jobs/{id}/rerun) create billable work. Their result is unknown, and the error keeps its usualcodewhile addingidempotency_key, in each of these cases:tests/batch-uncertain.test.ts)A: create and rerun with no answer on every attempt keep the keyB: a write that times out on every attempt keeps the keyC: create and rerun whose last answer is a 5xx keep the keyD:testsjob_id. The request was accepted, so the job or run probably exists.E: a 2xx with an empty, truncated or job_id-less answer keeps the key (create and rerun)create --follow: the job was created, then following it failed. The error names the job and says not to create it again.F: create --follow that fails after the job exists names the job and says not to create againIn cases A to E the error says not to resubmit with a new key, and prints the exact command that repeats the request with its key:
--idempotency-key <the key that was sent>comes right after the subcommand, so it stays an option even after a--;Tests run the printed command through
shand through the CLI, and check that the repeat sends the same key. A repeat within 24 hours of the first attempt cannot create a duplicate. After that, the API forgets the key.Definite outcomes carry no key: a refusal on the first attempt, and every
GET.cancelsends no key and is not retried, because the API treats a repeated stop as a no-op. With a workspace,createandretry-failedrecord the key in their run artifact.Running a command again without the key is not deduped. A repeated
createcreates another job. A repeatedretry-failed, once the previous run has ended, may start another run of the tasks that still match, or getno_matching_tasks. The README, skill and help text say so.Errors: same codes, more detail
Every code stays as it was for the situation that produced it. The new fields come on top:
code(unchanged)POLICY_MAX_CREDITS_EXCEEDEDserver_code,reason: insufficient_creditsapi_key_cap_reachedKEY_CREDIT_CAP_REACHEDserver_code,reason: api_key_cap_reachedBATCH_QUOTA_EXCEEDEDserver_code,reason: too_many_active_jobsBATCH_QUOTA_EXCEEDEDserver_code,reason: too_many_scheduled_jobsfailed,api_key_cap_reachedKEY_CREDIT_CAP_REACHEDserver_code,reasonfailed,insufficient_creditsBATCH_FAILEDserver_code,reason: insufficient_credits, credit advicewaittimed outBATCH_FAILEDreason: wait_timeoutAUTH_INVALID/BATCH_ACCESS_DENIED/BATCH_NOT_FOUNDserver_codeBATCH_FAILEDserver_codeThe 429 advice now says how each kind of slot is freed:
Credit estimate and submitted params
estimateCreditsused to price only flat per-line flags. It ignored job-level flags and nestedzenrows_params, and treated the string"false"as on. Two URLs with--premium-proxyprinted~2 creditswhile sendingpremium_proxy: true(real cost 20). Each task is now priced on the params it is submitted with: job params, then the task'szenrows_params, then its flat keys. A task key set tonullis not sent, so the job's value applies, both for the estimate and for theproxy_countrycheck.max_credits_per_runcheck keeps using the old estimate, so a run that passed the cap before still passes. When the corrected estimate is over the cap,createprints a warning with the exactzenrows policy setcommand, and submits as before.true/falseand the strings"true"/"false"in any case. It rejects anything else,1and"yes"included, with a 400 that fails the whole job. The CLI now sends"true"/"false", and refuses other values before sending (see below).Smaller fixes
metadatais checked as string-to-string before submitting. The API accepts nothing else.results --status processingis accepted. The API supports it.wait --timeoutstill takes milliseconds, and a bare number is read exactly as before.600s(seconds) and600000msare also accepted, and must be positive.create --followtakes--timeouttoo.waiton a paused run fails right away withBATCH_FAILED(reason: run_paused) and exit 1. That's the same code and exit as the timeout it used to wait for. A paused run picks up no new task.statusshows a paused run with"paused": true. A stopped run that keptpause_stateis not reported as paused.zenrows policy setcommand and state the default.batch_waitis listed as an MCP tool.--follow, and--waitstays as an alias.Compatibility
A compatibility script, run locally and not part of this PR, builds
origin/main(c8eefa6) and this branch, then runs 44 scenarios against both through a local mock of the Batch API. The scenarios cover every subcommand, every error status, failed, paused and stopped runs, timeouts, policy caps and malformed answers. For each scenario it compares the exit code,ok, the errorcode, and the full JSON key shape. It also diffs--help, each subcommand's flags, and theErrorCodeunion.okchangedcreate --idempotency-key,create --timeout,retry-failed --idempotency-key,retry-failed --include-pendingErrorCodevalues removed or addedcodechangedmetadatavalue andjs_render: 1(BATCH_FAILEDbecomesINVALID_USAGE)error.server_code,error.reason,error.idempotency_key,error.suggested_commands[](now filled where it was empty), and oncancelstatus,stats,failure_reason;pausedonstatus--helpresults --statusaddsprocessing;wait --timeout <ms>adds the default and thesunit), 8 lines added for the new flagsTesting
npm test: 430 passed, 0 failed, run undernice -n 19.origin/mainhas 364, so this branch adds 66:batch canceltest intests/batch-command.test.ts;tests/batch-hardening.test.ts;tests/batch-uncertain.test.ts.main's tests still pass. The error-code assertions in its own
tests/batch-api.test.tsandtests/batch-command.test.tsare back to main's codes, with the newserver_code/reasonasserted next to them. The only fixture changed is the/stopbody, which now has the run shape the API actually returns.npm run typecheck: clean.Coverage of the changed lines. I ran
node --test --experimental-test-coverageon a sourcemapped build and intersected the lcov report with the lines this branch adds: 717 of 717 changed executable lines are covered (100%).src/adapters/batch.tssrc/cli/commands/batch.tssrc/core/batch-api.tssrc/core/errors.tssrc/core/policy.tsBuilt CLI against a local mock server:
POLICY_MAX_CREDITS_EXCEEDEDand the credit advice, added that the earlier attempt may have been accepted, and printed the repeat command with the key.my "key" it's: the command came out quoted, andshread the key back intact.create ... -- flaky.jsonlwith all 4 answers lost: the printed command put--idempotency-keybefore the--. Running it sent the same key and got the original job back.create --followwhose status reads kept failing: the error named the job and pointed tozenrows batch wait <id>.Needs a human decision
Open points from the last fact-check
A final independent fact-check of this branch against the Batch API source left these points open: