Skip to content

Make worker identity an advertised, per-worker property — Closes #356 - #375

Draft
conradbzura wants to merge 9 commits into
wool-labs:mainfrom
conradbzura:356-rename-identity-to-peers
Draft

Make worker identity an advertised, per-worker property — Closes #356#375
conradbzura wants to merge 9 commits into
wool-labs:mainfrom
conradbzura:356-rename-identity-to-peers

Conversation

@conradbzura

@conradbzura conradbzura commented Aug 15, 2026

Copy link
Copy Markdown
Member

Summary

A worker was verified against a logical name the client configured: one expectation for a whole fleet. No worker declared anything about itself, so two workers signed by one authority were indistinguishable, and a client could not say which of them it meant to reach.

A worker now carries an identity — a logical name for the workload it is, independent of where it was scheduled — advertised through discovery alongside its address. A client declares which names it accepts, admits a worker only when that policy covers the identity the worker advertised, and verifies that name at the handshake. Which name a connection proves is chosen per worker, so connections pinned to different peers never share a pooled channel.

Admission has two states. With no accepted names configured, advertisements are ignored entirely and a connection verifies against the material's own peer or the dialed address. With accepted names configured, a worker is admitted only if it advertises one of them, and a worker advertising nothing is rejected whatever the shape of the policy. Cardinality does not enter into it: an accept-list of one behaves exactly as an accept-list of two, and exactly as a predicate accepting only that one name.

Freeing identity for this meaning requires renaming what it meant before, so the credential surface breaks. identity= becomes peers= on WorkerCredentialsProvider and WorkerCredentials.as_provider, WorkerCredentials.identity becomes peer, and identity_channel_options() becomes peer_channel_options(). There is no deprecation shim — removed keywords fail with TypeError. The break is deliberate while #249 adoption is young: a caller passing identity="wool-worker.svc" as an expectation must never have it quietly become an advertised self-identity.

The worker factory surface breaks too, in three ways. A factory pre-supplying host or identity through functools.partial no longer keeps that value — the pool's wins — so a factory that must own its binding has to decline the keyword rather than pre-supply it. A **kwargs-forwarding factory now receives the bind host it was previously denied. And isinstance against the factory protocols raises TypeError, since they are no longer runtime_checkable.

The advertisement selects which name is verified and never widens what is accepted. It is applied only where a policy exists to have accepted it, so a worker's own claim cannot displace a name the caller configured. A worker claiming a name it holds no certificate for is admitted by the gate and then rejected by the handshake, which is pinned by test. Security therefore does not rest on the discovery plane being trustworthy; only availability does, since forged advertisements cost connection attempts that go nowhere.

Two limits are worth stating up front. Verification is outbound only — who a client will dial. A worker still serves any caller its authority signed, and refusing callers by their identity is #251. And nothing checks that a declared identity appears in the worker's own certificate, because wool does not parse certificates; a mismatch surfaces at the first handshake as a diagnosable failure.

Upgrading a fleet that does not yet advertise runs workers-first: a client predating this field ignores the advertisement, so roll the workers out with an identity, then configure the accepted names on the clients.

Closes #356

Proposed changes

Rename the credential surface

WorkerCredentials.identity becomes peer, normalized in __post_init__; identity_channel_options() becomes peer_channel_options(); as_provider(identity=) and WorkerCredentialsProvider(identity=) become peers=, with the provider's identity property becoming peers. The pairing carries the semantics: peers names the acceptance policy, peer names the one artifact a snapshot verifies against and rides the channel-pool key. pin was considered for the material field and rejected, since canonical TLS pinning means certificate or public-key pinning.

Accept several peer names or a predicate

Compile peers into an internal policy admitting a single name, an iterable of names, or a predicate over a candidate name. A blank name and an empty iterable both mean "not configured", like None, rather than a policy accepting nothing that would reject every peer and read as a silent outage. The shape follows go-spiffe's Authorizer, which covers the same three cases; the predicate form is the seam that makes the pattern acceptance #355 needs additive rather than a further widening of this parameter.

The provider answers the acceptance question rather than publishing the compiled policy: it reports whether a candidate name is accepted and describes the accepted names for a diagnostic. That leaves one home for the decision instead of a value each caller must interpret. A provider exposes the whole surface or none of it, where none opts out of identity gating, and coerce rejects one offering only part of it so a half-implemented contract fails at configuration rather than from inside a proxy.

Advertise a worker's identity

Add identity to the worker metadata and to protobuf field 9, threaded from LocalWorker and WorkerPool through WorkerProcess. The LAN backend gains a TXT key that tolerates its own absence, so records published by workers predating the field still parse; the local backend rides the protobuf converters unchanged. WorkerMetadata normalizes the field on construction, so every path that produces one — the wire, a discovery backend, a caller building metadata directly — collapses a blank name to None the same way. A worker's identity requires credentials, since a name with no certificate behind it proves nothing.

Verify each worker against what it advertised

Add an identity arm to the proxy's admission gate, reported after security and version. It applies the configured policy to the advertised name and never treats the advertisement as authority. WorkerConnection gains a peer argument applied during pool-key resolution, so the chosen name reaches grpc.ssl_target_name_override per worker; the proxy supplies it only where a policy exists to have accepted it. A worker's own stop RPC pins its identity for the same reason: a policy naming several peers stamps nothing on the material, and a certificate carrying only a logical name cannot be verified against an address.

Enumerate the worker factory shapes

The pool passes a factory two optional keywords: the bind host prescribed by its discovery publisher, and the identity its workers should advertise. Whether a factory receives either is a property of that factory, so the surface is four shapes along two orthogonal axes — BoundWorkerFactory, WorkerFactory, IdentifiedBoundWorkerFactory, and IdentifiedWorkerFactory — joined by a public WorkerFactoryLike alias. Whichever keyword a factory does not take, it owns: its own binding, or the name its workers advertise.

Enumeration is forced rather than chosen. Composing "accepts host" and "accepts identity" as two constraints on one callable needs a type intersection, which Python does not have. Protocol inheritance resolves __call__ by MRO and silently keeps only the first base, so a factory taking only a host satisfies a protocol whose name claims both — no error, just a weaker type. An overloaded __call__ does produce a genuine intersection, but overloads are a union for the caller and an intersection for the implementer, so a four-overload protocol rejects every legacy shape. WorkerFactoryLike is the only name callers need, so the shapes behind it can be replaced when the language grows an intersection.

identity is required in the two identified arms. Conforming to one is the factory's statement that it wants the value, so the pool always sends one, and None means the pool has no name to give. runtime_checkable comes off all four, because for a protocol whose only member is __call__ an isinstance check tests nothing but callability and admits any callable at all.

Classify factories by binding the call

Replace the keyword classifier with accepts_kwarg, requires_kwarg, and unbindable_call, which decide by binding the call the pool will actually make rather than by inspecting parameter kinds. No kind-keyed rule is correct: a positional-or-keyword parameter collides with a forwarded positional at one argument count and binds cleanly at another, so its classification is a property of the call.

accepts_kwarg binds partially, which is what keeps the question about one keyword. A complete bind fails whenever any other required parameter is unsatisfied, so probing one keyword would report on another, and a factory declaring both host and identity without defaults would be found to accept neither. requires_kwarg inspects the parameter instead of attempting a call, because a failed bind does not say which argument it wanted; reading any TypeError as evidence about the queried name reports a factory requiring some unrelated keyword as requiring this one. unbindable_call answers what the per-keyword predicates deliberately do not — whether the call is satisfiable at all — so the pool refuses a factory it cannot call and names the argument at fault.

Two behaviours change as a consequence. A **kwargs-forwarding factory now receives the publisher's bind host, where before it was classified bound and LocalWorker fell back to loopback, leaving the worker unreachable at the address a LAN pool advertised for it. And a pre-supplying functools.partial no longer suppresses the keyword it binds, since that call succeeds and the call-site value overrides.

Diagnose only what a signature proves

The pool's identity has three states, distinguished because an explicit None is a value and an unset parameter is not. Unset withholds the keyword entirely and the factory owns the name its workers advertise. A str or an explicit None is delivered to any factory that can receive it, and withheld with IneffectiveIdentityWarning when it cannot.

Passing the value is all the pool guarantees. Whether a factory that accepts an identity then honors it is not a question a signature answers, so the pool asserts nothing about it. The self-refusal check — which raises for an ephemeral pool whose own peers policy would refuse everything it starts, and warns for a hybrid one — therefore runs only when the pool owns the default factory and can predict what its workers advertise. With a custom factory that prediction is unfounded, and the cost is a startup timeout in place of a construction error, documented on the parameter so an operator knows to suspect it.

Three diagnostics survive, each decidable from a signature: a configured identity the factory cannot receive, a factory requiring an identity the pool has none to give, and a factory the pool cannot call at all. An identity set without credentials still raises at construction rather than from inside a spawned subprocess.

Pin the upstream limitation blocking SPIFFE

tests/integration/test_workload_identity.py pins why #355 cannot proceed: gRPC Python's client-side name check never consults URI SANs, so a SPIFFE identity cannot be verified at the handshake. The success path is a strict xfail constrained to the drain it documents, so an unrelated regression surfaces as a real failure rather than a green expected one. A control test dials a worker whose certificate carries no URI SAN at all, which distinguishes a future unexpected pass meaning "URI-SAN matching landed" from one meaning "the override was silently dropped".

Extend the certificate test helpers

Add generate_authority, which builds a certificate authority a caller can then hand to generate_ca_and_leaf, so several leaves chain to one trust bundle while carrying distinct names. Without it no test can tell two workers apart by identity rather than by authority, which is exactly the distinction this PR turns on. The self-signed and issued paths now share one code path, so the two cannot drift into differently shaped certificates.

Test cases

# Test Suite Given When Then Coverage Target
1 TestWorkerCredentials Credential material and any peer name — None, blank, or padded WorkerCredentials is instantiated It should carry the stripped peer, with blank or None collapsing to None Peer normalization
2 TestWorkerCredentials A collection supplied as the singular peer WorkerCredentials is instantiated It should raise TypeError naming where several names belong Typed rejection
3 TestWorkerCredentials Material with and without a peer name peer_channel_options() is called It should return the override option only when a name is configured Override assembly
4 TestWorkerCredentialsProvider A single name, a set, a predicate, blank input, or an unsupported shape A provider is constructed It should compile each into the policy, treat blank and empty as unconfigured, and reject the rest Policy compilation
5 TestWorkerCredentialsProvider Any list of candidate names, including blank and padded ones A provider is constructed The compiled policy should be exactly the stripped non-blank names, collapsing to None when none survive Normalization invariant
6 TestWorkerCredentialsProvider Any list of candidate names The provider resolves its credentials The material should carry a peer name only when exactly one name survives Stamp invariant
7 TestWorkerCredentialsProvider A provider whose peers is a locally defined predicate The provider is pickled and restored The restored policy should accept and reject exactly as the original did Spawn-boundary transport
8 TestWorkerCredentialsProvider A duck-typed provider exposing only part of the peer-gate surface It is coerced It should raise TypeError naming what is missing Contract completeness
9 TestAcceptsKwarg Callables declaring a keyword-only parameter, positionally, through **kwargs, or not at all, and the same positional parameter under differing argument counts The predicate is evaluated It should answer whether the call binds, so a **kwargs sink opts in and a positional declaration's answer differs with the count Deliverability by call shape
10 TestAcceptsKwarg A callable declaring two required keywords and supplying neither Each name is asked about in turn It should accept both, so neither required parameter masks the other Probe isolation
11 TestRequiresKwarg A callable requiring a keyword it is not asked about, and one whose parameter the caller already supplies The predicate is evaluated It should be False for both, since neither is a statement about the queried name Mandatory-keyword attribution
12 TestUnbindableCall A satisfiable call, one missing a required argument, one passing an unexpected keyword, and an uninspectable builtin The whole call is checked It should report nothing for the first and last, and name the argument at fault for the others Whole-call diagnosis
13 TestWorkerProxyIdentityAdmission A policy that is unconfigured, names one peer, names several, or is a predicate, against a worker advertising a name it accepts The proxy starts It should admit the worker Admission
14 TestWorkerProxyIdentityAdmission A configured policy against a worker advertising a mismatched name or nothing at all A proxy is constructed over that worker alone It should raise ValueError, since the quorum can never be satisfied Rejection and monotonicity
15 TestWorkerProxyIdentityAdmission A provider exposing none of the peer-gate surface The proxy starts over a worker advertising an identity It should admit the worker, the documented opt-out from identity gating Duck-typed opt-out
16 TestLanDiscoveryPublisher Metadata declaring an identity, and metadata declaring none The worker is published The record should carry the identity, or a valueless key when none is declared Advertisement encoding
17 TestLanDiscoverySubscriber A raw record carrying no identity property, as published by a worker predating the field The subscriber iterates events It should yield that worker with no identity rather than rejecting the record Wire back-compat
18 TestLanDiscoverySubscriber A worker declaring an identity, and arbitrary metadata under Hypothesis The worker is published and discovered The received metadata should carry the identity, so the two halves of the encoding cannot drift Round trip
19 TestWorkerConnection A connection pinned to a peer name, over a provider that configures none or stamps a different one A task is dispatched The channel should verify against the connection's name, and against nothing when the name is blank Per-connection pin
20 TestWorkerConnection Two connections to one target pinned to different names, and two pinned to the same name A task is dispatched through each Differing names should build separate channels and matching names should share one Pool-key separation
21 TestLocalWorker A worker declaring an identity, with and without credentials The worker is constructed It should normalize the declared name, and raise ValueError when no credentials back it Declaration
22 TestLocalWorker A running worker declaring an identity, over a provider accepting several peers stop() is called The stop channel should verify against the worker's own identity, and against the address when none is declared Stop-RPC pin
23 TestWorkerPool A factory accepting identity, absorbing keywords, pre-supplying its own, or unable to receive one The pool is entered It should deliver the identity wherever the call binds, overriding a pre-supplied value Pass-down
24 TestWorkerPool A factory whose signature is exactly what IdentifiedWorkerFactory declares, with host and identity both required The pool is constructed and entered It should warn about neither keyword and deliver both Canonical shape
25 TestWorkerPool A factory accepting identity and declaring no host The pool is entered It should deliver the identity and no host Bound-and-identified dispatch
26 TestWorkerPool A pool with no identity, an explicit None, or a name, against factories that can and cannot receive one The pool is constructed and entered It should withhold the keyword when unset, deliver it when configured, and warn when a configured value cannot be delivered Three-state identity
27 TestWorkerPool A factory requiring a keyword this pool never passes The pool is constructed It should raise naming that keyword rather than identity Whole-call refusal
28 TestWorkerPool A pool whose own peers policy would refuse the workers it spawns, over the default factory The pool is constructed It should raise in the ephemeral mode and warn in the hybrid one Self-refusal diagnostic
29 TestWorkerPool The same refusing policy, over any custom factory The pool is constructed It should construct, since the pool cannot predict what a factory it does not own will advertise Prediction boundary
30 test_worker_identity (integration) A worker claiming an identity a client accepts among several, or by predicate A routine is dispatched It should verify against the advertised identity rather than the address and succeed Per-worker verification
31 test_worker_identity (integration) A worker chaining to the shared authority but claiming an unaccepted identity A proxy is constructed over it It should raise, since a shared certificate authority is never sufficient on its own Authority is insufficient
32 test_worker_identity (integration) A worker whose advertisement claims an accepted identity its certificate does not carry A routine is dispatched The gate should admit it and the handshake should reject it, draining with a handshake warning Forged advertisement
33 test_identity_mtls (integration) Workers with logical-name certificates, mismatched names, and rotated material Routines are dispatched Matching dispatches succeed and mismatches drain with handshake warnings #249 regression pin
34 test_workload_identity (integration) A worker whose certificate carries a SPIFFE URI SAN, and one carrying loopback names but no URI SAN Dispatch is attempted against a SPIFFE identity The strict xfail should fail today and fail loudly when gRPC gains URI-SAN verification Upstream limitation pin
35 test_helpers A leaf issued with no authority supplied, and one issued under an explicit authority The issuer of each is read It should be identical, so the two paths cannot drift into differently shaped authorities Helper parity

@conradbzura conradbzura self-assigned this Aug 15, 2026
@conradbzura
conradbzura force-pushed the 356-rename-identity-to-peers branch from ccd3360 to fd8f9ef Compare August 16, 2026 16:07
@conradbzura conradbzura changed the title Rename the credential identity parameter to peers — Closes #356 Make worker identity an advertised, per-worker property — Closes #356 Aug 16, 2026
@conradbzura
conradbzura force-pushed the 356-rename-identity-to-peers branch 3 times, most recently from 774a0aa to 2bcbfc5 Compare August 17, 2026 21:24
The identity name on the credential surface described an expectation
about peers, not an identity of self: the logical certificate name a
client verifies dialed workers against. Rename it so the vocabulary
matches the model and the identity name is freed for its natural
future meaning, a worker's own workload identity, without silently
flipping semantics for existing callers.

The provider level takes peers, the acceptance policy; the material
level takes peer, the one name a given credential snapshot verifies
its dialed worker against. The singular of the plural carries the
relationship exactly: the policy resolves to the name. The term pin
was considered and rejected because canonical TLS pinning means
certificate or public-key pinning, which this is not. The plural
names a policy that admits exactly one name today, so a collection
is refused with TypeError rather than failing as an AttributeError
inside string normalization; widening it to a set is issue 251.

Verification behavior is otherwise unchanged: value equality,
channel-pool keying, blank-normalizes-to-None,
provider-overrides-material precedence, and the
ssl_target_name_override mechanics are all preserved under the new
names. Removed keywords fail loudly with TypeError.

BREAKING CHANGE: WorkerCredentialsProvider and
WorkerCredentials.as_provider accept peers= instead of identity=,
and the provider's identity property is now peers.
WorkerCredentials.identity is renamed to peer, and
identity_channel_options() to peer_channel_options().
gRPC Python cannot verify a SPIFFE URI SAN at the client-side TLS
handshake, for two independent reasons: HostNameCertificateVerifier
consults DNS SANs, IP SANs, and the CN only when no DNS SAN is
present, and a URI-form target name is mangled by host-port
splitting besides. This blocks the client half of peer verification
by workload identity (issue 251) until upstream gRPC exposes
URI-SAN or custom verification to Python.

Pin the limitation the way the TLS 1.3 client-rejection blind spot
is pinned: the success-path test is a strict xfail constrained to
the handshake-drain failure it documents, so an unrelated
regression surfaces as a real failure rather than a green expected
one, and the day a grpcio upgrade lifts the limitation the suite
fails loudly and the blocked work can resume.

Two companion tests keep that signal readable. The rejection-path
test pins what holds today, which is narrower than its arrangement
suggests: no URI SAN is matched at all, so a SPIFFE pin is refused
whether or not it matches, and trust in the bundle alone is never
sufficient. The control test dials a worker whose certificate
carries no URI SAN, proving the pin is applied and unmatched rather
than discarded in favor of the address. Without it, a future gRPC
that drops an unparsable target-name override would make the strict
pin pass and be misread as upstream support.

Move the shared started_worker fixture into the integration conftest
rather than copy it into the new module, and note in the sibling
mTLS suite that identity there means the credential surface's peers
and peer, not the advertised worker identity issue 251 introduces.
@conradbzura
conradbzura force-pushed the 356-rename-identity-to-peers branch 3 times, most recently from f2d60ba to 4d2201f Compare August 19, 2026 01:36
The pool decides whether to pass a factory the bind host by inspecting
its signature for an explicitly declared, keyword-only parameter. That
rule is not specific to the host: it is how the pool passes any
optional value down without handing a third-party factory a keyword it
never asked for.

Generalize the classifier over the parameter name, so a second such
value reuses the rule rather than copying it, and call it directly for
the host rather than through a host-specific wrapper that would leave
two names for one question. Behavior is unchanged.
A client could name exactly one peer it would accept from the workers
it dials, which cannot express a pool running more than one kind of
workload under a single certificate authority.

Compile peers into an acceptance policy that admits a single name, an
iterable of names, or a predicate over a candidate name. A blank name
and an empty iterable both mean "not configured", like None, rather
than a policy accepting nothing, which would reject every peer and
read as a silent outage.

The policy is modelled on go-spiffe's Authorizer, which covers the
same three shapes for the same reason. The predicate form is the seam
that makes pattern acceptance additive later rather than a further
widening of this parameter.

A policy naming exactly one peer is still stamped onto the material as
its peer, so single-name verification behaves exactly as before. A
policy naming several, or a predicate, stamps nothing: which of them a
given connection verifies against cannot be known until there is a
worker to verify.

The provider answers the acceptance question itself rather than
publishing the compiled policy: it reports whether a name is accepted
and describes the accepted names for a diagnostic. That keeps one home
for the decision instead of a value each caller must interpret. A
provider may expose the whole surface or none of it, where none opts
out of identity gating entirely, and coercion rejects one offering
only part of it so a half-implemented contract fails at configuration
rather than from inside a proxy.

BREAKING CHANGE: WorkerCredentialsProvider.peers returns the accepted
names as a frozenset, or the predicate, rather than a single string.
A worker was verified against a logical name the client configured:
one expectation for a whole fleet. No worker declared anything about
itself, so two workers signed by one authority were indistinguishable
and a client could not say which of them it meant to reach.

Give a worker an identity, a logical name for the workload it is,
carried in its certificate and advertised through discovery alongside
its address. A client admits a worker only when its accepted names
cover the identity that worker advertised, then verifies that name
rather than a fleet-wide one at the handshake. Which name a connection
proves is therefore chosen per worker, and connections pinned to
different peers never share a pooled channel.

Admission has two states and no others. With no accepted names
configured, advertisements are ignored entirely and a connection
verifies against the material's own peer or the dialed address. With
accepted names configured, a worker is admitted only if it advertises
one of them, and one advertising nothing is rejected whatever the
shape of the policy. Deriving that second question from how many names
happened to be accepted would make widening an accept list narrow what
is admitted, and make a single name disagree with a predicate
accepting only that name.

The advertisement selects which name is verified; it never widens what
is accepted. It is applied only where a policy exists to have accepted
it, so a worker's own claim can never displace a name the caller
configured. A worker claiming a name it holds no certificate for is
admitted by the gate and then rejected by the handshake, which is
pinned by test. Security therefore does not rest on the discovery
plane being trustworthy, and wool's built-in backends are not
authenticated. Only availability does: forged advertisements cost
connection attempts that go nowhere.

A pool that both spawns workers and dispatches through them hands one
provider to both roles, so it can be configured to refuse everything
it starts. That fails at construction where no worker could ever be
admitted, and warns where a pool may legitimately contribute capacity
it does not itself dial. An identity set without credentials fails at
construction too, rather than from inside a spawned subprocess.

Nothing checks that a declared identity appears in the worker's own
certificate, because wool does not parse certificates. A mismatch
surfaces at the first handshake as a diagnosable failure carrying
whatever the TLS stack reported, including for a worker's own stop
RPC.

Verification is outbound only. A worker still serves any caller its
authority signed.

BREAKING CHANGE: A client that configures accepted peer names no
longer dials a worker advertising no identity. To upgrade a fleet that
does not yet advertise, roll the workers out with an identity first —
a client predating this field ignores the advertisement — and then
configure the accepted names on the clients.
Cover the identity a worker claims and advertises, the shapes a client
may accept it in, and the property the design rests on: an
advertisement selects which name gets verified and never widens what
is accepted, so a worker cannot claim its way into a pool. State the
two limits plainly as well — verification is outbound only, and a
declared identity is not checked against the worker's own certificate.

Add a decision chart to the pool-modes section resolving the
constructor's non-deprecated overloads: which mode a call lands in,
what that then requires of the worker factory, and where the
bind-host question fits. The chart marks that last question as
orthogonal, since it is answered by signature inspection rather than
by whichever overload matched.
Four of these named subjects that do not exist. test___enter___ and
test_current asserted that WorkerCredentials is not a context manager
and has no current method, neither of which anyone proposed; they pin
Python's own defaults rather than this package's behaviour. The
_eager_provider helper was referenced nowhere. And one concurrency test
set a private _window attribute that no longer exists anywhere in the
source, so the line created a fresh attribute nothing reads and the
test stopped arranging what its docstring describes — it now sets the
debounce interval through the public fresh_for parameter instead.

Three more asserted only isinstance on server and client credentials,
across one mutual flag value or on repeated access. The property-based
test over the mutual domain already covers repeated access for both
methods, both types, and their consistency, so the three were strictly
subsumed.
Every test in TestLanDiscoverySubscriber drives real zeroconf: live
mDNS service registration, real multicast discovery, and wall-clock
waits of up to two seconds. Running them under the default selection
meant the unit suite took a network dependency and paid tens of seconds
for it, and a failure there could as easily mean a hostile network as a
regression.

They still run, behind the marker that already separates the rest of
the cross-boundary suite.
@conradbzura
conradbzura force-pushed the 356-rename-identity-to-peers branch from 547bc35 to f905595 Compare August 20, 2026 00:23
The rotation helper called refresh once and asserted the material had
changed. Refreshing.refresh joins a refresh already in flight rather
than starting a second, and the zero freshness interval these tests
configure means the read preceding the rotation left one in flight.
That flight was derived from the pre-rotation files, so joining it
returns exactly the value the caller is waiting to see replaced.

Repeat the refresh until the re-read lands. No sleep is needed: each
call either joins the stale flight or starts a fresh one.

Surfaced as a CI failure on one Python version against a tree that had
passed on the same content hours earlier.
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.

Make worker identity an advertised, per-worker property

1 participant