fix(batch): scope Batch job ids to the selected pool - #99
Conversation
Capacity-aware spillover failed whenever two tasks were in flight:
Job <h100-pool> is bound to pool <h100-pool> but this task targets pool
<t4-pool>, and rebinding failed (OperationInvalidForCurrentState).
Three properties collide. The Batch job id is static (it defaults to the
singular pool id), the pool is chosen per task by select_pool, and a Batch job
is permanently bound to the pool it was created against -- re-pointable only
while it has no active tasks. So the first task created a job on the preferred
pool, and a second task that spilled over to another pool collided with it.
Spillover broke in exactly the concurrent case it exists to serve.
Rebinding, added in #98, was the wrong remedy: one static job id cannot span
multiple pools, and once concurrency exists it cannot be rebound at all.
- new resolve_job_id(): derive the job id from the routed pool, so each pool
gets its own job. Environments with a single candidate pool keep their
existing job id, so no jobs are renamed there. Truncation to the 64-char
Batch limit removes characters from the base, never the pool, so two pools
can never collapse onto the same id.
- create_job() is now graceful and returns the job id it actually used: reuse
when the binding matches, rebind when the job is idle, otherwise fall back to
a pool-scoped job instead of failing the submission.
- processors capture the returned job id. All five persist jobId into metadata,
which is later used for status polling and log retrieval, so the id the
runner actually used has to be the one recorded.
- drop BatchJobPoolMismatchError, now unreachable.
… dump Pool ids were wired through GitHub Environment *variables*, but this repo is public and Actions logs are public with it. GitHub masks secret values in logs and does not mask variables, and `az functionapp config appsettings set` was printing the resulting settings JSON to stdout, so the values would have landed in a public log. Store them as environment secrets instead, matching how RESOURCE_PREFIX, LOCATION, RESOURCE_SUFFIX and STATIC_APP_DOMAIN are already handled, and add `--output none` so the app-settings dump is not emitted at all. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dd98b384-7e26-47a9-8c03-6f9594eb611c
RC artifacts readyAll branch deployment references use the same RC tag:
|
Verified in a live environmentDeployed this branch ( Setup: the H100 pool had a warm node and was running an imageryprep task; the T4 pool had an idle node. A third image layer was then submitted while the first was still running — the exact sequence that previously failed at submit time with Result:
Separate pre-existing issue found while testingSpillover only engages once the secondary pool already has a node. The T4 pool autoscales on I had to warm the T4 pool manually to exercise the spillover path here. This is a gap in the autoscale/routing interaction from the shared-pool work, not something introduced by this PR, so I have left it out of scope — but it is worth a follow-up, since in practice spillover will rarely trigger on its own. |
Problem
Two image layers submitted close together — the first ran, the second failed before reaching Batch:
Capacity-aware spillover broke in exactly the concurrent case it exists to serve.
Root cause
Three properties collide:
*_BATCH_JOB_ID).select_poolreturns the first candidate with an idle node.t4has no idle node → routed toh100; job created bound to h100h100busy,t4idle → routed to t4h100OperationInvalidForCurrentState— the job has active tasksThe
_rebind_job_poolI added in #98 was the wrong remedy: one static job id cannot span multiple pools, and the moment concurrency exists it cannot be rebound at all. This PR replaces that approach.Fix — one job per pool
New
resolve_job_id(base, selected_pool, candidates)derives the job id from the pool a task was routed to:baseis one of the candidate pool ids (the default convention)baseunchanged — legacy environments are not renamedbase"<base>-<selected_pool>", trimmed to 64Truncation removes characters from
base, never the pool, so two different pools can never collapse onto the same job id under the 64-char Batch limit.create_jobis now graceful and returns the job id it actually used — reuse when the binding matches, rebind when the job is idle, otherwise fall back to a pool-scoped job instead of failing the submission.Callers capture the returned job id
This is the easy-to-miss part.
add_taskreturns(job_id, task_id)and the returned id may now differ from the one passed in. All five processors persistjobIdinto metadata, which is later used for status polling and log retrieval — so each now captures the returned value (imagery,train,inference,artifacts,embedding). Previously none of them used the return value at all.LocalRunner.add_taskwas verified to honour the same contract (returns the tuple, or raises).Verification
create_jobcovered for reuse / rebind / fallback-creates / fallback-reuses.108 passed, 7 skippedlocally (modules needinggeopandas/shapely/pytest-mockexcluded — absent from my env, unrelated to this change).pre-commit runclean;Config driftguard still passes (no application settings change here).Blast radius
Limited to environments routing across multiple pools. Single-pool environments resolve to the identical job id they use today, so no jobs are renamed and no in-flight metadata is invalidated. Existing
preprocessJob.jobIdvalues keep pointing at jobs that still exist.Affected environments self-heal — the spillover route simply starts using its own job. No manual job cleanup required.
Notes
BatchJobPoolMismatchError, now unreachable.*_POOL_IDnote indocs/configuration.mdthat fix(batch): sync Batch application settings across code and deploy paths #98 added, which described the (now superseded) rebinding behaviour.spec/features/batch-pool-job-binding/.Note on the second commit.
fix(deploy): store Batch pool config as secrets...is replicated from #94, where it currently also lives. It is required here so this branch can be deployed to a test environment on its own: the pool config was moved from GitHub Environment variables to secrets (variables are not masked in this repo's public Actions logs), so a branch still readingvars.BATCH_*resolves them to empty and falls back to a derived pool name that no longer exists.The content is identical to #94's copy, so whichever merges second should collapse cleanly.