Repository navigation
Conversation
A three-node bootstrap could leave follower clusterd units permanently blocked. The join token was consumed during admission, before the joining node had finished initialising, so a node whose join failed after admission could not retry: every retry returned "CoreTokenRecord not found", and the charm never regenerates a token it has already handed over. Consume the token exactly once, when the member's dqlite role leaves "pending" (i.e. the join actually succeeded). Admission keeps the token and reuses an existing pending member record, member removal/rollback leaves the token intact so the node can retry, and a token that is missing is rejected rather than re-issued. Assisted-by: openrouter/deepseek-pro-latest
|
Hey @hmlanigan have you tried using the latest LTS version
Can you please share the returned error message? It could be the leader wasn't yet ready/started when performing the join operation. There is the |
|
@roosterfish I have not tried v3 because we're at the last stages of the 2026.1 release and trying to get a release candidate. Trying to move to v3 at this point is too risky for our testing. As well, AI tells me that the problem exists in all version of the library. Working on the other questions asked. |
|
The logs I'm working off are from SolQA and hard to sort thru, sent DeepSeek AI after the answer:
From what I understand Ready() is used to see in the local cluster is ready. It has no indication that the remote cluster can be joined successfully. Please confirm. DeepSeek is also telling me that adding a Ready() equivalent might shrink the race window, but not close it. https://pastebin.canonical.com/p/sZ42vwT82M/ |
Yes If you cannot get the actual error from the reports, can you please try reproducing it? I would really like to understand the root cause first before we start thinking about a potential code change here for any of the stable LTS branches. |
|
@roosterfish how do we get token if the leader isn't ready? I can try to reproduce, it is a race, however we are not seeing error messages from the 500 message. What kind of error messages are you looking for, please be specific. I will also try to reproduce outside of sunbeam. |
That was just an idea. I don't know if your leader gets stopped/started in between.
The exact JSON response from the Microcluster API would be ideal including the error message. |
Fixing a bug seen in sunbeam: clusterd followers fail admission, then retries fail with CoreTokenRecord not found
Root cause
The cluster join is not retryable because the one-shot join token is consumed
too early and the rollback path consumes it again.
In microcluster, admission happens in
clusterPost(
internal/rest/resources/cluster.go), which runs on the leader. Itstransaction:
GetCoreTokenRecord),CoreClusterMemberrecord (rolepending),CoreTokenRecord— all before the follower has actuallyjoined the dqlite cluster.
Control then returns to the follower's
controlPost(
internal/rest/resources/control.go), which still has to runsetupLocalMemberandStartAPI→d.db.Join(the dqlite join andUpgrade: dqlitehandshake to peers on port 7000). When that fails — as inthis report, "database-upgrade request … port 7000: connection refused" — the
deferred
revertertears the member back out viaDeleteClusterMember(..., force=true)(which lands inclusterMemberDelete)and re-execs the daemon, leaving it "uninitialized", and
POST /core/controlreturns HTTP 500.
The key detail:
clusterMemberDeletealso runs on that rollback path. If itdeletes the join token (as an earlier iteration of the fix did), then after the
rollback both the member record and the token are gone. The charm's
join_node_to_clusterretries with the same token,GetCoreTokenRecordreturns HTTP 404 "CoreTokenRecord not found", and the node can never rejoin.
The charm does not regenerate the token (
add_node_to_clusterskips generationwhenever the
{unit}.join_tokenpeer-data key still exists), so the unitsblock permanently.
Investigation: consume only on success, not "tolerate consumed"
Two failure windows exist:
d.db.Joinfails before the member isever registered in dqlite, so its role never leaves
pending. The token mustsurvive so the follower can retry.
d.db.Joinsucceeds, the member gets a dqlite role andthe heartbeat confirms it; only then does the token get consumed. If the
follower fails after that point it has already joined, so a retry should be
rejected, not servo'd a fresh join.
Treating a missing token as "re-issue for the existing member" (the earlier
approach) is wrong:
too, so there is no existing member to re-issue against.
would incorrectly authorise a duplicate join of an already-joined member.
Conclusion: the token is the sole credential authorising a join. It should be
consumed exactly once — when the join succeeds — and a missing token should
always be rejected.
Fix
clusterPost/admitClusterMember) never deletes the token.A missing token is rejected outright — no fallback to an existing member.
(Member creation remains idempotent so a replayed admission whose member
record survived reuses it, and mismatched certificates/addresses are still
rejected.)
beginHeartbeat) deletes the token when the member's dqliterole transitions out of
pending— the single consumption point, i.e."consume only on successful join".
clusterMemberDelete) no longer deletes the token, soa failed join's rollback preserves the token and the follower can retry.
This is covered by unit tests in
internal/rest/resources/cluster_test.go:createsMemberAndKeepsTokenretryAfterRollback(the reported scenario)consumedTokenWithExistingMember(consumed token is rejected)consumedTokenWithoutMember(unknown token is rejected)existingMemberMismatch(mismatched cert / address is rejected)expiredToken,sanMismatchTradeoff
A member that is aborted while still
pending(e.g. operator scales downmid-join and it never confirms or retries) leaves its token record behind
(no expiry). This is harmless: the secret is no longer known to anyone and the
token is bound to a member name, and a confirmed-then-removed member has
already had its token deleted at confirmation.
The same defect exists in v2.2.0, v2, v3, and v4 of microcluster (all still
delete the token in the admission transaction with no retry path)
Assisted-by: openrouter/deepseek-pro-latest
bug fix: LP 2169835
OPEN-4867
Test run compiling the change into sunbeamd: https://pastebin.canonical.com/p/KMZNkcXfg9/