Skip to content

fix(identity): make RecipientDataCache provisioning cancellable - #2137

Open
AkramBitar wants to merge 3 commits into
mainfrom
fix/2122-recipient-data-cache-goroutine
Open

fix(identity): make RecipientDataCache provisioning cancellable#2137
AkramBitar wants to merge 3 commits into
mainfrom
fix/2122-recipient-data-cache-goroutine

Conversation

@AkramBitar

@AkramBitar AkramBitar commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Both issues are about the same background worker, so they are fixed together in one PR.

Background. An anonymous wallet needs a fresh pseudonym for every payment. Making one is slow, so each wallet keeps a small stock of them ready. A background worker refills that stock. There is one worker per wallet. Idemix identities are cached the same way, by a second worker of the same shape.

#2122 — the worker cannot be stopped.
Once started, the worker runs until the process exits. There is no way to shut it down. When the stock is full, the worker waits to hand over the next pseudonym, and that wait never ends. So the worker stays alive forever, and it keeps its wallet, its stock, and everything they point to alive with it. Because wallets are created as they are used and never removed, the number of stuck workers grows with usage.
This PR gives the worker a proper shutdown path, so it can be told to stop and it actually exits.

#2080 — the same worker, plus three more problems.

  1. A broken backend is invisible. If pseudonym creation keeps failing, the worker retries immediately, forever. It writes nothing to the log and reports nothing to monitoring. The result is one CPU core at full load and no clue why. This PR makes it log the failure, count it on a new metric, and wait a second between attempts.
  2. A metric slowly becomes wrong. The counter that reports how full the stock is counts a pseudonym before it is really added. Over time it reports more than the stock actually holds. This PR counts it only once it is really added.
  3. The second cache has the same problem, and its cleanup is unreachable. The idemix cache already has a shutdown method, but the object holding it is thrown away as soon as it is created, so nothing can ever call that method. This PR keeps the object reachable and calls it during shutdown.

A defect this PR had to fix in itself. The idemix cache prepared its cancellation while serving the first request, and read it back while shutting down, with no synchronisation between the two. That was harmless while nothing could call its shutdown method. Making it reachable turned it into a real race, with a worse effect than the race itself: a shutdown that arrived at the wrong moment silently did nothing and left the worker running — the very thing this PR sets out to fix. It is now prepared up front, once, so the two can no longer overlap.

New metrics. recipient_data_provision_failures_total and cache_provision_failures_total, so a failing identity backend can be alerted on instead of only appearing in the logs. Both declare network/channel/namespace, and were checked against a real Prometheus provider behind the TMS wrapper, because a missing label there panics on first use.

What the two issues share. Only the first point: giving the worker a way to shut down. Everything else is unique to #2080.

What is still not solved. Wallets are never removed once created. So a worker is only released when the whole token service is unloaded, which happens on a public-parameter update. During normal operation nothing releases them. Fixing that means deciding when a wallet is no longer needed, which is a separate change.

Testing. 16 new tests. 8 of them were checked against the old code first and do fail there, so they really catch these bugs. Existing tests that used to leave workers running now release them. make checks, make lint, ./token/..., -race, and repeated shuffled runs are all clean. Docs updated in docs/services/identity.md, including a table of the four cache metrics.

Fixes #2122
Fixes #2080

@AkramBitar AkramBitar added this to the Q3/26 milestone Aug 4, 2026
@AkramBitar AkramBitar self-assigned this Aug 4, 2026
@AkramBitar
AkramBitar marked this pull request as draft August 4, 2026 12:34
@AkramBitar
AkramBitar marked this pull request as ready for review August 4, 2026 13:51
RecipientDataCache started provisionIdentities as a bare goroutine under a
sync.Once with a hardcoded context.Background(). It had no termination path: it
lived for the process lifetime, pinned the cache, its channel and the backend
closure, and blocked forever on the channel send once the cache filled up. Its
error path was a bare continue, so a persistently failing identity backend
turned it into a silent busy loop with nothing in the logs to explain it.

TMSProvider.Update unloads the previous token management service on every
public-parameter update, so each reload abandoned one such goroutine per
anonymous owner wallet.

The cache now builds a cancellable context in its constructor and exposes an
idempotent Close(). The goroutine selects on ctx.Done() around the channel send,
so cancellation releases it even when parked on a full cache, and it refuses to
start at all if the cache was closed before its first use. Backend failures are
logged and retried after a one second interruptible backoff instead of spinning,
and the cache level gauge is incremented only once an entry has actually been
handed over, so a cancelled send cannot skew it.

Close() is wired into the existing teardown chain: AnonymousOwnerWallet.Close()
forwards to the cache, and Registry.Done() closes every wallet it created that
holds releasable resources before dropping the wallet map. The assertion uses a
local closer interface rather than widening driver.Wallet, since only anonymous
owner wallets have anything to release.

Fixes #2122

Signed-off-by: AkramBitar <akram@il.ibm.com>

Signed-off-by: Effi-S <effi.szt@gmail.com>
KeyManagerProvider.Get built an IdentityCache and kept only its Identity method
value, discarding the cache object. The cache was therefore unreachable and its
Close() had no callers, so its background provisioning goroutine could never be
stopped — the very pattern the RecipientDataCache fix is modelled on was itself
inert.

WrappedKeyManager now retains the cache (nil for remote key managers, which do
not pre-provision) and exposes Close(). LocalMembership tracks the key managers
it loads and releases them in Close(), which is already reached by Role.Done().
Key managers are closed before the notifier teardown, since that block can return
early or fail and must not skip the release.

Addresses the remaining item of #2080.

Signed-off-by: AkramBitar <akram@il.ibm.com>

Signed-off-by: Effi-S <effi.szt@gmail.com>
…sioning failures

Two follow-ups found while re-checking this branch against #2122 and #2080.

The idemix identity cache assigned its cancel function inside sync.Once while
Close read it unsynchronised. That was harmless as long as Close had no callers,
but this branch gives it one, which makes it a live data race — confirmed by the
race detector — with a worse consequence than the race itself: a Close that
observed the pre-write nil value silently skipped the cancellation and leaked the
goroutine anyway, which is the exact failure this branch is meant to remove. The
context is now built in the constructor, so the field is write-once, and a cache
closed before its first use no longer starts a goroutine at all. Its cache level
gauge is also incremented only after a successful send, matching the other cache.

Both caches now count failed provisioning attempts. #2080 notes that the busy
loop had "no counter to signal the condition externally" and that there was
"nothing to alert on"; a log line alone does not close that gap, since nothing
pages on it. The new counters declare network/channel/namespace in LabelNames,
as the TMS-wrapped provider binds those on every metric and a mismatch panics on
first use.

Existing idemix tests now release the key managers and caches they create.
They were leaving provisioning goroutines running for the remainder of the test
binary, which is what made goroutine assertions order-dependent.

Signed-off-by: AkramBitar <akram@il.ibm.com>

Signed-off-by: Effi-S <effi.szt@gmail.com>
@Effi-S
Effi-S force-pushed the fix/2122-recipient-data-cache-goroutine branch from 5b3e6ab to 60a24f7 Compare August 7, 2026 14:51
@LFDT-Panurus LFDT-Panurus deleted a comment from github-actions Bot Aug 7, 2026
@LFDT-Panurus LFDT-Panurus deleted a comment from github-actions Bot Aug 7, 2026
@LFDT-Panurus LFDT-Panurus deleted a comment from github-actions Bot Aug 7, 2026

@Effi-S Effi-S left a comment

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.

Looks Good!

if c.cancel != nil {
c.cancel()
}
c.cancel()

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.

You sure about this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants