-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add bootstrap state machine #90
Conversation
routing/bootstrap.go
Outdated
|
||
if cfg.QueueCapacity < 1 { | ||
return &kaderr.ConfigurationError{ | ||
Component: "PoolConfig", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Component: "PoolConfig", | |
Component: "BootstrapConfig", |
coord/coordinator.go
Outdated
type StateMachine[S any, E event.Action] interface { | ||
// Enqueue enqueues an event to be processed by the state machine. | ||
Enqueue(context.Context, E) | ||
// Advance advances the state of the state machine. | ||
Advance(context.Context) S | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a StateMachine
interface makes sense. I also think it's great that we can constrain the type of events that we pass into the state machine with the generic type parameters!
I'm not sure about the part where we require each state to maintain its own queue of events it needs to process. I would have expected that this is the responsibility of another component. What drove the requirement to have a separate queue in each state?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I just read the PR description
|
||
bootstrap, err := routing.NewBootstrap(self, bootstrapCfg) | ||
if err != nil { | ||
return nil, fmt.Errorf("query pool: %w", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return nil, fmt.Errorf("query pool: %w", err) | |
return nil, fmt.Errorf("bootstrap: %w", err) |
coord/coordinator.go
Outdated
c.queue.Enqueue(ctx, ev) | ||
// c.inboundEvents <- ev | ||
|
||
c.pool.Enqueue(ctx, qev) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if the queue is at its capacity? Enqueue
is exported so anyone could put messages in the queue. If it was just internal we probably could make sure that this won't happen.
Besides in the coordinator, Enqueue
is only called in tests, so we could make it a private method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if the queue is at its capacity? Enqueue is exported so anyone could put messages in the queue. If it was just internal we probably could make sure that this won't happen.
The pool is only internal - the coordinator creates it - so noone else is adding events.
Besides in the coordinator, Enqueue is only called in tests, so we could make it a private method?
Do you mean pool.Enqeue? That's from synchronous methods exposed by the coordinator, not just tests.
pool StateMachine[query.PoolState, query.PoolEvent] | ||
|
||
// bootstrap is the bootstrap state machine, responsible for bootstrapping the routing table | ||
bootstrap StateMachine[routing.BootstrapState, routing.BootstrapEvent] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could define the StateMachine type in routing
, e.g.:
type BootstrapStateMachine coord.StateMachine[BootstrapState, BootstrapEvent]
(cyclic dependency as the packages are structured now though)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer to define interfaces where they are used. In this case the coordinator is using the StateMachine interface.
I think it should be possible to already start queries as soon as we have connected to the first peer. It'll probably not be the fastest query but it'll resolve and will be faster than waiting for the whole bootstrap process to finish.
IMO this should result in an error for the caller. So the coordinator would process that message normally, determine that it cannot start the query, and return an error to the caller. However, I see your point and that, in the general case, it might be necessary to delay events until a certain state is reached. I think having separate queues undermines the hierarchy of the hierarchical state machine. Sub-components can enqueue events with sub-states, by-passing the parent state machines that might want to react to or track these events as well. I think it's easier to reason about the control flow if data always flows from the root to the leaves and cannot skip any intermediate states. |
I don't understand this point. The coordinator is coordinating state machines and is translating asynchronous behaviour into synchronous. That async to sync interface needs to have some kind of queueing. Each hierarchical state machine is synchronous and there is no enqueing of substates, only the coordinator is feeding async events to the top of each state machine hierarchy. |
I discuss this a bit in the design issue #45 and expand in this comment. If we're only connected to a bootstrap node then queries will hit that bootstrap node far more than is usual and the bootstrap node has to deal with that for every node that starts up. We should at least wait until the routing table is better populated before allowing queries (or we could throttle them based on bootstrap progress). |
That only works in this specific case. A more general example is what happens if a non-bootstrap query response is received as an event while the bootstrap is waiting for its own query response? There is no place to return an error in that case. In general the coordinator will also be advancing other state machines like the routing table probe/refresh, exploring etc. These will essentially be background tasks and should be given the chance to progress and potentially change state, which needs to be returned by the coordinator. Any incoming event needs to be queued in that case. The rust-libp2p design handles this by calling methods to mutate state on receipt of an inbound event. So if a message response is received it calls the equivalent of onMessageReceived on pool, which looks up the query and calls onMessageReceived on the query which then calls onMessageReceived on the iterator which looks up the responding node and changes its state. |
@dennis-tra I moved the event queues out of the state machines and into the coordinator. After some thought I ended up agreeing with your point that this is the responsibility of the coordinator, and it allows state machines to be composed in a simple way |
Based on sm-bootstrap branch (#90) This adds a state machine for running the include process described in #45. The state machine manages a queue of candidates nodes and processes them by checking whether they respond to a find node request. Candidates that respond with one or more closer nodes are considered live and added to the routing table. Nodes that do not respond or do not provide any suggested closer nodes are dropped from the queue. The number of concurrent checks in flight is configurable. Not done yet: - [ ] check timeouts - [ ] removing nodes failing checks from routing table - [ ] notifying of unroutable nodes
This adds a state machine for running the bootstrap query described in #45. The state machine is very simple (it runs a query that attempts to find the self node), but it drives the design of the coordinator in a number of ways:
Advance
method. However this causes complications in the presence of multiple state machines. What should happen if the bootstrap is waiting for a message response but the caller attempts to start a user query? The coordinator needs to remember the "add query" event until the bootstrap is complete, so that event would need to remain on the coordinator's queue. But the coordinator needs to read from the queue to detect if an incoming message response is available for the bootstrap, without removing the "add query" event. Thus we need separate queues for the two state machines. Rather than manage those in the coordinator, we give each state machine its own event queue. External callers enqueue events and the state machine dequeues the next one each time it attempts to advance state.The above change leads to a simple interface for state machines: an Enqueue method for notifying a new event and an Advance method that returns the next state.There are still some ugly parts which I may be able to fix within the scope of this PR:
the name of the bootstrap query needs to be factored into a constant or remembered by the coordinator- coordinator now uses a separate callback to deal with bootstrap query instead of checking query idevents are action.Action interfaces so they can use the standard queue interface. The Run method is unused. The queue could simply be a channel or we could modify the queue interface to be parameterised by type, allowing us to have a queue of BootstrapEvents(removed 2023-08-15)Fixes #47