Skip to content

fix: follow the task TTL of the taskmaster job - #118

Merged
uniqueg merged 6 commits into
devfrom
fix/task-ttl
Nov 7, 2026
Merged

uniqueg merged 6 commits into
devfrom
fix/task-ttl

Conversation

@uniqueg

@uniqueg uniqueg commented Nov 7, 2026 •

Copy link
Copy Markdown
Member

Summary

TESK API now follows the optional TTL of the taskmaster job (elixir-cloud-aai/tesk-core#109, part of elixir-cloud-aai/TESK#123).

  • ConfigMap owned by the taskmaster job. Kubernetes now deletes the task's ConfigMap together with the job, for example after its TTL.
    • The owner reference needs the job's UID, which exists only after the job was created. So TESK API now creates the job first and the ConfigMap right after it. The job's pod waits until the ConfigMap it mounts exists.
    • Why not patch the ConfigMap after the job? The TESK chart runs TESK API with the taskmaster service account. Its Role has no patch on configmaps, but it already has delete on jobs, which the new order needs.
    • Before the job is created, a dry run (dryRun=All) checks the ConfigMap with a placeholder owner reference. That way an oversized ConfigMap, a missing right to set the owner (OwnerReferencesPermissionEnforcement, e.g. on OpenShift) and a name that's already taken all fail before anything exists. A taken name retries with a new one, as before.
    • If the real ConfigMap still fails, TESK API deletes the job again with background propagation. If that delete fails too, the request fails without a retry, so no more jobs are left behind and none runs with another task's ConfigMap.
    • If an admission webhook for ConfigMaps doesn't support dry runs, the check is skipped with a warning.
    • blockOwnerDeletion isn't set, so it needs no right on the job's finalizers.
  • Jobs being deleted are gone. A taskmaster job with a deletionTimestamp counts as deleted: GET /tasks/{id} (and cancel) answer 404, and GET /tasks leaves it out.
  • Docs.
    • docs/index.md: the creation steps, a new section "Failures when creating a task", a new section "Deleting tasks", and a note on short pages.
    • docs/deployment.md: the RBAC table adds delete on jobs, plus delete on configmaps for OwnerReferencesPermissionEnforcement.
    • docs/upgrading.md: the new rights, the behaviour change, and that ConfigMaps from before the upgrade have no owner.

Closes #117

Verification

  • ./mvnw clean verify checkstyle:check (Temurin 8): 7363 unit tests and 77 integration tests pass, 0 Checkstyle violations; spotless:apply was run.
  • mkdocs build --strict: clean.
  • New tests:
    • Written first and seen failing.
    • TesServiceImplCreateTest covers:
      • the order: dry run, then job, then owned ConfigMap;
      • the placeholder owner in the dry run;
      • a refused dry run, which creates no job;
      • name collisions found by the dry run and found after it;
      • a ConfigMap failure, which deletes the job;
      • a failed delete, which stops retries;
      • a failed job creation.
    • KubernetesClientWrapperTest covers:
      • readTaskmasterJob and listAllTaskmasterJobsForUser with a deletion timestamp;
      • deleteJob (Background);
      • checkConfigMap (dryRun=All).
    • NoAuthIT checks against WireMock that both the dry-run request and the real one carry the owner reference, and that the real one names the created job's UID.
  • Reviews (adversarial-reviewer and quality-reviewer, 2 rounds):
    • Round 1: mediums on leftover jobs when the ConfigMap fails after the job exists (oversized ConfigMap, a missing delete right), OwnerReferencesPermissionEnforcement, and the docs. Fixed with the dry run, no retry after a failed cleanup, and the docs.
    • Round 2: a medium on the dry run lacking the owner reference, fixed. Docs restructuring and a better log message, both done.
  • Sourcery (4 threads), all replied to and resolved:
    • Fixed: dry runs refused by a webhook without dry-run support; a 409 from the cleanup delete taken for a name collision.
    • Declined, see below: a race with a foreign ConfigMap of the same name; a timed-out job creation.
  • CI: iteration 1 failed only codecov/patch, on an unreachable null check, which I removed. Iteration 2 passed every check. Iteration 3 (the Sourcery fixes): see the checks.

Declined review findings

  • "The chart may not grant delete on jobs" (quality, round 1): checked. TESK's charts/tesk/templates/common/taskmaster-rbac.yaml on dev grants create, delete, get, list and patch on jobs, and create, delete and get on configmaps. TESK API's deployment uses that service account.

  • Unit tests may not pin Background and dryRun=All (quality, round 2): they do, with eq("Background") and eq("All") in KubernetesClientWrapperTest.

  • Deleting the job by name when createJob times out after Kubernetes created it (adversarial, round 2, low): only documented. The error can't tell a timeout before the job was stored from one after it, and a delete by name could hit a job of the same name created by another request.

  • Setting activeDeadlineSeconds on the taskmaster job, so a pod that never starts fails eventually (adversarial, round 1): not done. It would also limit how long a task may run, and that is the taskmaster's and the chart's decision, not part of this issue.

  • A ConfigMap of the same name created by another actor between the dry run and the real create (Sourcery): not changed. The name is random, and the window lasts milliseconds. An actor that can create ConfigMaps in the namespace can already change any task's input. Gating the taskmaster would need tesk-core.

  • A timed-out job creation (Sourcery): documented, as above.

Open questions

None.

🤖 Generated with Claude Code

uniqueg and others added 4 commits November 7, 2026 21:55
The taskmaster's job now owns the task's ConfigMap, so that Kubernetes
deletes it with the job (e.g. after the TTL of tesk-core#109). The
job's UID is known only after it was created, so TESK API creates the
job first and the ConfigMap right after it, with the owner reference;
the job's pod waits for the ConfigMap it mounts. If the ConfigMap
can't be created, the job is deleted (background propagation) and the
error is passed on, so that a name collision still retries.

Taskmaster jobs with a deletion timestamp count as gone: GET
/tasks/{id} answers 404 and GET /tasks leaves them out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review findings: with the job created first, a ConfigMap that
Kubernetes refuses (e.g. above 1 MiB) or whose name is taken by a
leftover ConfigMap failed only after the job existed. A dry run of the
ConfigMap now catches these before the job is created; a taken name
retries with a new one, as before.

If the job can't be deleted after its ConfigMap failed, the request
fails without a retry, so that no more jobs are left behind and none
runs with another task's ConfigMap of the same name.

Docs: delete on configmaps (OwnerReferencesPermissionEnforcement, e.g.
OpenShift), phantom QUEUED tasks, short pages, upgrade steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round-2 review findings: the dry run of the ConfigMap had no owner
reference, so it couldn't catch OwnerReferencesPermissionEnforcement
refusing the owner (e.g. on OpenShift without delete on configmaps),
and the job was created for nothing. The dry run now carries an owner
reference to the job with a placeholder UID, which Kubernetes doesn't
store.

The log of a failed cleanup names the ConfigMap's error, too. Docs move
the failure handling to its own section, and note that a job whose
create request timed out after Kubernetes created it stays behind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Implementation

Create the taskmaster job before its ConfigMap, validate the ConfigMap with a placeholder owner reference, and clean up the job if real ConfigMap creation fails.

  • src/main/java/uk/ac/ebi/tsc/tesk/tes/service/TesServiceImpl.java L12-18, L77-83, L99-154 (+1 small edits)

Add Kubernetes helpers for dry-run ConfigMap validation and background job deletion, and treat jobs with a deletion timestamp as absent from task reads and lists.

  • src/main/java/uk/ac/ebi/tsc/tesk/k8s/constant/K8sConstants.java L30-35
  • src/main/java/uk/ac/ebi/tsc/tesk/k8s/service/KubernetesClientWrapper.java L71-118, L127-144, L297-304 (+1 small edits)

Documentation

Explain the revised task creation order, failure handling, task deletion lifecycle, and behavior for jobs being deleted.

  • docs/index.md L41-46, L65-81, L139-160

Document the additional Kubernetes delete permissions required for job and ConfigMap cleanup.

  • docs/deployment.md L52-55

Describe upgrade implications, required RBAC changes, and the new behavior for deleting tasks.

  • docs/upgrading.md L88-109

Tests

Test that task creation validates ownership before creating the job, creates an owned ConfigMap afterward, and handles collisions and cleanup failures safely.

  • src/test/java/uk/ac/ebi/tsc/tesk/tes/service/TesServiceImplCreateTest.java L1-281

Test dry-run validation, background job deletion, and omission of jobs marked for deletion.

  • src/test/java/uk/ac/ebi/tsc/tesk/k8s/service/KubernetesClientWrapperTest.java L55-105, L168-229

Update integration-test request stubs for URL path matching and verify dry-run and persisted ConfigMap owner references.

  • src/integration-test/java/uk/ac/ebi/tsc/tesk/AuthIT.java L89-242 (5 small edits)
  • src/integration-test/java/uk/ac/ebi/tsc/tesk/ClaimAuthIT.java L140, L157, L171
  • src/integration-test/java/uk/ac/ebi/tsc/tesk/NoAuthIT.java L77-82, L93-112

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov

codecov Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 65.00%. Comparing base (fa3a97b) to head (caf28be).

Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #118      +/-   ##
==========================================
+ Coverage   64.05%   65.00%   +0.94%     
==========================================
  Files          66       66              
  Lines        2579     2626      +47     
  Branches      268      272       +4     
==========================================
+ Hits         1652     1707      +55     
+ Misses        778      769       -9     
- Partials      149      150       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Kubernetes always returns a job's metadata, and the code around it
reads the metadata without a check; the branch was the only line of the
patch without coverage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@uniqueg
uniqueg marked this pull request as ready for review November 7, 2026 21:26

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 4 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/main/java/uk/ac/ebi/tsc/tesk/tes/service/TesServiceImpl.java" line_range="82-83" />
<code_context>
+                // a placeholder UID, which Kubernetes doesn't store
+                setOwner(taskMasterConfigMap, taskMasterJob, UUID.randomUUID().toString());
+                this.kubernetesClientWrapper.checkConfigMap(taskMasterConfigMap);
                 V1Job createdJob = this.kubernetesClientWrapper.createJob(taskMasterJob);
+                this.createOwnedConfigMap(taskMasterConfigMap, createdJob);
                 return this.converter.fromK8sJobToTesCreateTaskResponse(createdJob);
             } catch (KubernetesException e) {
</code_context>
<issue_to_address>
**Task runs with another task's input**

When another actor creates a ConfigMap with the generated name after the dry run but before the real ConfigMap create, and the pod starts before cleanup removes it, `createOwnedConfigMap` encounters the name conflict after `createJob` has started a pod; the pod mounts the existing ConfigMap by name and can run with another task's input before cleanup removes it.

Gate the pod from processing until the ConfigMap created for its job is verified.

Also at `src/main/java/uk/ac/ebi/tsc/tesk/tes/service/TesServiceImpl.java:130`.
</issue_to_address>

### Comment 2
<location path="src/main/java/uk/ac/ebi/tsc/tesk/tes/service/TesServiceImpl.java" line_range="82" />
<code_context>
+                // a placeholder UID, which Kubernetes doesn't store
+                setOwner(taskMasterConfigMap, taskMasterJob, UUID.randomUUID().toString());
+                this.kubernetesClientWrapper.checkConfigMap(taskMasterConfigMap);
                 V1Job createdJob = this.kubernetesClientWrapper.createJob(taskMasterJob);
+                this.createOwnedConfigMap(taskMasterConfigMap, createdJob);
                 return this.converter.fromK8sJobToTesCreateTaskResponse(createdJob);
</code_context>
<issue_to_address>
**Timed-out creation leaves a waiting job**

When the job-create request times out or otherwise errors after Kubernetes has stored the job, `createTask` exits on the exception without creating the ConfigMap, while Kubernetes may already have stored the job; its pod then waits for the missing ConfigMap and the client never receives the task ID. Reconcile the ambiguous create outcome and either finish setup or remove the created job when safe.

Reconcile an ambiguous job-create result so a stored job either receives its ConfigMap or is safely removed.
</issue_to_address>

### Comment 3
<location path="src/main/java/uk/ac/ebi/tsc/tesk/k8s/service/KubernetesClientWrapper.java" line_range="94" />
<code_context>
+     */
+    public void checkConfigMap(V1ConfigMap map) {
+        try {
+            this.coreApi.createNamespacedConfigMap(namespace, map, null, DRY_RUN_ALL, null);
+        } catch (ApiException e) {
+            throw KubernetesException.fromApiException(e);
</code_context>
<issue_to_address>
**Task creation fails with webhook**

When the cluster has a matching admission webhook that rejects dry-run requests, `checkConfigMap` propagates the admission error as a `KubernetesException`, so `createTask` exits before creating the job and every task-creation request fails.

Handle rejection of dry-run requests by the matching webhook without aborting task creation.
</issue_to_address>

### Comment 4
<location path="src/main/java/uk/ac/ebi/tsc/tesk/tes/service/TesServiceImpl.java" line_range="141" />
<code_context>
+                        e.getMessage(),
+                        deleteError);
+                deleteError.addSuppressed(e);
+                throw deleteError;
+            }
+            throw e;
</code_context>
<issue_to_address>
**Cleanup conflict creates more jobs**

When deleting the job after ConfigMap creation fails returns HTTP 409, `createTask` catches the propagated delete error as a `KubernetesException`; because `isObjectNameDuplicated()` treats every HTTP 409 as a name collision, it retries even though cleanup failed. The caller can get an error after additional jobs are left behind.

Propagate cleanup failures in a form the outer retry handler cannot mistake for a job-name collision.
</issue_to_address>

Linked issues

#117 fix: follow the task TTL in tesk-api · 3 of 3 met

# Requirement Status Notes
1 Give the task's ConfigMap an owner reference to the taskmaster job once the job exists. ✅ Met The ConfigMap is created after the job with an owner reference containing the created job's UID.
2 Treat taskmaster jobs with a deletion timestamp as gone in GET /tasks/{id} and GET /tasks. ✅ Met Single-task reads reject jobs being deleted, and task-list results filter them out.
3 Add regression tests and update docs for the task lifecycle and upgrade notes. ✅ Met Tests cover ownership and deletion-timestamp behavior; the docs describe task creation, deletion, and upgrade implications.

Sourcery assessment

Needs a human reviewer. 4 findings to address first, and when a taskmaster job is deleted or its TTL expires, Kubernetes now also deletes the task's ConfigMap, which holds its inputs and task configuration. If that deletion is wrong, reverting won't restore the ConfigMap or its contents.

Blocking findings: src/main/java/uk/ac/ebi/tsc/tesk/tes/service/TesServiceImpl.java:83, src/main/java/uk/ac/ebi/tsc/tesk/tes/service/TesServiceImpl.java:82, src/main/java/uk/ac/ebi/tsc/tesk/k8s/service/KubernetesClientWrapper.java:94, src/main/java/uk/ac/ebi/tsc/tesk/tes/service/TesServiceImpl.java:141


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Sourcery review findings:

- An admission webhook for ConfigMaps that doesn't support dry runs
  makes Kubernetes refuse every dry run (400, "does not support dry
  run"), which failed every task creation. The check is now skipped
  with a warning in that case.
- A 409 from deleting the job after a failed ConfigMap was taken for a
  name collision and created the task again. The cleanup failure is now
  an IllegalStateException (500), with the delete's error as its cause.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@uniqueg
uniqueg merged commit 59f242d into dev Nov 7, 2026
29 of 34 checks passed
@uniqueg
uniqueg deleted the fix/task-ttl branch November 7, 2026 21:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: follow the task TTL in tesk-api

1 participant