Skip to content

Commit d91667d

Browse files
committed
fix: dummy el race condition
1 parent 377eb98 commit d91667d

File tree

2 files changed

+90
-16
lines changed

2 files changed

+90
-16
lines changed

beacon_node/client/src/builder.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,24 @@ where
205205
jwt_secret_path: None,
206206
};
207207

208+
// Create a channel to wait for the dummy EL to be ready
209+
let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
210+
208211
// Spawn the dummy EL in a background task
209212
tokio::spawn(async move {
210-
if let Err(e) = dummy_el::start_dummy_el(dummy_el_config).await {
213+
if let Err(e) = dummy_el::prepare_and_start_dummy_el(dummy_el_config, ready_tx).await
214+
{
211215
eprintln!("Error starting dummy execution layer: {:?}", e);
212216
}
213217
});
218+
219+
// Wait for the dummy EL to be ready before continuing
220+
if let Err(_) = ready_rx.await {
221+
return Err(
222+
"Dummy execution layer failed to start or signal readiness".to_string(),
223+
);
224+
}
225+
info!("Dummy execution layer is ready");
214226
}
215227

216228
let kzg_err_msg = |e| format!("Failed to load trusted setup: {:?}", e);

dummy_el/src/lib.rs

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use serde_json::{json, Value as JsonValue};
2020
use std::net::SocketAddr;
2121
use std::path::PathBuf;
2222
use std::sync::Arc;
23+
use tokio::sync::oneshot;
2324
use tracing::{debug, error, warn};
2425

2526
const JSONRPC_VERSION: &str = "2.0";
@@ -36,6 +37,20 @@ pub struct DummyElConfig {
3637
pub jwt_secret_path: Option<PathBuf>,
3738
}
3839

40+
/// Represents a prepared dummy execution layer ready to run
41+
pub struct PreparedDummyEl {
42+
engine_listener: tokio::net::TcpListener,
43+
engine_app: Router,
44+
rpc_listener: tokio::net::TcpListener,
45+
rpc_app: Router,
46+
ws_listener: tokio::net::TcpListener,
47+
ws_app: Router,
48+
metrics_listener: tokio::net::TcpListener,
49+
metrics_app: Router,
50+
p2p_tcp_task: tokio::task::JoinHandle<()>,
51+
p2p_udp_task: tokio::task::JoinHandle<()>,
52+
}
53+
3954
#[derive(Debug, Clone)]
4055
struct AppState {
4156
jwt_secret: Option<Vec<u8>>,
@@ -322,11 +337,29 @@ fn read_jwt_secret(path: &PathBuf) -> anyhow::Result<Vec<u8>> {
322337
Ok(bytes)
323338
}
324339

325-
/// Start the dummy execution layer server
340+
/// Prepare the dummy execution layer for startup
326341
///
327-
/// This spawns the dummy EL HTTP servers on the configured ports.
328-
/// Returns a task handle that should be spawned or awaited.
329-
pub async fn start_dummy_el(config: DummyElConfig) -> anyhow::Result<()> {
342+
/// This function binds all necessary ports and prepares the servers,
343+
/// then signals readiness via the oneshot channel before running the servers.
344+
/// The function does not return until the servers are shut down.
345+
pub async fn prepare_and_start_dummy_el(
346+
config: DummyElConfig,
347+
ready_tx: oneshot::Sender<()>,
348+
) -> anyhow::Result<()> {
349+
let prepared = prepare_dummy_el(config).await?;
350+
351+
// Signal that we're ready
352+
let _ = ready_tx.send(());
353+
354+
// Now run the servers
355+
prepared.run().await
356+
}
357+
358+
/// Prepare the dummy execution layer server without starting it
359+
///
360+
/// This binds all ports and prepares the servers but does not start accepting connections.
361+
/// Returns a `PreparedDummyEl` that can be run with the `run()` method.
362+
pub async fn prepare_dummy_el(config: DummyElConfig) -> anyhow::Result<PreparedDummyEl> {
330363
// Read JWT secret if provided
331364
let jwt_secret = match &config.jwt_secret_path {
332365
Some(path) => match read_jwt_secret(path) {
@@ -426,22 +459,51 @@ pub async fn start_dummy_el(config: DummyElConfig) -> anyhow::Result<()> {
426459
}
427460
});
428461

429-
debug!("Ready to accept requests on all ports");
430-
431-
// Spawn all servers concurrently
462+
// Bind all servers without starting them
432463
let engine_listener = tokio::net::TcpListener::bind(engine_addr).await?;
433464
let rpc_listener = tokio::net::TcpListener::bind(rpc_addr).await?;
434465
let ws_listener = tokio::net::TcpListener::bind(ws_addr).await?;
435466
let metrics_listener = tokio::net::TcpListener::bind(metrics_addr).await?;
436467

437-
tokio::select! {
438-
result = axum::serve(engine_listener, engine_app) => result?,
439-
result = axum::serve(rpc_listener, rpc_app) => result?,
440-
result = axum::serve(ws_listener, ws_app) => result?,
441-
result = axum::serve(metrics_listener, metrics_app) => result?,
442-
_ = p2p_tcp_task => {},
443-
_ = p2p_udp_task => {},
468+
debug!("All listeners bound and ready");
469+
470+
Ok(PreparedDummyEl {
471+
engine_listener,
472+
engine_app,
473+
rpc_listener,
474+
rpc_app,
475+
ws_listener,
476+
ws_app,
477+
metrics_listener,
478+
metrics_app,
479+
p2p_tcp_task,
480+
p2p_udp_task,
481+
})
482+
}
483+
484+
impl PreparedDummyEl {
485+
/// Run the prepared dummy execution layer servers
486+
pub async fn run(self) -> anyhow::Result<()> {
487+
debug!("Running dummy execution layer servers");
488+
489+
tokio::select! {
490+
result = axum::serve(self.engine_listener, self.engine_app) => result?,
491+
result = axum::serve(self.rpc_listener, self.rpc_app) => result?,
492+
result = axum::serve(self.ws_listener, self.ws_app) => result?,
493+
result = axum::serve(self.metrics_listener, self.metrics_app) => result?,
494+
_ = self.p2p_tcp_task => {},
495+
_ = self.p2p_udp_task => {},
496+
}
497+
498+
Ok(())
444499
}
500+
}
445501

446-
Ok(())
502+
/// Start the dummy execution layer server (legacy function)
503+
///
504+
/// This is a convenience function that prepares and starts the dummy EL.
505+
/// For more control, use `prepare_dummy_el()` and `prepare_and_start_dummy_el()`.
506+
pub async fn start_dummy_el(config: DummyElConfig) -> anyhow::Result<()> {
507+
let prepared = prepare_dummy_el(config).await?;
508+
prepared.run().await
447509
}

0 commit comments

Comments
 (0)