This document describes the service lifecycle management system in fuel-core, which provides a standardized pattern for starting, stopping, and monitoring asynchronous services throughout the node. This infrastructure ensures orderly initialization sequences, graceful shutdown, and error recovery.
For information about the overall service orchestration and sub-service composition, see FuelService Architecture. For details on individual sub-services, see Sub-Services Overview.
The service lifecycle system is built around three core abstractions:
All services in fuel-core follow this pattern, from the top-level FuelService down to individual sub-services like TxPool, GraphQL, and BlockProducer.
Sources: crates/services/src/service.rs1-52 crates/fuel-core/src/service.rs1-76
Services transition through a well-defined state machine represented by the State enum:
State Descriptions:
| State | Description | Duration |
|---|---|---|
NotStarted | Service created but not yet started | Until start() called |
Starting | Service initializing via into_task() | Until initialization completes |
Started | Service running normally, executing run() loop | Until stop() called or error |
Stopping | Service shutting down gracefully | Until shutdown() completes |
Stopped | Service cleanly terminated | Terminal state |
StoppedWithError(String) | Service terminated due to panic | Terminal state |
Sources: crates/services/src/state.rs5-47
The state machine enforces valid transitions. Invalid operations return errors:
start() on a service already starting/started returns an errorstop() on an already stopped service returns falseNotStarted, Starting, and Started states can transition to StoppingThe state is managed via a tokio::sync::watch channel, allowing multiple observers to monitor state changes reactively.
Sources: crates/services/src/service.rs261-321
The StateWatcher provides reactive monitoring of service state changes. It wraps a watch::Receiver<State> and adds utility methods:
Key Methods:
| Method | Purpose |
|---|---|
borrow() | Get current state without marking as seen |
borrow_and_update() | Get current state and mark as seen |
changed().await | Wait for state change |
while_started().await | Loop until state is not Started |
wait_stopping_or_stopped().await | Wait until service begins or completes shutdown |
The while_started() method is particularly important for task loops - it continues until the service receives a stop signal.
Sources: crates/services/src/state.rs49-136
The ServiceRunner<S> type wraps a service and manages its lifecycle:
The ServiceRunner separates control operations (start/stop) from service execution. When created, it spawns a background task that:
Starting state signalRunnableService::into_task() to initializeStarted stateRunnableTask::run() while state is StartedRunnableTask::shutdown() when stoppingStoppedWithErrorSources: crates/services/src/service.rs188-253 crates/services/src/service.rs323-379
The Service trait defines the control interface:
| Operation | Behavior | Return |
|---|---|---|
start() | Send start signal | Result<(), anyhow::Error> |
start_and_await() | Start and wait until Started or stopped | Result<State> |
stop() | Send stop signal | bool (true if was running) |
stop_and_await() | Stop and wait for Stopped | Result<State> |
state() | Get current state | State |
state_watcher() | Get StateWatcher for monitoring | StateWatcher |
Sources: crates/services/src/service.rs22-52 crates/services/src/service.rs256-321
Services are created using the new or new_with_params constructors:
The shared_data() method returns a cloneable shared state that external code can use to interact with the service. For example, TxPool exposes methods to insert transactions via its shared state.
Sources: crates/services/src/service.rs209-230 crates/services/src/service.rs381-424
Services implement RunnableService to define initialization logic:
Associated Types:
SharedData: Cloneable state exposed to external callers (e.g., TxPoolSharedState)Task: The runnable task type returned by into_task()TaskParams: Optional parameters passed during initializationThe into_task() method is called when the service starts. It can perform setup operations like connecting to peers or initializing databases. The state_watcher parameter allows monitoring for early shutdown signals.
Sources: crates/services/src/service.rs54-84
After initialization, the RunnableTask trait defines the execution loop:
TaskNextAction Enum:
The run() method contains one iteration of the service's main logic. It should:
watcher.borrow() periodically to detect stop signalsTaskNextAction::Continue to run againTaskNextAction::Stop to initiate shutdownThe shutdown() method performs cleanup when the service stops.
Sources: crates/services/src/service.rs86-186
The GraphQL API service demonstrates this pattern:
The GraphqlService::into_task() method:
AsyncProcessor with the specified thread countaxum::Server to the TCP listenerTask containing the server futureThe Task::run() method simply awaits the server future, which internally loops handling HTTP requests.
Sources: crates/fuel-core/src/graphql_api/api_service.rs161-233
The FuelService is the top-level orchestrator that manages all sub-services:
The FuelService::new() constructor:
init_sub_services() to create all sub-servicesTask and ServiceRunnerSources: crates/fuel-core/src/service.rs138-197
The init_sub_services() function in sub_services.rs creates all services in a specific order:
Critical Ordering:
The block producer must be last because it depends on all other services being available.
Sources: crates/fuel-core/src/service/sub_services.rs133-592
The FuelService::start_and_await() method orchestrates the startup:
The into_task() method starts each sub-service sequentially, using tokio::select! to respect early stop signals. After all services start, it sends the block_production_ready_signal, which unblocks the PoA service to begin producing blocks.
Sources: crates/fuel-core/src/service.rs415-421 crates/fuel-core/src/service.rs478-511
The Task::run() method monitors all sub-services for shutdown:
The shutdown sequence is defensive:
FuelService stopsSources: crates/fuel-core/src/service.rs513-547
The service infrastructure catches panics at two levels:
into_task() are not caught - the service fails to startrun() or shutdown() are caught and converted to StoppedWithErrorWhen a panic occurs:
catch_unwind()panic_to_string()StoppedWithError(panic_info)resume_unwind()This ensures graceful notification of other services while still propagating the panic for visibility.
Sources: crates/services/src/service.rs336-378 crates/services/src/service.rs426-444
The TaskNextAction::ErrorContinue variant allows services to report errors without stopping:
This is useful for transient errors like network timeouts. The error is logged at the error level, but the service continues running.
Helper Macros:
Sources: crates/services/src/service.rs86-145
When a sub-service stops with an error, the FuelService detects it:
The continue_on_error configuration flag controls whether some errors are tolerated or cause immediate shutdown. This is primarily used during development.
Sources: crates/fuel-core/src/service.rs513-531
The fuel-core run command is the standard entry point:
The exec() function:
ShutdownListener for signal handlingtokio::select! to handle early cancellationsend_stop_signal_and_await_shutdown() on terminationSources: bin/fuel-core/src/cli/run.rs840-866
The Command::get_config() method builds the complete configuration:
Configuration validation includes:
allow_syscall requires debug modeThe configuration is made consistent with Config::make_config_consistent(), which ensures flags like utxo_validation are synchronized across services.
Sources: bin/fuel-core/src/cli/run.rs342-798 crates/fuel-core/src/service/config.rs302-316
Each service is instrumented with futures metrics:
The metrics track:
Sources: crates/services/src/service.rs227-228
Test helpers provide utilities for monitoring service state:
This is extensively used in integration tests to verify proper service startup and shutdown.
Sources: crates/fuel-core/src/service.rs628-651
Services expose shared state via cloneable handles:
The shared state typically uses Arc, tokio::sync::watch, or other concurrency primitives to enable safe multi-threaded access.
Sources: crates/fuel-core/src/service/adapters.rs317-324
Services depend on port traits, not concrete implementations:
This enables dependency injection and testability. See Adapters and Ports Pattern for details.
Sources: crates/fuel-core/src/service/adapters.rs447-474
Services must start in dependency order. The init_sub_services() function carefully orders initialization:
Sources: crates/fuel-core/src/service/sub_services.rs549-589
Services should:
StateWatcher for stop signalsTaskNextAction::Stop to initiate shutdownshutdown() methodSources: crates/services/src/service.rs177-186
Tests verify that stopping any sub-service triggers full shutdown:
This ensures the orchestration logic correctly handles individual service failures.
Refresh this wiki