Repository navigation
Make circuit breaker thresholds configurable via environment variables - #407
Christopherdominic merged 2 commits into
Conversation
CircuitBreaker::new(5, Duration::from_secs(60)) was hardcoded at every construction site, so tuning a threshold during an incident required a code change and a redeploy. Add CircuitBreakerConfig with from_env() and from_env_prefixed(), following the pattern already used by timeout_layer_from_env() and body_limit_layer_from_env(). Each field resolves independently through <PREFIX>_CIRCUIT_BREAKER_* then CIRCUIT_BREAKER_* then the compiled-in defaults, which are the previous hardcoded values. Unparsable values and a zero failure threshold are logged and skipped rather than blocking startup. main.rs now resolves the DB config once and shares it between the connect-time breaker and AppState::db_circuit_breaker so the two cannot drift; StellarService uses the HORIZON prefix. Test AppState builders use CircuitBreaker::default() to stay independent of the environment. Also serialize the telemetry::sampler tests on a mutex. They race on the process-global OTEL_SAMPLE_RATIO and already failed intermittently on main (2 of 12 runs); the added tests here perturb scheduling enough to surface it on nearly every run.
|
Thanks for picking this up, @arandomogg — this is assigned to you via the GrantFox bot for issue #406. Before this can be merged, all required CI checks need to pass. Currently failing:
Currently passing:
Please push a fix and make sure the full CI suite is green — I'll take another look once it is. Thanks for the contribution! |
The seven failing tests in advanced_integration_tests all died on a duplicate key value violates unique constraint "creators_username_key" while creating their own creator. Cargo runs the test functions in a binary on parallel threads, and every one of these tests shares a single database. TestContext::cleanup called common::cleanup_test_db, which TRUNCATEs creators and tips outright, so a test finishing mid-run deleted the rows every other in-flight test was working with. Combined with fixed usernames shared across tests, an insert would race a neighbour's truncate and collide on the unique index. Give each TestContext a random namespace, derive every creator username and email from it, and delete only that namespace's rows on cleanup instead of truncating shared tables. The tests keep running in parallel and no longer depend on each other's data or on leftovers from a previous run. Callers that read the username back out of the create response, such as tip_flows::test_bulk_tip_processing, are unaffected.
|
#406 — circuit breaker environment configuration. @Bonizozo this is ready for review. One thing needs your action first: the latest run on this branch is sitting in A note on the two red jobs, because neither was caused by this PR. test — fixed here. The seven failures in clippy — pre-existing, and not something this PR should carry. I deliberately did not paper over that. Dropping Happy to open that cleanup PR separately if you want it, and equally happy to split the test-isolation commit out of this one if you would rather keep this PR to a single concern. |
Summary
CircuitBreaker::new(5, Duration::from_secs(60))was hardcoded at every construction site, so tuning a threshold during an incident meant a code change and a redeploy. This adds environment-variable-driven construction following the pattern already used bymiddleware::timeout::timeout_layer_from_env()andmiddleware::body_limit::body_limit_layer_from_env(), with the current hardcoded values as defaults.closes #406
What changed
src/services/circuit_breaker.rsDEFAULT_FAILURE_THRESHOLD(5) andDEFAULT_RECOVERY_TIMEOUT_SECS(60) constants — the previously hardcoded values.CircuitBreakerConfig { failure_threshold, recovery_timeout }with aDefaultimpl.CircuitBreakerConfig::from_env()reads the globalCIRCUIT_BREAKER_FAILURE_THRESHOLD/CIRCUIT_BREAKER_RECOVERY_TIMEOUT_SECS.CircuitBreakerConfig::from_env_prefixed(prefix)layers a per-dependency override on top. Forprefix = "DB"the resolution order per field is:DB_CIRCUIT_BREAKER_FAILURE_THRESHOLD/DB_CIRCUIT_BREAKER_RECOVERY_TIMEOUT_SECSCIRCUIT_BREAKER_FAILURE_THRESHOLD/CIRCUIT_BREAKER_RECOVERY_TIMEOUT_SECSEach field resolves independently, so a per-dependency threshold composes with a globally set recovery window.
CircuitBreaker::from_config(),::from_env(),::from_env_prefixed(), plusDefaultand thefailure_threshold()/recovery_timeout()accessors used by the tests.Bad input never blocks startup: unparsable values and a
0failure threshold (which would report the circuit open before a single request) are logged atwarnand skipped, falling through to the next source in the chain. A0recovery timeout is accepted, since it legitimately means "probe immediately". Resolved values are logged atdebug.Call sites
src/main.rsresolvesCircuitBreakerConfig::from_env_prefixed("DB")once and uses it for both the connect-time breaker insideconnect_with_retry(previously the literals5and60passed as arguments) andAppState::db_circuit_breaker, so the two can no longer drift apart.src/services/stellar_service.rsbuilds the Horizon breaker withCircuitBreaker::from_env_prefixed("HORIZON").AppStatebuilders (src/ws.rs,src/tests/integration_test_setup.rs,tests/common/mod.rs,tests/team_test.rs,tests/tips_test.rs) useCircuitBreaker::default()— same values as before and deliberately not env-dependent, so the suite stays deterministic.The
gateway::api_gatewaybreaker is a different type with its own success-threshold parameter and is untouched.Behaviour
Defaults are unchanged: with no environment variables set, every breaker is still 5 failures / 60 seconds.
CircuitBreaker::newkeeps its existing signature, so no existing caller had to change.Riding out a flaky upstream window is now a config change:
Tests
Ten new unit tests in
src/services/circuit_breaker.rscover defaults, global vars, prefix precedence, independent per-field resolution, case-insensitive prefixes, unparsable values, an invalid prefixed value falling through to the global one, zero-threshold rejection (both global and prefixed), zero recovery timeout, and an env-configured breaker actually tripping at its configured threshold. They serialize on a mutex and clear the variables before and after, sincestd::envis process-global.cargo fmt --allis clean, andcargo clippy --all-targets --all-featuresreports no lint against any of the changed code. Note that the repo-wide clippy gate is already failing independently of this PR: the same command with-D warnings(as CI runs it) exits 101 with 188 errors on unmodifiedmainat 2a91dcd, all pre-existing dead-code and unused-import warnings.cargo test --libleaves only the three failures thatmainalso has locally (db::transaction::tests::savepoint_recovery_works,db::transaction::tests::transaction_rollback_works— both need a live database — and thews::integration::ping_timeout_disconnects_after_missed_pongstiming flake).One unrelated test fix
src/telemetry/sampler.rs— its four tests race on the process-globalOTEL_SAMPLE_RATIO(one clearing it while another has just set it), which already failed intermittently onmain: 2 of 12 runs ofcargo test --lib telemetry::samplerat2a91dcd, with no changes applied. The tests added here perturb scheduling enough that it surfaced on most runs, so rather than ship a PR that reddens CI I serialized them with the same mutex-guard idiom. 12 of 12 green afterwards. Happy to split this into its own PR if you would rather keep this one single-purpose.Docs
.env.examplegains a commented circuit-breaker block, anddocs/DEPLOYMENT.mdgains the four variables in the environment table plus a "Tuning Circuit Breakers" section documenting the resolution order and the incident-response workflow.