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 (adapter log patch)
  • lib/gitlab/database/query_analyzers/capture.rb:53 (query analyzer, runs on every query — its enabled? gate is only Gitlab::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):

  1. Worker issues INSERT ... RETURNING → adapter log patch calls Capture.enabled? → Feature.enabled?(:database_capture).
  2. Cold Flipper cache → the flag check issues its own gate SQL.
  3. 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.
  4. Feature#unsafe_get re-entry guard raises RecursionError and 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.log and 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_capture flag evaluation is already on the stack.
  • Skip analysis when the target connection's type map is not yet initialised.