Feature: Return 503 when a goo SPARQL circuit breaker is open - #254
Merged
Conversation
goo can shed load when Redis or the triple store is failing (Ship 2, de-fork review D1/D2), but as a library it can only raise Goo::SPARQL::Resilience::CircuitOpenError. Nothing translated that, so an open breaker surfaced as a 500 with a stack trace: indistinguishable from an application bug, and telling the client nothing about retrying. That made D1/D2 implemented but not in force (recorded as D15). config/resilience.rb adds: * a Sinatra handler mapping CircuitOpenError to 503, with Retry-After set from the breaker's own cool-off and Cache-Control: no-store, since Rack::Cache fronts this app and a cached 503 would keep serving the outage after the dependency recovered; * on_state_change wired to the app logger and a New Relic custom event. goo only warns to stderr, which nothing watches, and a load-bearing dependency tripping is page-worthy. Called per transition, not per rejected request, so an outage produces one notice rather than a flood; * on_invalidation_failure wired to a log line and a counter, since a dropped invalidation leaves stale cache entries until that graph's next successful write. Both hooks swallow their own exceptions: reporting must never be the thing that breaks a request. Inert unless OP_SPARQL_CIRCUIT_BREAKER is set, so this changes nothing on its own. Gemfile.lock moves goo to 8a80f1c (development), which is the first revision containing Goo::SPARQL::Resilience. Tests assert 503 rather than 500, the Retry-After and no-store headers, that normal requests are unaffected, and that both hooks are wired and cannot raise. Verified the breaker tests error out without the handler.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #254 +/- ##
========================================
Coverage 78.94% 78.94%
========================================
Files 69 69
Lines 3767 3767
========================================
Hits 2974 2974
Misses 793 793
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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.
What this does
goo Ship 2 (ncbo/goo#196) can shed load when the Redis cache or the triple store is failing, but as a library it can only raise
Goo::SPARQL::Resilience::CircuitOpenError. Nothing here translated that, so an open breaker surfaced as a 500 with a stack trace: indistinguishable from an application bug, and telling the client nothing about retrying.This adds
config/resilience.rb, the web-layer half of that design. It is the last piece needed before the breaker can actually be switched on.Inert unless
OP_SPARQL_CIRCUIT_BREAKERis set, so merging changes no behavior.What's in it
CircuitOpenError→ 503, withRetry-Aftertaken from the breaker's own cool-off so clients back off for roughly as long as the breaker stays open, andCache-Control: no-store. The no-store matters:Rack::Cachefronts this app, and a cached 503 would keep serving the outage after the dependency recovered. Body follows the usual{errors: [...], status: 503}shape.on_state_changewired to the app logger and a New Relic custom event. goo only warns to stderr, which nothing watches, and a load-bearing dependency tripping is page-worthy. goo calls this per transition rather than per rejected request, so an outage produces one notice, not one per request.on_invalidation_failurewired to a log line and a counter, since a dropped invalidation leaves stale cache entries for that graph until its next successful write.Both hooks swallow their own exceptions: reporting must never be the thing that breaks a request.
Gemfile.lockmoves goo to8a80f1c(development), the first revision containingGoo::SPARQL::Resilience. That also brings in Ship 3 (query logging), likewise off by default.Why 503 rather than 500
The request failed because a dependency is unavailable, not because the request was wrong. 503 is what clients and load balancers already treat as "retry later", and it is the only status that carries
Retry-After. Returning 500 also pollutes error dashboards with what is really deliberate load shedding.Testing
test/controllers/test_resilience_503.rbasserts an open circuit returns 503 and not 500, thatRetry-Aftermatches the breaker cool-off and the response isno-store, that normal requests are untouched, and that both hooks are wired and cannot raise. The breaker itself is goo's to test (test/test_resilience.rb); this covers only the translation, by making a goo query raise the way an open breaker would.Verified the two breaker tests error out with the handler removed, so they are testing the mapping rather than passing incidentally. Locally: the new file plus
test_ontologies_controllerandtest_metrics_controlleragainst 4store, 25 tests, 0 failures.Enabling it
Set
OP_SPARQL_CIRCUIT_BREAKER=truein a deployment's environment once this and goo Ship 2 are released.OP_SPARQL_BREAKER_THRESHOLD(default 5) andOP_SPARQL_BREAKER_COOL_OFF(default 30s) tune it. Worth doing on staging first and watching for the state-change log line, since breaker state is per worker (goo D14), so a real outage produces one notice per unicorn worker.