Skip to content

Commit 0f4013e

Browse files
Merge pull request #6769 from onflow/janez/expose-node-component-management
Expose node component management
2 parents a7656ea + e6fb7ad commit 0f4013e

File tree

15 files changed

+206
-103
lines changed

15 files changed

+206
-103
lines changed

cmd/access/main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package main
22

33
import (
4+
"context"
5+
46
"github.com/onflow/flow-go/cmd"
57
nodebuilder "github.com/onflow/flow-go/cmd/access/node_builder"
68
"github.com/onflow/flow-go/model/flow"
@@ -24,5 +26,5 @@ func main() {
2426
if err != nil {
2527
builder.Logger.Fatal().Err(err).Send()
2628
}
27-
node.Run()
29+
node.Run(context.Background())
2830
}

cmd/collection/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"context"
45
"fmt"
56
"time"
67

@@ -646,7 +647,7 @@ func main() {
646647
if err != nil {
647648
nodeBuilder.Logger.Fatal().Err(err).Send()
648649
}
649-
node.Run()
650+
node.Run(context.Background())
650651
}
651652

652653
// createQCContractClient creates QC contract client

cmd/consensus/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"context"
45
"encoding/json"
56
"errors"
67
"fmt"
@@ -933,7 +934,7 @@ func main() {
933934
if err != nil {
934935
nodeBuilder.Logger.Fatal().Err(err).Send()
935936
}
936-
node.Run()
937+
node.Run(context.Background())
937938
}
938939

939940
func loadBeaconPrivateKey(dir string, myID flow.Identifier) (*encodable.RandomBeaconPrivKey, error) {

cmd/execution/main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package main
22

33
import (
4+
"context"
5+
46
"github.com/onflow/flow-go/cmd"
57
"github.com/onflow/flow-go/model/flow"
68
)
@@ -19,5 +21,5 @@ func main() {
1921
if err != nil {
2022
exeBuilder.FlowNodeBuilder.Logger.Fatal().Err(err).Send()
2123
}
22-
node.Run()
24+
node.Run(context.Background())
2325
}

cmd/ghost/main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package main
22

33
import (
4+
"context"
5+
46
"github.com/spf13/pflag"
57

68
"github.com/onflow/flow-go/cmd"
@@ -45,5 +47,5 @@ func main() {
4547
if err != nil {
4648
nodeBuilder.Logger.Fatal().Err(err).Send()
4749
}
48-
node.Run()
50+
node.Run(context.Background())
4951
}

cmd/node.go

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,61 @@ type Node interface {
2222
// Run initiates all common components (logger, database, protocol state etc.)
2323
// then starts each component. It also sets up a channel to gracefully shut
2424
// down each component if a SIGINT is received.
25-
Run()
25+
// The context can also be used to signal the node to shutdown.
26+
Run(ctx context.Context)
2627
}
2728

2829
// FlowNodeImp is created by the FlowNodeBuilder with all components ready to be started.
2930
// The Run function starts all the components, and is blocked until either a termination
3031
// signal is received or a irrecoverable error is encountered.
3132
type FlowNodeImp struct {
32-
component.Component
33+
NodeImp
3334
*NodeConfig
35+
}
36+
37+
// NodeImp can be used to create a node instance from:
38+
// - a logger: to be used during startup and shutdown
39+
// - a component: that will be started with Run
40+
// - a cleanup function: that will be called after the component has been stopped
41+
// - a fatal error handler: to handle any error received from the component
42+
type NodeImp struct {
43+
component.Component
3444
logger zerolog.Logger
3545
postShutdown func() error
3646
fatalHandler func(error)
3747
}
3848

3949
// NewNode returns a new node instance
40-
func NewNode(component component.Component, cfg *NodeConfig, logger zerolog.Logger, cleanup func() error, handleFatal func(error)) Node {
50+
func NewNode(
51+
component component.Component,
52+
cfg *NodeConfig,
53+
logger zerolog.Logger,
54+
cleanup func() error,
55+
handleFatal func(error),
56+
) Node {
4157
return &FlowNodeImp{
58+
NodeConfig: cfg,
59+
NodeImp: NewBaseNode(
60+
component,
61+
logger.With().
62+
Str("node_role", cfg.BaseConfig.NodeRole).
63+
Hex("spork_id", logging.ID(cfg.SporkID)).
64+
Logger(),
65+
cleanup,
66+
handleFatal,
67+
),
68+
}
69+
}
70+
71+
// NewBaseNode returns a new base node instance
72+
func NewBaseNode(
73+
component component.Component,
74+
logger zerolog.Logger,
75+
cleanup func() error,
76+
handleFatal func(error),
77+
) NodeImp {
78+
return NodeImp{
4279
Component: component,
43-
NodeConfig: cfg,
4480
logger: logger,
4581
postShutdown: cleanup,
4682
fatalHandler: handleFatal,
@@ -51,13 +87,10 @@ func NewNode(component component.Component, cfg *NodeConfig, logger zerolog.Logg
5187
// which point it gracefully shuts down.
5288
// Any unhandled irrecoverable errors thrown in child components will propagate up to here and
5389
// result in a fatal error.
54-
func (node *FlowNodeImp) Run() {
55-
// Cancelling this context notifies all child components that it's time to shutdown
56-
ctx, cancel := context.WithCancel(context.Background())
57-
defer cancel()
90+
func (node *NodeImp) Run(ctx context.Context) {
5891

5992
// Block until node is shutting down
60-
err := node.run(ctx, cancel)
93+
err := node.run(ctx)
6194

6295
// Any error received is considered fatal.
6396
if err != nil {
@@ -73,14 +106,18 @@ func (node *FlowNodeImp) Run() {
73106
node.logger.Error().Err(err).Msg("error encountered during cleanup")
74107
}
75108

76-
node.logger.Info().Msgf("%s node shutdown complete", node.BaseConfig.NodeRole)
109+
node.logger.Info().Msg("node shutdown complete")
77110
}
78111

79112
// run starts the node and blocks until a SIGINT/SIGTERM is received or an error is encountered.
80113
// It returns:
81114
// - nil if a termination signal is received, and all components have been gracefully stopped.
82-
// - error if a irrecoverable error is received
83-
func (node *FlowNodeImp) run(ctx context.Context, shutdown context.CancelFunc) error {
115+
// - error if an irrecoverable error is received
116+
func (node *NodeImp) run(ctx context.Context) error {
117+
// Cancelling this context notifies all child components that it's time to shut down
118+
ctx, shutdown := context.WithCancel(ctx)
119+
defer shutdown()
120+
84121
// Components will pass unhandled irrecoverable errors to this channel via signalerCtx (or a
85122
// child context). Any errors received on this channel should halt the node.
86123
signalerCtx, errChan := irrecoverable.WithSignaler(ctx)
@@ -97,8 +134,7 @@ func (node *FlowNodeImp) run(ctx context.Context, shutdown context.CancelFunc) e
97134
select {
98135
case <-node.Ready():
99136
node.logger.Info().
100-
Hex("spork_id", logging.ID(node.SporkID)).
101-
Msgf("%s node startup complete", node.BaseConfig.NodeRole)
137+
Msg("node startup complete")
102138
case <-ctx.Done():
103139
}
104140
}()
@@ -118,7 +154,7 @@ func (node *FlowNodeImp) run(ctx context.Context, shutdown context.CancelFunc) e
118154

119155
// 3: Shut down
120156
// Send shutdown signal to components
121-
node.logger.Info().Msgf("%s node shutting down", node.BaseConfig.NodeRole)
157+
node.logger.Info().Msg("node shutting down")
122158
shutdown()
123159

124160
// Block here until all components have stopped or an irrecoverable error is received.

cmd/node_builder.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ import (
3333
const NotSet = "not set"
3434

3535
type BuilderFunc func(nodeConfig *NodeConfig) error
36-
type ReadyDoneFactory func(node *NodeConfig) (module.ReadyDoneAware, error)
36+
37+
// ReadyDoneFactory is a function that returns a ReadyDoneAware component or an error if
38+
// the factory cannot create the component
39+
type ReadyDoneFactory[Input any] func(input Input) (module.ReadyDoneAware, error)
3740

3841
// NodeBuilder declares the initialization methods needed to bootstrap up a Flow node
3942
type NodeBuilder interface {
@@ -73,7 +76,7 @@ type NodeBuilder interface {
7376
// The ReadyDoneFactory may return either a `Component` or `ReadyDoneAware` instance.
7477
// In both cases, the object is started according to its interface when the node is run,
7578
// and the node will wait for the component to exit gracefully.
76-
Component(name string, f ReadyDoneFactory) NodeBuilder
79+
Component(name string, f ReadyDoneFactory[*NodeConfig]) NodeBuilder
7780

7881
// DependableComponent adds a new component to the node that conforms to the ReadyDoneAware
7982
// interface. The builder will wait until all of the components in the dependencies list are ready
@@ -86,15 +89,15 @@ type NodeBuilder interface {
8689
// IMPORTANT: Dependable components are started in parallel with no guaranteed run order, so all
8790
// dependencies must be initialized outside of the ReadyDoneFactory, and their `Ready()` method
8891
// MUST be idempotent.
89-
DependableComponent(name string, f ReadyDoneFactory, dependencies *DependencyList) NodeBuilder
92+
DependableComponent(name string, f ReadyDoneFactory[*NodeConfig], dependencies *DependencyList) NodeBuilder
9093

9194
// RestartableComponent adds a new component to the node that conforms to the ReadyDoneAware
9295
// interface, and calls the provided error handler when an irrecoverable error is encountered.
9396
// Use RestartableComponent if the component is not critical to the node's safe operation and
9497
// can/should be independently restarted when an irrecoverable error is encountered.
9598
//
9699
// Any irrecoverable errors thrown by the component will be passed to the provided error handler.
97-
RestartableComponent(name string, f ReadyDoneFactory, errorHandler component.OnError) NodeBuilder
100+
RestartableComponent(name string, f ReadyDoneFactory[*NodeConfig], errorHandler component.OnError) NodeBuilder
98101

99102
// ShutdownFunc adds a callback function that is called after all components have exited.
100103
// All shutdown functions are called regardless of errors returned by previous callbacks. Any
@@ -299,16 +302,16 @@ func DefaultBaseConfig() *BaseConfig {
299302
// DependencyList is a slice of ReadyDoneAware implementations that are used by DependableComponent
300303
// to define the list of dependencies that must be ready before starting the component.
301304
type DependencyList struct {
302-
components []module.ReadyDoneAware
305+
Components []module.ReadyDoneAware
303306
}
304307

305308
func NewDependencyList(components ...module.ReadyDoneAware) *DependencyList {
306309
return &DependencyList{
307-
components: components,
310+
Components: components,
308311
}
309312
}
310313

311314
// Add adds a new ReadyDoneAware implementation to the list of dependencies.
312315
func (d *DependencyList) Add(component module.ReadyDoneAware) {
313-
d.components = append(d.components, component)
316+
d.Components = append(d.Components, component)
314317
}

cmd/node_test.go

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"context"
45
"errors"
56
"os"
67
"syscall"
@@ -42,7 +43,7 @@ func TestRunShutsDownCleanly(t *testing.T) {
4243

4344
finished := make(chan struct{})
4445
go func() {
45-
node.Run()
46+
node.Run(context.Background())
4647
close(finished)
4748
}()
4849

@@ -62,6 +63,44 @@ func TestRunShutsDownCleanly(t *testing.T) {
6263
}, testLogger.logs)
6364
})
6465

66+
t.Run("Run shuts down gracefully on context cancel", func(t *testing.T) {
67+
testLogger.Reset()
68+
manager := component.NewComponentManagerBuilder().
69+
AddWorker(func(ctx irrecoverable.SignalerContext, ready component.ReadyFunc) {
70+
testLogger.Log("worker starting up")
71+
ready()
72+
testLogger.Log("worker startup complete")
73+
74+
<-ctx.Done()
75+
testLogger.Log("worker shutting down")
76+
testLogger.Log("worker shutdown complete")
77+
}).
78+
Build()
79+
node := NewNode(manager, nodeConfig, logger, postShutdown, fatalHandler)
80+
81+
ctx, cancel := context.WithCancel(context.Background())
82+
83+
finished := make(chan struct{})
84+
go func() {
85+
node.Run(ctx)
86+
close(finished)
87+
}()
88+
89+
<-node.Ready()
90+
91+
cancel()
92+
93+
<-finished
94+
95+
assert.Equal(t, []string{
96+
"worker starting up",
97+
"worker startup complete",
98+
"worker shutting down",
99+
"worker shutdown complete",
100+
"running cleanup",
101+
}, testLogger.logs)
102+
})
103+
65104
t.Run("Run encounters error during postShutdown", func(t *testing.T) {
66105
testLogger.Reset()
67106
manager := component.NewComponentManagerBuilder().
@@ -82,7 +121,7 @@ func TestRunShutsDownCleanly(t *testing.T) {
82121

83122
finished := make(chan struct{})
84123
go func() {
85-
node.Run()
124+
node.Run(context.Background())
86125
close(finished)
87126
}()
88127

@@ -123,7 +162,7 @@ func TestRunShutsDownCleanly(t *testing.T) {
123162

124163
finished := make(chan struct{})
125164
go func() {
126-
node.Run()
165+
node.Run(context.Background())
127166
close(finished)
128167
}()
129168

@@ -157,7 +196,7 @@ func TestRunShutsDownCleanly(t *testing.T) {
157196

158197
finished := make(chan struct{})
159198
go func() {
160-
node.Run()
199+
node.Run(context.Background())
161200
close(finished)
162201
}()
163202

@@ -191,7 +230,7 @@ func TestRunShutsDownCleanly(t *testing.T) {
191230

192231
finished := make(chan struct{})
193232
go func() {
194-
node.Run()
233+
node.Run(context.Background())
195234
close(finished)
196235
}()
197236

cmd/observer/main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package main
22

33
import (
4+
"context"
5+
46
nodebuilder "github.com/onflow/flow-go/cmd/observer/node_builder"
57
)
68

@@ -22,5 +24,5 @@ func main() {
2224
if err != nil {
2325
anb.Logger.Fatal().Err(err).Send()
2426
}
25-
node.Run()
27+
node.Run(context.Background())
2628
}

0 commit comments

Comments
 (0)