Skip to content

Milagro crypto bls - #4

Merged
Mikerah merged 7 commits into
masterfrom
milagro_crypto_bls
Aug 12, 2018
Merged

Milagro crypto bls#4
Mikerah merged 7 commits into
masterfrom
milagro_crypto_bls

Conversation

@Mikerah

@Mikerah Mikerah commented Aug 12, 2018

Copy link
Copy Markdown
Contributor

Locally, I forgot to switch branches. Instead of cherry-picking the commits and adding those commits to the master branch, I am going to merge this branch with the master.

I will personally make sure this never happens again.

@Mikerah
Mikerah merged commit 597a662 into master Aug 12, 2018
@Mikerah
Mikerah deleted the milagro_crypto_bls branch August 12, 2018 18:13
@Mikerah
Mikerah restored the milagro_crypto_bls branch August 12, 2018 18:13
@Mikerah
Mikerah deleted the milagro_crypto_bls branch October 20, 2018 19:59
wemeetagain pushed a commit that referenced this pull request Aug 2, 2019
Serialize implementation and tests
nflaig added a commit that referenced this pull request Aug 9, 2026
Graceful shutdown hangs and the process has to be force-killed:

```
Aug-07 20:13:03.049 []        info:  Stopping gracefully
Aug-07 20:13:05.065 [network] debug: terminating network worker   <- last shutdown progress
...                                  chain keeps ticking slots for another 54s
dockerd: "Container failed to exit within 1m0s of signal 15 - using the force"
```

`terminateWorkerThread` awaits `Thread.terminate()` outside the timeout
race, so the `retryCount * retryMs` budget is unreachable. The budget is
3s, the hang was 56s, and there is no `Worker thread failed to
terminate, retrying...` in the logs, i.e. it never returned from the
first call.

**Why terminate never resolves.** gdb stacks captured from a live wedged
process show the worker is not blocked, it is spinning:

```
Thread 73 (LWP 1524870 "WorkerThread"):     <- state R, on CPU
#2  uv_run (loop=0x7fd473dc6938, mode=UV_RUN_ONCE)   deps/uv/src/unix/core.c:434
#3  node::Environment::CleanupHandles()
#4  node::Environment::RunCleanup()
#5  node::FreeEnvironment(node::Environment*)
#6  node::worker::Worker::Run()
```

`CleanupHandles()` ends in `while (handle_cleanup_waiting_ != 0 ||
request_waiting_ != 0 || !handle_wrap_queue_.IsEmpty())
uv_run(event_loop(), UV_RUN_ONCE);`. A libuv handle on the worker's loop
never closes, so the loop never exits and the thread never dies. Which
handle is still open is not identified.

**What it costs.** `BeaconNode.close()` closes the network before
`chain.persistToDisk()`, so the hang means the finalized state is never
archived and the db is never closed cleanly. On the affected node
`checkpoint_states/` was empty for 5 days and a restart fell back to a
db state 319 slots behind the head it had at shutdown.

- race `Thread.terminate()` against the timeout so the `retryCount *
retryMs` budget is enforced
- return a boolean instead of throwing, so a failed termination does not
abort the rest of `BeaconNode.close()`
- bound `getApi().close()`, an unbounded RPC into the same worker that
runs before the archive
- log `getActiveResourcesInfo()` when the network core closes, so the
next stuck shutdown can be diagnosed from a log line rather than gdb

**Scope.** This keeps a stuck worker from costing us the state archive.
It does not stop the worker getting stuck, and it does not make the
process exit promptly: `process.exit()` joins every worker via
`stop_sub_worker_contexts()`, confirmed in the same capture, so a stuck
shutdown still runs to the process manager's stop timeout.

```
Thread 1 (LWP 1524136 "MainThread"):
#2  uv_thread_join                     deps/uv/src/unix/thread.c:295
#3  node::worker::Worker::JoinThread()
#4  node::Environment::stop_sub_worker_contexts()
#5  node::DefaultProcessExitHandlerInternal(...)
```

I tried to fix that here too, by surfacing the failed termination and
hard exiting from the CLI. It did not work - on both wedges that
occurred during validation the flag read false at the CLI even though
the worker had set it, and the process still waited for the docker
timeout. That is dropped from this PR rather than shipped unproven, and
`unref()` went with it since `process.exit()` joins regardless of
refcounting.

**Testing.** 101 mainnet shutdowns on this branch, each after soaking at
~200 peers with 90-160 live inbound QUIC connections for at least 5
minutes:

- **99/101 archived the finalized state and logged `Beacon node
closed`**, including all 7 where the worker failed to terminate
- clean shutdowns complete in 5.9-9.1s
- the 7 stuck ones still archived and closed internally in ~9s before
being force-killed on the docker timeout

The build without this change hung at `terminating network worker` and
lost the archive on all 4 shutdowns observed.

`NETWORK_CORE_CLOSE_TIMEOUT_MS` is 5s. At the original 3s it tripped on
35 of 101 shutdowns, so the measurement was censored. Raising it to 10s
temporarily made it uncensored: 21 shutdowns closed the core in 2.0-4.1s
and none hit the bound, so 5s clears the observed max with margin while
still bounding an `await` that sits in front of the archive. Note the 35
censored runs mean it is not established that this RPC always resolves,
which is the argument for bounding it at all.

Root cause notes, gdb captures and the handle-walk tooling:
https://gist.github.com/nflaig/b266d89c03cdd2c76338823afed5b2c0

**AI Assistance Disclosure**

Investigation and patch developed with Claude Code. Cause traced from
debug logs and gdb captures of a live wedged process, validated on a
mainnet node as above.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: lodekeeper <lodekeeper@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
nflaig added a commit that referenced this pull request Aug 9, 2026
Graceful shutdown hangs and the process has to be force-killed:

```
Aug-07 20:13:03.049 []        info:  Stopping gracefully
Aug-07 20:13:05.065 [network] debug: terminating network worker   <- last shutdown progress
...                                  chain keeps ticking slots for another 54s
dockerd: "Container failed to exit within 1m0s of signal 15 - using the force"
```

`terminateWorkerThread` awaits `Thread.terminate()` outside the timeout
race, so the `retryCount * retryMs` budget is unreachable. The budget is
3s, the hang was 56s, and there is no `Worker thread failed to
terminate, retrying...` in the logs, i.e. it never returned from the
first call.

**Why terminate never resolves.** gdb stacks captured from a live wedged
process show the worker is not blocked, it is spinning:

```
Thread 73 (LWP 1524870 "WorkerThread"):     <- state R, on CPU
#2  uv_run (loop=0x7fd473dc6938, mode=UV_RUN_ONCE)   deps/uv/src/unix/core.c:434
#3  node::Environment::CleanupHandles()
#4  node::Environment::RunCleanup()
#5  node::FreeEnvironment(node::Environment*)
#6  node::worker::Worker::Run()
```

`CleanupHandles()` ends in `while (handle_cleanup_waiting_ != 0 ||
request_waiting_ != 0 || !handle_wrap_queue_.IsEmpty())
uv_run(event_loop(), UV_RUN_ONCE);`. A libuv handle on the worker's loop
never closes, so the loop never exits and the thread never dies. Which
handle is still open is not identified.

**What it costs.** `BeaconNode.close()` closes the network before
`chain.persistToDisk()`, so the hang means the finalized state is never
archived and the db is never closed cleanly. On the affected node
`checkpoint_states/` was empty for 5 days and a restart fell back to a
db state 319 slots behind the head it had at shutdown.

- race `Thread.terminate()` against the timeout so the `retryCount *
retryMs` budget is enforced
- return a boolean instead of throwing, so a failed termination does not
abort the rest of `BeaconNode.close()`
- bound `getApi().close()`, an unbounded RPC into the same worker that
runs before the archive
- log `getActiveResourcesInfo()` when the network core closes, so the
next stuck shutdown can be diagnosed from a log line rather than gdb

**Scope.** This keeps a stuck worker from costing us the state archive.
It does not stop the worker getting stuck, and it does not make the
process exit promptly: `process.exit()` joins every worker via
`stop_sub_worker_contexts()`, confirmed in the same capture, so a stuck
shutdown still runs to the process manager's stop timeout.

```
Thread 1 (LWP 1524136 "MainThread"):
#2  uv_thread_join                     deps/uv/src/unix/thread.c:295
#3  node::worker::Worker::JoinThread()
#4  node::Environment::stop_sub_worker_contexts()
#5  node::DefaultProcessExitHandlerInternal(...)
```

I tried to fix that here too, by surfacing the failed termination and
hard exiting from the CLI. It did not work - on both wedges that
occurred during validation the flag read false at the CLI even though
the worker had set it, and the process still waited for the docker
timeout. That is dropped from this PR rather than shipped unproven, and
`unref()` went with it since `process.exit()` joins regardless of
refcounting.

**Testing.** 101 mainnet shutdowns on this branch, each after soaking at
~200 peers with 90-160 live inbound QUIC connections for at least 5
minutes:

- **99/101 archived the finalized state and logged `Beacon node
closed`**, including all 7 where the worker failed to terminate
- clean shutdowns complete in 5.9-9.1s
- the 7 stuck ones still archived and closed internally in ~9s before
being force-killed on the docker timeout

The build without this change hung at `terminating network worker` and
lost the archive on all 4 shutdowns observed.

`NETWORK_CORE_CLOSE_TIMEOUT_MS` is 5s. At the original 3s it tripped on
35 of 101 shutdowns, so the measurement was censored. Raising it to 10s
temporarily made it uncensored: 21 shutdowns closed the core in 2.0-4.1s
and none hit the bound, so 5s clears the observed max with margin while
still bounding an `await` that sits in front of the archive. Note the 35
censored runs mean it is not established that this RPC always resolves,
which is the argument for bounding it at all.

Root cause notes, gdb captures and the handle-walk tooling:
https://gist.github.com/nflaig/b266d89c03cdd2c76338823afed5b2c0

**AI Assistance Disclosure**

Investigation and patch developed with Claude Code. Cause traced from
debug logs and gdb captures of a live wedged process, validated on a
mainnet node as above.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: lodekeeper <lodekeeper@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant