Skip to content

containers: add setLabels API to mutate container labels at runtime#6787

Open
martinezjandrew wants to merge 2 commits into
mainfrom
amartinez/set-container-labels
Open

containers: add setLabels API to mutate container labels at runtime#6787
martinezjandrew wants to merge 2 commits into
mainfrom
amartinez/set-container-labels

Conversation

@martinezjandrew
Copy link
Copy Markdown
Contributor

Customers keep containers in a per-customer "warm pool" and need to attach customer-identifying labels after the container has been claimed.

Today labels can only be set at start() time (#6352), so there's no way to label an already-running container without destroying and restarting it.

This PR adds a setLabels() runtime API to mutate labels on a running container:

  • Adds setLabels @15 to the Container capnp interface.
  • Forwards labels from JS to the capnp RPC in container.c++.
  • Tracks labels in-memory in the local Docker shim; inspect() returns the current set. A one-time WARNING is logged because Docker's HTTP API does not support runtime label mutation.
  • Extracts label validation into a shared helper used by both start() and setLabels().
  • Gates setLabels behindbehind workerdExperimental

Adds a new container.setLabels(labels: Record<string, string>): Promise<void>
JS method, gated behind the workerdExperimental compatibility flag, backed by
a new capnp RPC method setLabels @15 (labels :List(Label)) on the Container
interface. Each call fully replaces the existing label set; the container
must be running. Label-name and label-value validation is shared with start()
via a new requireValidLabels() helper.
Wires up workerd's local Docker container shim so container.setLabels() works
end-to-end against the developer-loop workerd binary. Docker's HTTP API does
not support runtime label mutation on an existing container, so the
ContainerClient keeps an authoritative in-memory currentLabels map that is
seeded from StartParams.labels in start() and replaced wholesale on each
setLabels() call. inspectContainer() returns this map overlaid on top of the
Docker-derived labels, so the JS API sees the latest set.
@martinezjandrew martinezjandrew requested review from a team as code owners May 28, 2026 03:41
Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

I'm Bonk, and I've done a quick review of your PR.

Adds a setLabels() runtime API to mutate container labels in-memory, with validation refactoring, capnp schema addition, and Docker shim support.

Issues found (ranked by severity):

  1. [MEDIUM] setLabelsWarningLogged is an instance member but the comment claims it's process-wide — it fires once per ContainerClient, not once per process.
  2. [LOW] testSetLabelsSerialized asserts { b: '2' } wins, relying on FIFO RPC-turn ordering — the comment's wording ("whichever call was awaited last") is misleading.

This review was generated by an AI assistant and may contain inaccuracies.


// Process-wide latch ensuring the "Docker does not propagate runtime label changes"
// warning is emitted at most once per workerd process, on the first setLabels() call.
std::atomic_bool setLabelsWarningLogged{false};
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.

setLabelsWarningLogged is a non-static instance member, so every ContainerClient gets its own copy. The comment says "process-wide latch" but it only gates per-instance.

The existing staleSnapshotVolumeCheckScheduled (which IS process-wide) is declared as a file-scope variable in the anonymous namespace in container-client.c++:67. If the intent is truly once-per-process, this should follow the same pattern:

Suggested change
std::atomic_bool setLabelsWarningLogged{false};
static inline std::atomic_bool setLabelsWarningLogged{false};

Alternatively, if once-per-container is acceptable, just update the comment to say "per-container" instead of "process-wide".


// Issue two concurrent setLabels calls without awaiting between them. The server-side
// RpcTurn mechanism serializes them, so the final inspect() must equal whichever call
// was awaited last.
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.

Nit: the comment says "the final inspect() must equal whichever call was awaited last" — but it's the last call queued (FIFO RPC-turn order) that wins, not the last awaited. Both are awaited simultaneously via Promise.all. Consider rewording to:

Suggested change
// was awaited last.
// Issue two concurrent setLabels calls without awaiting between them. The server-side
// RpcTurn mechanism serializes them in FIFO order, so the final inspect() must reflect
// the second (last-queued) call.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 28, 2026

Review posted on PR #6787 with two findings:

  1. [MEDIUM] setLabelsWarningLogged is declared as a non-static instance member of ContainerClient, but the comment says "process-wide latch." Each container client gets its own copy, so the warning fires once per container, not once per process. Suggestion provided to either make it static inline or update the comment.

  2. [LOW] The testSetLabelsSerialized test comment says "the final inspect() must equal whichever call was awaited last" which is misleading -- it's the last call queued (FIFO RPC-turn order) that wins. Suggestion provided with clearer wording.

The rest of the PR looks solid: the capnp schema addition (@15) is wire-compatible, the validation refactoring into requireValidLabels is clean, setLabels is properly gated behind workerdExperimental, the inspect() label merge logic correctly overlays in-memory labels over Docker-reported ones, and the test coverage is thorough (happy path, replace, clear, before-start, after-destroy, validation, serialization).

github run

@codecov-commenter
Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 63 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.59%. Comparing base (f56f736) to head (8469717).
⚠️ Report is 27 commits behind head on main.

Files with missing lines Patch % Lines
src/workerd/server/container-client.c++ 0.00% 36 Missing ⚠️
src/workerd/api/container.c++ 0.00% 27 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6787      +/-   ##
==========================================
- Coverage   66.62%   66.59%   -0.04%     
==========================================
  Files         402      402              
  Lines      115914   115967      +53     
  Branches    19425    19436      +11     
==========================================
- Hits        77231    77228       -3     
- Misses      27094    27148      +54     
- Partials    11589    11591       +2     

☔ View full report in Codecov by Sentry.
📢 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment on lines +2824 to +2827
const id = env.MY_CONTAINER.idFromName(
getRandomDurableObjectName('testSetLabels')
);
const stub = env.MY_CONTAINER.get(id);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: you can use getByName instead of idFromName + get

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.

3 participants