-
Notifications
You must be signed in to change notification settings - Fork 0
Post-PR8 follow-up: host lifecycle state enum, signing/verification tests, disconnect test realism #12
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
base: main
Are you sure you want to change the base?
Post-PR8 follow-up: host lifecycle state enum, signing/verification tests, disconnect test realism #12
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,6 +38,15 @@ const ( | |
| validationTimeout = 20 * time.Second | ||
| ) | ||
|
|
||
| // hostState represents the lifecycle state of the Host. | ||
| type hostState int | ||
|
|
||
| const ( | ||
| hostStopped hostState = iota // initial state; never started or fully shut down | ||
| hostRunning // host is up and processing messages | ||
| hostStopping // Stop has been called; shutdown is in progress | ||
| ) | ||
|
|
||
| // Host manages the P2P network functionality | ||
| type Host struct { | ||
| cfg *config.P2PConfig | ||
|
|
@@ -48,7 +57,7 @@ type Host struct { | |
| peerStore *PeerStore | ||
| validator *security.Validator | ||
| logger *zap.Logger | ||
| running bool | ||
| state hostState | ||
|
|
||
| // Channels for coordination | ||
| shutdown chan struct{} | ||
|
|
@@ -164,10 +173,16 @@ func NewHost(ctx context.Context, cfg *config.Config, logger *zap.Logger, repo d | |
| // Start begins P2P network operations | ||
| func (h *Host) Start(ctx context.Context) error { | ||
| h.mu.Lock() | ||
| if h.running { | ||
| if h.state != hostStopped { | ||
| h.mu.Unlock() | ||
| return fmt.Errorf("host is already running") | ||
| if h.state == hostRunning { | ||
| return fmt.Errorf("host is already running") | ||
| } | ||
| return fmt.Errorf("host is shutting down, cannot start") | ||
|
Comment on lines
175
to
+181
|
||
| } | ||
| // Mark as running while the lock is still held so that concurrent Start | ||
| // calls are rejected before any startup work begins. | ||
| h.state = hostRunning | ||
| h.mu.Unlock() | ||
|
|
||
| h.logger.Info("Starting P2P host", | ||
|
|
@@ -189,21 +204,20 @@ func (h *Host) Start(ctx context.Context) error { | |
| h.logger.Warn("Failed to connect to some bootstrap peers", zap.Error(err)) | ||
| } | ||
| h.status.UpdateStatus(true, false, nil) | ||
| h.mu.Lock() | ||
| h.running = true | ||
| h.mu.Unlock() | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // Stop gracefully shuts down the P2P host | ||
| func (h *Host) Stop() error { | ||
| h.mu.Lock() | ||
| if !h.running { | ||
| if h.state != hostRunning { | ||
| h.mu.Unlock() | ||
| return nil | ||
| } | ||
| h.running = false | ||
| // Transition to stopping so that IsRunning() immediately reflects the | ||
| // in-progress shutdown while work below is still executing. | ||
| h.state = hostStopping | ||
| h.mu.Unlock() | ||
|
|
||
| h.logger.Info("Stopping P2P host") | ||
|
|
@@ -230,9 +244,17 @@ func (h *Host) Stop() error { | |
| } | ||
| } | ||
| if err := h.host.Close(); err != nil { | ||
| // Even on error, record that the host is no longer running. | ||
| h.mu.Lock() | ||
| h.state = hostStopped | ||
| h.mu.Unlock() | ||
| return fmt.Errorf("failed to close libp2p host: %w", err) | ||
| } | ||
|
|
||
| h.mu.Lock() | ||
| h.state = hostStopped | ||
| h.mu.Unlock() | ||
|
|
||
| h.logger.Info("P2P host stopped") | ||
| return nil | ||
| } | ||
|
|
@@ -599,11 +621,13 @@ func (h *Host) StringToPeerID(peerIDStr string) (libp2pPeer.ID, error) { | |
| return libp2pPeer.Decode(peerIDStr) | ||
| } | ||
|
|
||
| // IsRunning returns the current running state of the host | ||
| // IsRunning returns true only when the host is fully started and not yet | ||
| // stopping. It returns false both before Start is called and once Stop has | ||
| // been called (even while shutdown is still in progress). | ||
| func (h *Host) IsRunning() bool { | ||
| h.mu.RLock() | ||
| defer h.mu.RUnlock() | ||
| return h.running | ||
| return h.state == hostRunning | ||
| } | ||
|
|
||
| func (h *Host) RequestData(ctx context.Context, peerID string, request data.DataRequest) error { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package host | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| // TestHostState_InitialStateIsNotRunning verifies that a zero-value Host | ||
| // reports IsRunning() == false. This guards against accidental initialisation | ||
| // of the state field to a truthy value. | ||
| func TestHostState_InitialStateIsNotRunning(t *testing.T) { | ||
| h := &Host{} | ||
| assert.False(t, h.IsRunning(), "a freshly-created host must not report itself as running") | ||
| } | ||
|
|
||
| // TestHostState_RunningStateIsDetected verifies that setting state to | ||
| // hostRunning causes IsRunning to return true. | ||
| func TestHostState_RunningStateIsDetected(t *testing.T) { | ||
| h := &Host{state: hostRunning} | ||
| assert.True(t, h.IsRunning(), "IsRunning must return true when state == hostRunning") | ||
| } | ||
|
|
||
| // TestHostState_StoppingIsNotRunning verifies that IsRunning returns false | ||
| // while the host is in the stopping state. This is the key semantic | ||
| // improvement: callers that observe IsRunning()==false cannot distinguish | ||
| // "never started" from "shutting down", but they correctly know the host is | ||
| // not accepting new work. | ||
| func TestHostState_StoppingIsNotRunning(t *testing.T) { | ||
| h := &Host{state: hostStopping} | ||
| assert.False(t, h.IsRunning(), "IsRunning must return false while the host is stopping") | ||
| } | ||
|
|
||
| // TestHostState_StoppedIsNotRunning verifies that IsRunning returns false | ||
| // after the host has fully stopped. | ||
| func TestHostState_StoppedIsNotRunning(t *testing.T) { | ||
| h := &Host{state: hostStopped} | ||
| assert.False(t, h.IsRunning(), "IsRunning must return false after the host has stopped") | ||
| } |
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.
In
Start, the non-stopped path unlocksh.muand then immediately readsh.stateagain to choose the error message. That second read is unsynchronized whileStopandStartboth writeh.stateunder the mutex, which introduces a real data race under concurrent start/stop calls and can also return the wrong branch if the state changes between unlock and read. Capture the state while still holding the lock (or keep the whole branch under lock) to avoid the race.Useful? React with 👍 / 👎.