Skip to content

Commit df5769f

Browse files
authored
Merge pull request #6202 from The-K-R-O-K/AndriiSlisarchuk/5790-add-stop-control
[Access] Stop Control feature for AN
2 parents 9653906 + 2b4d7d0 commit df5769f

File tree

9 files changed

+394
-10
lines changed

9 files changed

+394
-10
lines changed

cmd/access/node_builder/access_node_builder.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import (
5555
"github.com/onflow/flow-go/engine/access/subscription"
5656
followereng "github.com/onflow/flow-go/engine/common/follower"
5757
"github.com/onflow/flow-go/engine/common/requester"
58+
"github.com/onflow/flow-go/engine/common/stop"
5859
synceng "github.com/onflow/flow-go/engine/common/synchronization"
5960
"github.com/onflow/flow-go/engine/common/version"
6061
"github.com/onflow/flow-go/engine/execution/computation/query"
@@ -173,6 +174,7 @@ type AccessNodeConfig struct {
173174
programCacheSize uint
174175
checkPayerBalance bool
175176
versionControlEnabled bool
177+
stopControlEnabled bool
176178
}
177179

178180
type PublicNetworkConfig struct {
@@ -276,6 +278,7 @@ func DefaultAccessNodeConfig() *AccessNodeConfig {
276278
programCacheSize: 0,
277279
checkPayerBalance: false,
278280
versionControlEnabled: true,
281+
stopControlEnabled: false,
279282
}
280283
}
281284

@@ -325,6 +328,7 @@ type FlowAccessNodeBuilder struct {
325328
ExecutionDatastoreManager edstorage.DatastoreManager
326329
ExecutionDataTracker tracker.Storage
327330
VersionControl *version.VersionControl
331+
StopControl *stop.StopControl
328332

329333
// The sync engine participants provider is the libp2p peer store for the access node
330334
// which is not available until after the network has started.
@@ -994,6 +998,10 @@ func (builder *FlowAccessNodeBuilder) BuildExecutionSyncComponents() *FlowAccess
994998
return nil, err
995999
}
9961000

1001+
if builder.stopControlEnabled {
1002+
builder.StopControl.RegisterHeightRecorder(builder.ExecutionIndexer)
1003+
}
1004+
9971005
return builder.ExecutionIndexer, nil
9981006
}, builder.IndexerDependencies)
9991007
}
@@ -1260,6 +1268,10 @@ func (builder *FlowAccessNodeBuilder) extraFlags() {
12601268
"version-control-enabled",
12611269
defaultConfig.versionControlEnabled,
12621270
"whether to enable the version control feature. Default value is true")
1271+
flags.BoolVar(&builder.stopControlEnabled,
1272+
"stop-control-enabled",
1273+
defaultConfig.stopControlEnabled,
1274+
"whether to enable the stop control feature. Default value is false")
12631275
// ExecutionDataRequester config
12641276
flags.BoolVar(&builder.executionDataSyncEnabled,
12651277
"execution-data-sync-enabled",
@@ -1590,6 +1602,8 @@ func (builder *FlowAccessNodeBuilder) Build() (cmd.Node, error) {
15901602
builder.IndexerDependencies.Add(ingestionDependable)
15911603
versionControlDependable := module.NewProxiedReadyDoneAware()
15921604
builder.IndexerDependencies.Add(versionControlDependable)
1605+
stopControlDependable := module.NewProxiedReadyDoneAware()
1606+
builder.IndexerDependencies.Add(stopControlDependable)
15931607
var lastFullBlockHeight *counters.PersistentStrictMonotonicCounter
15941608

15951609
builder.
@@ -1824,6 +1838,24 @@ func (builder *FlowAccessNodeBuilder) Build() (cmd.Node, error) {
18241838

18251839
return versionControl, nil
18261840
}).
1841+
Component("stop control", func(node *cmd.NodeConfig) (module.ReadyDoneAware, error) {
1842+
if !builder.stopControlEnabled {
1843+
noop := &module.NoopReadyDoneAware{}
1844+
stopControlDependable.Init(noop)
1845+
return noop, nil
1846+
}
1847+
1848+
stopControl := stop.NewStopControl(
1849+
builder.Logger,
1850+
)
1851+
1852+
builder.VersionControl.AddVersionUpdatesConsumer(stopControl.OnVersionUpdate)
1853+
1854+
builder.StopControl = stopControl
1855+
stopControlDependable.Init(builder.StopControl)
1856+
1857+
return stopControl, nil
1858+
}).
18271859
Component("RPC engine", func(node *cmd.NodeConfig) (module.ReadyDoneAware, error) {
18281860
config := builder.rpcConf
18291861
backendConfig := config.BackendConfig

cmd/observer/node_builder/observer_builder.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import (
5454
statestreambackend "github.com/onflow/flow-go/engine/access/state_stream/backend"
5555
"github.com/onflow/flow-go/engine/access/subscription"
5656
"github.com/onflow/flow-go/engine/common/follower"
57+
"github.com/onflow/flow-go/engine/common/stop"
5758
synceng "github.com/onflow/flow-go/engine/common/synchronization"
5859
"github.com/onflow/flow-go/engine/common/version"
5960
"github.com/onflow/flow-go/engine/execution/computation/query"
@@ -164,6 +165,7 @@ type ObserverServiceConfig struct {
164165
executionDataPruningInterval time.Duration
165166
localServiceAPIEnabled bool
166167
versionControlEnabled bool
168+
stopControlEnabled bool
167169
executionDataDir string
168170
executionDataStartHeight uint64
169171
executionDataConfig edrequester.ExecutionDataConfig
@@ -239,6 +241,7 @@ func DefaultObserverServiceConfig() *ObserverServiceConfig {
239241
executionDataPruningInterval: pruner.DefaultPruningInterval,
240242
localServiceAPIEnabled: false,
241243
versionControlEnabled: true,
244+
stopControlEnabled: false,
242245
executionDataDir: filepath.Join(homedir, ".flow", "execution_data"),
243246
executionDataStartHeight: 0,
244247
executionDataConfig: edrequester.ExecutionDataConfig{
@@ -280,6 +283,7 @@ type ObserverServiceBuilder struct {
280283
TxResultsIndex *index.TransactionResultsIndex
281284
IndexerDependencies *cmd.DependencyList
282285
VersionControl *version.VersionControl
286+
StopControl *stop.StopControl
283287

284288
ExecutionDataDownloader execution_data.Downloader
285289
ExecutionDataRequester state_synchronization.ExecutionDataRequester
@@ -681,6 +685,10 @@ func (builder *ObserverServiceBuilder) extraFlags() {
681685
"version-control-enabled",
682686
defaultConfig.versionControlEnabled,
683687
"whether to enable the version control feature. Default value is true")
688+
flags.BoolVar(&builder.stopControlEnabled,
689+
"stop-control-enabled",
690+
defaultConfig.stopControlEnabled,
691+
"whether to enable the stop control feature. Default value is false")
684692
flags.BoolVar(&builder.localServiceAPIEnabled, "local-service-api-enabled", defaultConfig.localServiceAPIEnabled, "whether to use local indexed data for api queries")
685693
flags.StringVar(&builder.registersDBPath, "execution-state-dir", defaultConfig.registersDBPath, "directory to use for execution-state database")
686694
flags.StringVar(&builder.checkpointFile, "execution-state-checkpoint", defaultConfig.checkpointFile, "execution-state checkpoint file")
@@ -1523,6 +1531,10 @@ func (builder *ObserverServiceBuilder) BuildExecutionSyncComponents() *ObserverS
15231531
return nil, err
15241532
}
15251533

1534+
if builder.stopControlEnabled {
1535+
builder.StopControl.RegisterHeightRecorder(builder.ExecutionIndexer)
1536+
}
1537+
15261538
return builder.ExecutionIndexer, nil
15271539
}, builder.IndexerDependencies)
15281540
}
@@ -1826,6 +1838,8 @@ func (builder *ObserverServiceBuilder) enqueueRPCServer() {
18261838

18271839
versionControlDependable := module.NewProxiedReadyDoneAware()
18281840
builder.IndexerDependencies.Add(versionControlDependable)
1841+
stopControlDependable := module.NewProxiedReadyDoneAware()
1842+
builder.IndexerDependencies.Add(stopControlDependable)
18291843

18301844
builder.Component("version control", func(node *cmd.NodeConfig) (module.ReadyDoneAware, error) {
18311845
if !builder.versionControlEnabled {
@@ -1859,6 +1873,25 @@ func (builder *ObserverServiceBuilder) enqueueRPCServer() {
18591873

18601874
return versionControl, nil
18611875
})
1876+
builder.Component("stop control", func(node *cmd.NodeConfig) (module.ReadyDoneAware, error) {
1877+
if !builder.stopControlEnabled {
1878+
noop := &module.NoopReadyDoneAware{}
1879+
stopControlDependable.Init(noop)
1880+
return noop, nil
1881+
}
1882+
1883+
stopControl := stop.NewStopControl(
1884+
builder.Logger,
1885+
)
1886+
1887+
builder.VersionControl.AddVersionUpdatesConsumer(stopControl.OnVersionUpdate)
1888+
1889+
builder.StopControl = stopControl
1890+
stopControlDependable.Init(builder.StopControl)
1891+
1892+
return stopControl, nil
1893+
})
1894+
18621895
builder.Component("RPC engine", func(node *cmd.NodeConfig) (module.ReadyDoneAware, error) {
18631896
accessMetrics := builder.AccessMetrics
18641897
config := builder.rpcConf

engine/access/access_test.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -686,8 +686,7 @@ func (suite *Suite) TestGetSealedTransaction() {
686686
// create the ingest engine
687687
processedHeight := bstorage.NewConsumerProgress(db, module.ConsumeProgressIngestionEngineBlockHeight)
688688

689-
ingestEng, err := ingestion.New(suite.log, suite.net, suite.state, suite.me, suite.request, all.Blocks, all.Headers, collections,
690-
transactions, results, receipts, collectionExecutedMetric, processedHeight, lastFullBlockHeight)
689+
ingestEng, err := ingestion.New(suite.log, suite.net, suite.state, suite.me, suite.request, all.Blocks, all.Headers, collections, transactions, results, receipts, collectionExecutedMetric, processedHeight, lastFullBlockHeight)
691690
require.NoError(suite.T(), err)
692691

693692
// 1. Assume that follower engine updated the block storage and the protocol state. The block is reported as sealed
@@ -848,8 +847,7 @@ func (suite *Suite) TestGetTransactionResult() {
848847
require.NoError(suite.T(), err)
849848

850849
// create the ingest engine
851-
ingestEng, err := ingestion.New(suite.log, suite.net, suite.state, suite.me, suite.request, all.Blocks, all.Headers, collections,
852-
transactions, results, receipts, collectionExecutedMetric, processedHeight, lastFullBlockHeight)
850+
ingestEng, err := ingestion.New(suite.log, suite.net, suite.state, suite.me, suite.request, all.Blocks, all.Headers, collections, transactions, results, receipts, collectionExecutedMetric, processedHeight, lastFullBlockHeight)
853851
require.NoError(suite.T(), err)
854852

855853
background, cancel := context.WithCancel(context.Background())
@@ -1078,8 +1076,7 @@ func (suite *Suite) TestExecuteScript() {
10781076
require.NoError(suite.T(), err)
10791077

10801078
// create the ingest engine
1081-
ingestEng, err := ingestion.New(suite.log, suite.net, suite.state, suite.me, suite.request, all.Blocks, all.Headers, collections,
1082-
transactions, results, receipts, collectionExecutedMetric, processedHeight, lastFullBlockHeight)
1079+
ingestEng, err := ingestion.New(suite.log, suite.net, suite.state, suite.me, suite.request, all.Blocks, all.Headers, collections, transactions, results, receipts, collectionExecutedMetric, processedHeight, lastFullBlockHeight)
10831080
require.NoError(suite.T(), err)
10841081

10851082
// create another block as a predecessor of the block created earlier

engine/access/ingestion/engine_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,7 @@ func (s *Suite) initIngestionEngine(ctx irrecoverable.SignalerContext) *Engine {
189189
)
190190
require.NoError(s.T(), err)
191191

192-
eng, err := New(s.log, s.net, s.proto.state, s.me, s.request, s.blocks, s.headers, s.collections,
193-
s.transactions, s.results, s.receipts, s.collectionExecutedMetric, processedHeight, s.lastFullBlockHeight)
192+
eng, err := New(s.log, s.net, s.proto.state, s.me, s.request, s.blocks, s.headers, s.collections, s.transactions, s.results, s.receipts, s.collectionExecutedMetric, processedHeight, s.lastFullBlockHeight)
194193
require.NoError(s.T(), err)
195194

196195
eng.ComponentManager.Start(ctx)

engine/common/stop/stop_control.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package stop
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/coreos/go-semver/semver"
7+
"github.com/rs/zerolog"
8+
"go.uber.org/atomic"
9+
10+
"github.com/onflow/flow-go/module/component"
11+
"github.com/onflow/flow-go/module/counters"
12+
"github.com/onflow/flow-go/module/executiondatasync/execution_data"
13+
"github.com/onflow/flow-go/module/irrecoverable"
14+
)
15+
16+
type VersionMetadata struct {
17+
// incompatibleBlockHeight is the height of the block that is incompatible with the current node version.
18+
incompatibleBlockHeight uint64
19+
// updatedVersion is the expected node version to continue working with new blocks.
20+
updatedVersion string
21+
}
22+
23+
// StopControl is responsible for managing the stopping behavior of the node
24+
// when an incompatible block height is encountered.
25+
type StopControl struct {
26+
component.Component
27+
cm *component.ComponentManager
28+
29+
log zerolog.Logger
30+
31+
versionData *atomic.Pointer[VersionMetadata]
32+
33+
// Notifier for new processed block height
34+
processedHeightChannel chan uint64
35+
// Signal channel to notify when processing is done
36+
doneProcessingEvents chan struct{}
37+
38+
// Stores latest processed block height
39+
lastProcessedHeight counters.StrictMonotonousCounter
40+
}
41+
42+
// NewStopControl creates a new StopControl instance.
43+
//
44+
// Parameters:
45+
// - log: The logger used for logging.
46+
//
47+
// Returns:
48+
// - A pointer to the newly created StopControl instance.
49+
func NewStopControl(
50+
log zerolog.Logger,
51+
) *StopControl {
52+
sc := &StopControl{
53+
log: log.With().
54+
Str("component", "stop_control").
55+
Logger(),
56+
lastProcessedHeight: counters.NewMonotonousCounter(0),
57+
versionData: atomic.NewPointer[VersionMetadata](nil),
58+
processedHeightChannel: make(chan uint64),
59+
doneProcessingEvents: make(chan struct{}),
60+
}
61+
62+
sc.cm = component.NewComponentManagerBuilder().
63+
AddWorker(sc.processEvents).
64+
Build()
65+
sc.Component = sc.cm
66+
67+
return sc
68+
}
69+
70+
// OnVersionUpdate is called when a version update occurs.
71+
//
72+
// It updates the incompatible block height and the expected node version
73+
// based on the provided height and semver.
74+
//
75+
// Parameters:
76+
// - height: The block height that is incompatible with the current node version.
77+
// - version: The new semantic version object that is expected for compatibility.
78+
func (sc *StopControl) OnVersionUpdate(height uint64, version *semver.Version) {
79+
// If the version was updated, store new version information
80+
if version != nil {
81+
sc.log.Info().
82+
Uint64("height", height).
83+
Str("semver", version.String()).
84+
Msg("Received version update")
85+
86+
sc.versionData.Store(&VersionMetadata{
87+
incompatibleBlockHeight: height,
88+
updatedVersion: version.String(),
89+
})
90+
return
91+
}
92+
93+
// If semver is 0, but notification was received, this means that the version update was deleted.
94+
sc.versionData.Store(nil)
95+
}
96+
97+
// onProcessedBlock is called when a new block is processed block.
98+
// when the last compatible block is processed, the StopControl will cause the node to crash
99+
//
100+
// Parameters:
101+
// - ctx: The context used to signal an irrecoverable error.
102+
func (sc *StopControl) onProcessedBlock(ctx irrecoverable.SignalerContext) {
103+
versionData := sc.versionData.Load()
104+
if versionData == nil {
105+
return
106+
}
107+
108+
newHeight := sc.lastProcessedHeight.Value()
109+
if newHeight >= versionData.incompatibleBlockHeight-1 {
110+
ctx.Throw(fmt.Errorf("processed block at height %d is incompatible with the current node version, please upgrade to version %s starting from block height %d",
111+
newHeight, versionData.updatedVersion, versionData.incompatibleBlockHeight))
112+
}
113+
}
114+
115+
// updateProcessedHeight updates the last processed height and triggers notifications.
116+
//
117+
// Parameters:
118+
// - height: The height of the latest processed block.
119+
func (sc *StopControl) updateProcessedHeight(height uint64) {
120+
select {
121+
case sc.processedHeightChannel <- height: // Successfully sent the height to the channel
122+
case <-sc.doneProcessingEvents: // Process events are done, do not block
123+
}
124+
}
125+
126+
// RegisterHeightRecorder registers an execution data height recorder with the StopControl.
127+
//
128+
// Parameters:
129+
// - recorder: The execution data height recorder to register.
130+
func (sc *StopControl) RegisterHeightRecorder(recorder execution_data.ProcessedHeightRecorder) {
131+
recorder.SetHeightUpdatesConsumer(sc.updateProcessedHeight)
132+
}
133+
134+
// processEvents processes incoming events related to block heights and version updates.
135+
//
136+
// Parameters:
137+
// - ctx: The context used to handle irrecoverable errors.
138+
// - ready: A function to signal that the component is ready to start processing events.
139+
func (sc *StopControl) processEvents(ctx irrecoverable.SignalerContext, ready component.ReadyFunc) {
140+
ready()
141+
142+
defer close(sc.doneProcessingEvents) // Ensure the signal channel is closed when done
143+
144+
for {
145+
select {
146+
case <-ctx.Done():
147+
return
148+
case height, ok := <-sc.processedHeightChannel:
149+
if !ok {
150+
return
151+
}
152+
if sc.lastProcessedHeight.Set(height) {
153+
sc.onProcessedBlock(ctx)
154+
}
155+
}
156+
}
157+
}

0 commit comments

Comments
 (0)