SQL capture feature flag check causes handled RecursionError, "undefined method `lookup' for nil" (type_map), and PG::TypeMap TypeError on every instance
Summary
!246680 (merged) added two call sites that evaluate Feature.enabled?(:database_capture) from inside the query execution/notification path:
lib/gitlab/patch/postgresql_adapter_returning_values.rb:26(adapterlogpatch)lib/gitlab/database/query_analyzers/capture.rb:53(query analyzer, runs on every query — itsenabled?gate is onlyGitlab::Runtime.application?)
A DB-backed feature flag check in this path is re-entrant and can run against a partially-configured connection. This produces three recurring handled errors. All are tracked via Gitlab::ErrorTracking, so each occurrence is sent to Sentry (when configured) and always written at ERROR level to exceptions_json.log.
Errors observed (gstg-ref, Sidekiq Geo::* workers)
1. RecursionError: self recursion
Chain (from Sentry trace):
- Worker issues
INSERT ... RETURNING→ adapterlogpatch callsCapture.enabled?→Feature.enabled?(:database_capture). - Cold Flipper cache → the flag check issues its own gate SQL.
- That SQL fires
sql.active_record→ query analyzer →QueryAnalyzers::Capture.analyze→Feature.enabled?(:database_capture)again, while the first evaluation of the same key is still on the stack. Feature#unsafe_getre-entry guard raisesRecursionErrorand tracks it.
The recursion is between the two call sites the MR added, nested via Flipper's gate query. The RETURNING-pattern guard in the patch only breaks patch→patch recursion; the analyzer path has no equivalent guard.
2. NoMethodError: undefined method 'lookup' for nil
type_map.lookup(column.oid, column.fmod, column.sql_type)The analyzer's flag check triggers a Flipper model load (_default_attributes → lookup_cast_type_from_column) while a connection is mid-configure_connection: the SET SESSION/decoder setup queries fire sql.active_record notifications before reload_type_map runs, so @type_map is nil. verify! if type_map.nil? does not help because the connection is live.
3. TypeError: wrong argument type nil (expected PG::TypeMap)
self.type_map = type_map (pg/result.rb)Same window as (2), before add_pg_decoders initialises @type_map_for_results.
Key evidence: the flag value is irrelevant
- All errors occurred on gstg-ref where chatops reports "The feature database_capture does not exist" — the flag has never been set and evaluates to its default (
false). - The crash happens while evaluating the flag, before its value is known. Disabling the flag therefore cannot prevent any of these errors.
Impact
- No functional breakage: all three errors are rescued/handled; jobs complete.
- Each occurrence writes an ERROR-level entry to
exceptions_json.logand reports to Sentry where configured, on any instance running this code. - Frequency scales with traffic: Flipper's L1 cache expires every minute per process, so cold-cache windows recur constantly.
- Currently surfacing under
Sidekiq/Geo::*transactions only because Geo has staging-ref Sentry alert rules and query-heavy re-enqueueing workers; the code path is shared by all queries. - If the flag is ever enabled for its intended use (traffic capture for replay testing), captures would silently drop statements during these windows.
Suggested fix direction
Keep the Feature.enabled?(:database_capture) evaluation out of the query execution/notification path. Options:
- Memoise the flag value outside the hot path and refresh it periodically (the capture thread could own this).
- Add a re-entrancy guard so the analyzer/patch skip their check when a
database_captureflag evaluation is already on the stack. - Skip analysis when the target connection's type map is not yet initialised.
Related
- Introduced by: !246680 (merged) (commit e88fb5e2, 19.4)
- Feature: #548606, rollout: #571197
- Prior discussion: !246680 (comment 3746384642) (RecursionError reported 2026-08-27, type-map errors 2026-09-08)
- Adjacent (pre-existing, different cause): #592511 — type-map OID race