February 5, 2025·4 min read·engineeringrqlitediscoveryraft

rqlite 8.36.11 keeps discovery separate from cluster membership

February's dependency update refreshes discovery code. The bootstrap implementation explains why finding an address is only the start of joining a cluster.

Finding another database node does not establish membership in its cluster. That separation is useful context for rqlite 8.36.11, published on 5 February. The release updates dependencies, including the discovery client, and includes a small adjustment in the join path. Neither change introduces a new membership protocol. They affect the code around the process that gets a node into an existing or newly formed cluster.

I like rqlite's relatively small operating model. Keeping discovery distinct from replicated membership helps explain that model without treating every address returned by infrastructure as an authoritative database member. The distinction also makes a routine dependency release worth reading at the level of its implementation.

What the dependency update actually contains

The versioned module file selects a discovery-client revision whose Go pseudo-version identifies a 5 February commit. The previous release's module file selected a December 2023 revision. Those dates describe the selected source revisions. They do not by themselves establish which discovery behavior changed or whether a particular deployment suffered a defect.

Other module changes include moving the declared Go version from 1.22.7 to 1.23 and the toolchain selection from go1.23.3 to go1.23.4. Protobuf moves from 1.36.2 to 1.36.4. The SQLite driver remains at 1.37.0 in both module files. Reading the dependency diff prevents attributing an unrelated storage-engine change to this patch.

The changelog labels the dependency work as including DNS discovery code. It does not describe a new DNS record format or announce a security vulnerability. Its heading gives 4 February, while the GitHub release is published on 5 February. This post uses the public release date, which also accommodates the selected discovery-client revision.

Discovery supplies candidates

The bootstrap implementation consumes an AddressProvider. That interface has one operation, Lookup, which returns addresses or an error. The bootstrapper does not require a provider to represent membership. It needs possible destinations for the next step in joining or forming a cluster.

On each polling cycle, the bootstrapper asks the provider for targets. It logs lookup failures and continues when no targets are available. This means a lookup result participates in a retrying startup process. An empty response is not a command to remove existing members, and the interface does not itself expose membership changes.

The bootstrapper tries an explicit join before attempting to notify peers about forming a new cluster. The source comment states that joining an existing cluster has priority. This ordering matters when the same discovery mechanism can reveal nodes that are already operating together and nodes that are still starting.

After an unsuccessful join, voting nodes can notify the discovered targets of their existence. The code describes how those notifications can let a node reach its bootstrap expectation and form a cluster. Non-voting nodes do not take that notification branch. Discovery therefore feeds different behavior depending on the role of the joining node.

Resolving an address is a separate check

PR #2039 changes the join method to discard the successful return value from resolvableAddress explicitly. The error check remains. The change is small, and its meaning is clearest beside the released join implementation.

The method first requires an open store and the leader role. It then checks whether the joining address resolves before consulting the Raft configuration. Its comment explains that incomplete infrastructure DNS records can make resolution fail. Rejecting that join attempt allows the joining node to retry instead of treating the attempt as successful.

The code subsequently compares the requested identity and address against the existing configuration. An exact match can be ignored as an already satisfied join. This is another reason a DNS answer cannot stand in for the membership decision. Membership handling considers the node's identity and the configuration that the leader already holds.

The changed assignment does not add that validation. The old code already called the resolver and handled its error. Describing this release as newly enforcing DNS resolution would therefore overstate the patch. The implementation adjustment makes the unused successful result explicit while preserving the resolution requirement.

Startup has an explicit completion condition

The bootstrapper also accepts a done function and an overall timeout. It checks completion during polling and handles context cancellation separately. The source defines distinct statuses for a successful join, externally observed completion, timeout, and cancellation. These outcomes describe the bootstrap operation, not just whether one DNS query succeeded.

My interest in this release is the visibility of those boundaries. A dependency update can refresh how candidates are found while leaving the higher-level sequence understandable in one implementation. The versioned source shows exactly where candidate lookup stops and joining begins. That gives operators a more precise explanation for a node that resolves names successfully but has not completed cluster startup.

For this patch, the supported claim remains modest. Discovery dependencies change, and the join path receives a small cleanup. The existing bootstrap process still obtains candidate addresses, prioritizes joining, and uses explicit completion and timeout conditions to decide when startup has finished.