Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions bin/gravity_node/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,17 @@ impl<C: ChainSpecParser<ChainSpec = ChainSpec>, Ext: clap::Args + fmt::Debug> Cl
self.logs.log_file_directory =
self.logs.log_file_directory.join(self.chain.chain.to_string());

// greth keeps the node-subcommand default (5 files) outside LogArgs, so `--log.file.*`
// are otherwise accepted while the file layer is never installed (see its app.rs).
if matches!(self.command, Commands::Node(_)) {
self.logs.apply_node_defaults();
}

let _guard = self.init_tracing()?;
debug!(target: "reth::cli", "Initialized tracing, log directory: {}, log level {:?}", self.logs.log_file_directory, self.logs.verbosity);

let runner = CliRunner::try_default_runtime()?;
// reth v2.3.0: init/init-state execute() take a Runtime.
let runtime = runner.runtime();
let components = |spec: Arc<C::ChainSpec>| {
(EthEvmConfig::ethereum(spec.clone()), Arc::new(EthBeaconConsensus::new(spec)))
Expand Down
102 changes: 96 additions & 6 deletions cluster/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -449,14 +449,44 @@ START_SCRIPT
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORKSPACE="$SCRIPT_DIR/.."

# Wait until the kernel has reaped the process. greth v2.3+ can keep the
# RocksDB LOCK held for several seconds after SIGTERM while flushing; if we
# delete the pid file and return early, e2e restart races the lock and dies
# with "Resource temporarily unavailable".
wait_pid_gone() {
local pid="$1"
local max_iters="$2"
local i
for i in $(seq 1 "$max_iters"); do
if ! kill -0 "$pid" 2>/dev/null; then
return 0
fi
sleep 0.5
done
return 1
}

if [ -e "${WORKSPACE}/script/node.pid" ]; then
pid=$(cat "${WORKSPACE}/script/node.pid")
if kill -0 "$pid" 2>/dev/null; then
kill "$pid"
echo "Stopped node (PID: $pid)"
kill "$pid" 2>/dev/null || true
# ~30s graceful (SIGTERM)
if wait_pid_gone "$pid" 60; then
echo "Stopped node (PID: $pid)"
else
echo "Node (PID: $pid) still alive after SIGTERM; sending SIGKILL"
kill -9 "$pid" 2>/dev/null || true
# ~20s after SIGKILL for process to disappear
if wait_pid_gone "$pid" 40; then
echo "Stopped node (PID: $pid, forced)"
else
echo "ERROR: node PID $pid still alive after SIGKILL" >&2
fi
fi
else
echo "Node not running (stale PID file)"
fi
# Only drop the pid file after the process is gone (or we gave up).
rm -f "${WORKSPACE}/script/node.pid"
else
echo "No PID file found"
Expand Down Expand Up @@ -573,14 +603,44 @@ START_SCRIPT
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORKSPACE="$SCRIPT_DIR/.."

# Wait until the kernel has reaped the process. greth v2.3+ can keep the
# RocksDB LOCK held for several seconds after SIGTERM while flushing; if we
# delete the pid file and return early, e2e restart races the lock and dies
# with "Resource temporarily unavailable".
wait_pid_gone() {
local pid="$1"
local max_iters="$2"
local i
for i in $(seq 1 "$max_iters"); do
if ! kill -0 "$pid" 2>/dev/null; then
return 0
fi
sleep 0.5
done
return 1
}

if [ -e "${WORKSPACE}/script/node.pid" ]; then
pid=$(cat "${WORKSPACE}/script/node.pid")
if kill -0 "$pid" 2>/dev/null; then
kill "$pid"
echo "Stopped node (PID: $pid)"
kill "$pid" 2>/dev/null || true
# ~30s graceful (SIGTERM)
if wait_pid_gone "$pid" 60; then
echo "Stopped node (PID: $pid)"
else
echo "Node (PID: $pid) still alive after SIGTERM; sending SIGKILL"
kill -9 "$pid" 2>/dev/null || true
# ~20s after SIGKILL for process to disappear
if wait_pid_gone "$pid" 40; then
echo "Stopped node (PID: $pid, forced)"
else
echo "ERROR: node PID $pid still alive after SIGKILL" >&2
fi
fi
else
echo "Node not running (stale PID file)"
fi
# Only drop the pid file after the process is gone (or we gave up).
rm -f "${WORKSPACE}/script/node.pid"
else
echo "No PID file found"
Expand Down Expand Up @@ -706,14 +766,44 @@ START_SCRIPT
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORKSPACE="$SCRIPT_DIR/.."

# Wait until the kernel has reaped the process. greth v2.3+ can keep the
# RocksDB LOCK held for several seconds after SIGTERM while flushing; if we
# delete the pid file and return early, e2e restart races the lock and dies
# with "Resource temporarily unavailable".
wait_pid_gone() {
local pid="$1"
local max_iters="$2"
local i
for i in $(seq 1 "$max_iters"); do
if ! kill -0 "$pid" 2>/dev/null; then
return 0
fi
sleep 0.5
done
return 1
}

if [ -e "${WORKSPACE}/script/node.pid" ]; then
pid=$(cat "${WORKSPACE}/script/node.pid")
if kill -0 "$pid" 2>/dev/null; then
kill "$pid"
echo "Stopped node (PID: $pid)"
kill "$pid" 2>/dev/null || true
# ~30s graceful (SIGTERM)
if wait_pid_gone "$pid" 60; then
echo "Stopped node (PID: $pid)"
else
echo "Node (PID: $pid) still alive after SIGTERM; sending SIGKILL"
kill -9 "$pid" 2>/dev/null || true
# ~20s after SIGKILL for process to disappear
if wait_pid_gone "$pid" 40; then
echo "Stopped node (PID: $pid, forced)"
else
echo "ERROR: node PID $pid still alive after SIGKILL" >&2
fi
fi
else
echo "Node not running (stale PID file)"
fi
# Only drop the pid file after the process is gone (or we gave up).
rm -f "${WORKSPACE}/script/node.pid"
else
echo "No PID file found"
Expand Down
26 changes: 20 additions & 6 deletions cluster/stop.sh
Original file line number Diff line number Diff line change
Expand Up @@ -113,22 +113,36 @@ stop_node() {

log_info "Stopping $node_id (PID: $pid)..."
kill "$pid" 2>/dev/null || true

# Wait for graceful shutdown
for i in {1..10}; do

# Wait until the process is really gone before dropping the pid file.
# greth v2.3+ can hold the RocksDB LOCK for several seconds after SIGTERM
# while flushing; returning early races the next start
# ("Resource temporarily unavailable" on .../db/state/LOCK).
# ~30s graceful
for i in {1..60}; do
if ! kill -0 "$pid" 2>/dev/null; then
rm -f "$pid_file"
log_info "$node_id stopped"
return 0
fi
sleep 0.5
done
# Force kill if still running

# Force kill if still running, then wait again for the kernel to reap it.
log_warn "$node_id: Force killing..."
kill -9 "$pid" 2>/dev/null || true
for i in {1..40}; do
if ! kill -0 "$pid" 2>/dev/null; then
rm -f "$pid_file"
log_info "$node_id stopped (forced)"
return 0
fi
sleep 0.5
done

# Last resort: drop pid bookkeeping so callers are not stuck, but warn.
rm -f "$pid_file"
log_info "$node_id stopped (forced)"
log_error "$node_id: PID $pid still alive after SIGKILL"
}

# Main
Expand Down
7 changes: 7 additions & 0 deletions gravity_e2e/cluster_test_cases/prague/genesis.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
# !! pragueTime must be > genesis_timestamp_secs so the EIP-2935 deployment
# hook (pipe-exec eip_2935.rs gate parent_ts < pragueTime <= current_ts)
# fires on block 1. Change one, change the other.
#
# !! betaTime (gravity-reth #412): until Beta, filter_invalid_txs wholesale-rejects
# type-4 and from/to-delegated traffic (EIP-7702 emergency lockdown). Missing
# betaTime → lockdown forever (fail-closed). This suite exercises Prague 7702
# behaviour, so release lockdown at the same timestamp as pragueTime.

[dependencies.genesis_contracts]
repo = "https://github.com/Galxe/gravity_chain_core_contracts.git"
Expand All @@ -29,6 +34,8 @@ initial_locked_until_micros = 1798848000000000

[genesis.hardforks]
pragueTime = 1775664001
# Same wall-clock as pragueTime: Prague protocol + Beta lockdown release together.
betaTime = 1775664001

[genesis.faucet]
address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
Expand Down
48 changes: 44 additions & 4 deletions gravity_e2e/gravity_e2e/cluster/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,13 @@ async def stop(self) -> bool:
"""
Stop this individual node.
Returns True if node is now STOPPED.

Important: ``NodeState.STOPPED`` from get_state() only means the PID
file is gone or the process is not reachable via that file. The
generated stop.sh used to delete the pid file right after SIGTERM
while gravity_node was still flushing RocksDB. We capture the PID
*before* stop.sh runs and wait until the OS reports it gone so the
next start does not race the RocksDB LOCK.
"""
if not self.stop_script.exists():
LOG.warning(
Expand All @@ -288,6 +295,14 @@ async def stop(self) -> bool:
LOG.info(f"Node {self.id} is already stopped.")
return True

# Capture PID before stop.sh deletes the pid file.
pid: Optional[int] = None
if self.pid_file.exists():
try:
pid = int(self.pid_file.read_text().strip())
except ValueError:
pid = None

LOG.info(f"Stopping node {self.id}...")

try:
Expand All @@ -304,9 +319,16 @@ async def stop(self) -> bool:
LOG.error(f"Node {self.id} stop script failed: {stderr.decode()}")
return False

# Verify stopped
await asyncio.sleep(1)
# Re-check live state
# Belt-and-suspenders: wait for the real process to exit even if
# stop.sh returned early (older generated scripts, or race).
if pid is not None:
if not await self._wait_for_pid_exit(pid, timeout=50.0):
LOG.error(
f"Node {self.id}: PID {pid} still alive after stop script; "
f"restart would race RocksDB LOCK"
)
return False

final_state, _ = await self.get_state()

if final_state == NodeState.STOPPED:
Expand All @@ -322,11 +344,29 @@ async def stop(self) -> bool:
LOG.error(f"Exception stopping node {self.id}: {e}")
return False

async def _wait_for_pid_exit(self, pid: int, timeout: float = 50.0) -> bool:
"""Wait until ``pid`` is no longer alive (ProcessLookupError on kill 0)."""
import os
import time

deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
os.kill(pid, 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat zombie node PIDs as stopped

In Docker e2e runs, gravity_e2e/run_docker.sh:42 starts the suite under a plain bash -c PID 1, and the generated start.sh backgrounds gravity_node before exiting, so after Node.restart -> stop -> _wait_for_pid_exit sends SIGTERM the child can remain as a defunct process reparented to PID 1. os.kill(pid, 0) still succeeds for zombies, so this new wait can burn the full timeout and report stop() failure even though the node has exited and released RocksDB; the generated shell waits have the same predicate. Please treat Z/defunct as exited or use a reaper/child wait instead.

Useful? React with 👍 / 👎.

except ProcessLookupError:
return True
except PermissionError:
# Process exists but not owned by us — treat as still alive.
pass
await asyncio.sleep(0.25)
return False

async def restart(self) -> bool:
"""Bounce the node."""
if not await self.stop():
return False
await asyncio.sleep(2) # Grace period
# stop() already waits for process exit / RocksDB unlock; short settle only.
await asyncio.sleep(0.5)
return await self.start()

def is_running(self) -> bool:
Expand Down
Loading