De-genericize HTTP handlers via T-free node modules; add AdminClient#29
Merged
Conversation
StatusHandler(T) and AdminHandler(T) were generic only because their field was typed Node(T) — every method they call is T-free. The genericity forced embedders to wrap them in concrete classes to dodge a Crystal codegen bug in stdlib handler-chain dispatch (generic instantiations missing from call_next's type-id case table; see LavinMQ's RaftHandlerWrapper for the diagnosis). - Raft::StatusSource / Raft::AdminOps: T-free abstract modules at node level (not under http/ — Node includes them, and core requiring http/ would invert the ARCHITECTURE.md layering). Handler fields are named for the capability (@source, @ops), not the implementor, since future non-node implementations (e.g. a channel-marshaling admin wrapper) can include the same modules. - Node(T) includes both, plus four new T-free log scalars (last_log_index, last_log_term, first_log_index, segment_count) so status consumers stop reaching through the mutable Node#log. - StatusHandler / AdminHandler drop (T). Behavior byte-for-byte identical: routes, JSON shapes, status codes, error messages, and the admin-route rescue all unchanged. - Raft::HTTP::AdminClient.add_server(uri, node_id, address): client side of the join wire format. Single attempt, no retry loop. Returns ::HTTP::Status (not Bool) so embedders' retry policies can distinguish rejection (400 — stop) from unavailability (5xx — retry). Basic auth from URI userinfo; path prefix respected. - Examples drop the explicit generic args. This is a typing change only — no change to who may call the node or from where. The single-fiber threading contract still applies and is restated on both modules. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
997630c to
5ffde82
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
`StatusHandler(T)` / `AdminHandler(T)` were generic only because their field was typed `Node(T)` — every method they call is T-free. The genericity forced LavinMQ to wrap them in a concrete class (`RaftHandlerWrapper`) to dodge a Crystal codegen bug: stdlib `HTTP::Handler#call_next` builds a type-id case table over concrete includers; generic instantiations are missing from it, so a generic handler in a chain traps with `brk #1`.
Changes
What this is NOT
A typing change only — no change to who may call the node or from where. The single-fiber threading contract is restated on both modules. The thread-safety marshaling discussion (AsyncAdmin) is deliberately out of scope; an `AsyncAdmin` would `include AdminOps`, so nothing here forecloses it.
LavinMQ after this
`RaftHandlerWrapper` can be deleted — handlers are concrete and chain-safe directly. The join flow can use `AdminClient.add_server` instead of hand-rolled HTTP.
Test plan
🤖 Generated with Claude Code