service: pin gRPC ECALL dispatch to a fixed-size worker pool#144
Closed
ikehara wants to merge 1 commit into
Closed
service: pin gRPC ECALL dispatch to a fixed-size worker pool#144ikehara wants to merge 1 commit into
ikehara wants to merge 1 commit into
Conversation
Add `EcallPool`, a long-lived pool of `--max-enclave-concurrency` OS threads dedicated to executing ECALLs. All gRPC handlers in `elc.rs` (`create_client`, `update_client`, `update_client_stream`, `aggregate_messages`, `verify_membership`, `verify_non_membership`, `client`) now dispatch their ECALLs onto the pool via `tokio::task::spawn_blocking(|| pool.run(|| ...))` instead of either calling the enclave directly from a tonic async handler (which would bind a TCS to every tokio worker that ever ECALLs) or going through the default `tokio::task::spawn_blocking` pool (which can grow to hundreds of cached threads, each binding its own TCS). Under `TCSPolicy=BIND` the Intel SGX SDK pins one TCS to each ECALL-issuing host thread for the thread's lifetime. The `with_ecall_permit` semaphore in `enclave-api` only bounds *concurrent* ECALLs, not the cumulative set of distinct threads that have ever ECALLed. Once that cumulative set exceeds `TCSNum`, any new ECALL-issuing thread fails with `SGX_ERROR_OUT_OF_TCS`. This is what was observed during a long-running Arbitrum activate-client run on bcc-dev even with `--max-enclave-concurrency=4` and `TCSNum=8` (datachainlab/docs#10123). By pinning ECALL execution to `EcallPool`'s `size` permanent workers, the cumulative TCS bindings created by the service stay at `size` for the entire process lifetime, restoring the invariant the existing CLI guidance assumed. Speculative scheduler's own `thread::scope` workers (`speculative/scheduler.rs`) are intentionally left as-is in this change. Their threads terminate at scope exit so the SDK destructor releases their TCS bindings on each batch; addressing that path (folding speculative dispatch into the same pool) is a follow-up so this PR stays small. CLI help text for `--max-enclave-concurrency` is updated: the prior wording recommended `cap == TCSNum`, which is unsafe under BIND once any unbounded ECALL-thread source exists. The new wording describes the flag as the pool size and recommends `cap < TCSNum` to leave headroom for the SDK runtime and the speculative path. Tests: - `cargo test -p service` (37 tests, includes 3 new `ecall_pool` tests asserting concurrency cap, result delivery, and that observed worker-thread ids are bounded by pool size). - `cargo check --workspace` is clean.
Author
|
Closing in favor of pushing the commit directly to |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
EcallPool, a long-lived pool of--max-enclave-concurrencyOS threads dedicated to executing ECALLs. All gRPC handlers inmodules/service/src/elc.rsdispatch their ECALLs onto the pool viatokio::task::spawn_blocking(|| pool.run(|| ...))instead of either calling the enclave directly from a tonic async handler or going through the defaulttokio::task::spawn_blockingpool.Why
Under
TCSPolicy=BINDthe Intel SGX SDK pins one TCS to each ECALL-issuing host thread for the thread's lifetime, releasing it only on thread termination. The existingwith_ecall_permitsemaphore inenclave-apionly bounds concurrent ECALLs; it does not bound the cumulative set of distinct host threads that have ever ECALLed. Once that cumulative set exceedsTCSNum, any new ECALL-issuing thread fails withSGX_ERROR_OUT_OF_TCS.This is what was observed on bcc-dev during a long-running Arbitrum
activate-clienteven with--max-enclave-concurrency=4andTCSNum=8. Full root-cause analysis: datachainlab/docs#10123.The leak sources were:
update_clientandupdate_client_streamdispatched ECALLs throughtokio::task::spawn_blocking, whose default pool can grow to hundreds of cached threads (10 s idle retention).create_client,aggregate_messages,verify_membership,verify_non_membership, andclient(Query) called the enclave directly from the tonicasync fn, binding a TCS to every tokio worker that happened to handle the request.With this PR, ECALLs from all gRPC handlers run on exactly
sizepermanent threads, so the cumulative TCS bindings created by the service stay atsizefor the entire process lifetime.Out of scope
The speculative scheduler in
modules/service/src/speculative/scheduler.rsstill usesstd::thread::scopeper batch. Its workers terminate at scope exit, so the SDK destructor releases their TCS bindings on each batch boundary; the speculative path therefore does not exhibit the cumulative-binding leak. Folding speculative dispatch into the same pool (and removing the now-redundantwith_ecall_permitgate) is a worthwhile follow-up but kept out of this PR to limit blast radius.CLI help text
The current
--max-enclave-concurrencyhelp reads:That guidance is unsafe under BIND once any unbounded ECALL-thread source exists. Updated wording describes the flag as the pool size and recommends
cap < TCSNumto leave headroom for the SDK runtime and the still-unbounded speculative path.Tests
Three new unit tests in
modules/service/src/ecall_pool.rs:pool_limits_concurrent_jobs_to_worker_count— concurrency cap verification.pool_returns_job_result_to_caller— happy-path round-trip.pool_workers_have_stable_thread_ids_across_jobs— asserts that the set of OS thread ids that execute jobs is bounded by pool size, which is the property TCSPolicy=BIND relies on.References