Enforce Elasticsearch client timeout to prevent 500s
What does this MR do and why?
Fixes the 500 errors described in #468168 (closed), where Advanced Search requests can run unbounded past Elasticsearch's ?timeout=30s URL parameter (which is best-effort and not strictly enforced) and end up tripping Rack::Timeout at 60s, surfacing as a 500 to the user.
The root cause is that three timeout layers exist today, and only one is actually enforced:
| Layer | Value | Enforced? |
|---|---|---|
ES server-side ?timeout=30s URL param |
30s (1s for count) | Best-effort only |
| Faraday HTTP client request timeout | elasticsearch_client_request_timeout setting, defaults to 0 (= no limit) |
Only when admin sets it |
Rack::Timeout |
60s | Yes — produces the 500 |
This MR addresses all three sub-problems in one change:
- Default the Faraday request timeout to 30s. When
elasticsearch_client_request_timeoutis0(the default), useELASTIC_REQUEST_TIMEOUT(30s) instead of leaving the HTTP call unbounded. This stops requests from ever reaching Rack's 60s limit. - Route timeout exceptions to the existing timeout handler. The instrumentation interceptor previously treated every
Faraday::Error(includingFaraday::TimeoutError) and ESGatewayTimeoutas a genericGitlab::Search::Client::ConnectionError. Split outTIMEOUT_ERRORSand re-raiseElastic::TimeoutError, whichEE::SearchControlleralready rescues withrender_timeout(returning a 408). - Rescue
ConnectionError/AuthorizationErrorincount. Other endpoints (aggregations,autocomplete,haml_search_results) already do this.countdid not — so a transport error there would still 500. It now returns a 503 with{ count: '0' }so users get a graceful fallback (matching the expectation in the issue).
Combined effect for the user: the long-running ES query path now produces a clean 408 ("timed out") or 503 ("unavailable") instead of a 500, and the timeout fires at the network layer (~30s) rather than via Rack at ~60s.
References
- Closes #468168 (closed)
- Related (already merged): !156321 (merged) — removed the surrounding DB transaction for non-basic search count
- Background on best-effort ES timeout: https://discuss.elastic.co/t/search-requests-timeout-doesnt-work-as-expected/328247
How to set up and validate locally
- With Elasticsearch enabled and indexed (
gdk start,Gitlab::CurrentSettings.update!(elasticsearch_search: true, elasticsearch_indexing: true)): - In the rails console, simulate a slow ES query to exercise each path:
# Path 1 (timeout) — should now render 408 instead of 500 allow_any_instance_of(SearchService).to receive(:search_results) .and_raise(Faraday::TimeoutError.new('execution expired')) # Path 2 (connection error during count) — should now render 503 with count: 0 instead of 500 allow_any_instance_of(SearchService).to receive(:search_results) .and_raise(Gitlab::Search::Client::ConnectionError.new('boom')) - Verify
ApplicationSetting#elasticsearch_config[:client_request_timeout]returns30whenelasticsearch_client_request_timeoutis0. - Run targeted specs:
bundle exec rspec ee/spec/lib/gitlab/instrumentation/elasticsearch_transport_spec.rb \ ee/spec/controllers/ee/search_controller_spec.rb \ ee/spec/models/application_setting_spec.rb \ spec/controllers/search_controller_spec.rb
Behavior change for self-managed
Self-managed instances that left elasticsearch_client_request_timeout at 0 (the default) will start enforcing a 30s Faraday timeout. This is strictly an improvement over hanging until Rack kills the request, but operators with intentionally slow queries can still raise the value via the existing admin setting. No flag is added because the change converts an existing 500 path into a graceful 408/503 — the worst-case behavior is the same as today, just earlier and friendlier.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.