Skip to content

internal/db: Close incoming dqlite connections after a failed bootstrap or join - #856

Open
claudiubelu wants to merge 5 commits into
canonical:v4from
claudiubelu:v4-dqlite-accept-fail-fast
Open

claudiubelu wants to merge 5 commits into
canonical:v4from
claudiubelu:v4-dqlite-accept-fail-fast

Conversation

@claudiubelu

@claudiubelu claudiubelu commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

After a failed bootstrap or join, the daemon keeps serving its HTTPS endpoint without a dqlite app. Since #850, Accept holds every incoming dqlite connection for 10s before closing it. That makes sense during a Restart, but here no dqlite app will ever consume the connection.

If the failed joiner remains in the dqlite cluster (e.g.: its join failure cleanup was refused, see #774), the remaining members wait on it when probing for the dqlite leader:

  • The membership consistency check gives itself 5s and probes one member at a time (WithConcurrentLeaderConns(1)).
  • DialDqlite only applies the context to the TCP/TLS dial, not to the HTTP upgrade, so the probe of the failed joiner blocks until it closes the connection 10s later.
  • Whenever the failed joiner is probed first, token generation, joins and removals fail after ~10s with no available dqlite leader server found, and heartbeats are delayed by 10s.

This PR:

  • closes the accept channel without replacing it after a failed bootstrap or join, so Accept closes incoming connections right away. Restart and the daemon start-up still hold them as before.
  • aborts the HTTP upgrade in DialDqlite once the context is done.

Reproducer: bootstrap c1, then make c2 fail to join with an invalid patch.global.sql while c1 is still PENDING (without #774, the cleanup leaves c2 behind). Before this PR, every tokens add on c1 fails after 10.2s; with it, they succeed in ~40ms. Unit tests cover both changes; the DialDqlite test hangs without the fix.

Depends-On: #850

…tstrap or join

When Bootstrap or Join fails, the reverter closes the dqlite app but keeps
the accept channel open. The go-dqlite reader ranging over it keeps receiving
connections from the remaining cluster members and panics when it cannot dial
the closed app's local socket, crashing the daemon before the join failure
cleanup can run.

Rotate the accept channel before closing the dqlite app, like Restart already does.

Signed-off-by: Claudiu Belu <cbelu@cloudbasesolutions.com>
…ffline

On a failed join, the cluster member handling the cleanup resets the
joiner through PUT /cluster/{name}.

That endpoint required an open database, which a joiner whose dqlite
join failed never has, so the reset was refused and the joiner could
not be initialized again until its daemon was restarted.

Signed-off-by: Claudiu Belu <cbelu@cloudbasesolutions.com>
Signed-off-by: Claudiu Belu <cbelu@cloudbasesolutions.com>
…led bootstrap or join

After a failed bootstrap or join, the reverter closes the dqlite app and
leaves a fresh accept channel without a consumer. Accept then holds every
incoming dqlite connection for 10s before closing it, as it does during a
Restart, although no dqlite app will consume it.

If the failed joiner remains in the dqlite cluster, e.g. because its join
failure cleanup did not remove it, the remaining members wait on it while
probing for the dqlite leader. The membership consistency check probes one
member at a time within 5s, so it fails with "no available dqlite leader
server found" whenever the failed joiner is probed first, blocking token
generation, joins and removals. Heartbeats are delayed by 10s as well.

Close the accept channel without replacing it after a failed bootstrap or
join, so that Accept closes incoming connections right away, and create a
new one on the next Bootstrap or Join.

Signed-off-by: Claudiu Belu <cbelu@cloudbasesolutions.com>
DialDqlite only applies the context to the TCP and TLS dial. The HTTP
upgrade request that follows waits for the response without any limit, so a
remote member that never answers it, e.g. because it has no dqlite app,
blocks the dial past the dqlite leader probe and dial timeouts.

Set the connection deadline once the context is done, so that the upgrade
is aborted as well.

Signed-off-by: Claudiu Belu <cbelu@cloudbasesolutions.com>
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.

1 participant