Skip to content

Darling: unbounded memory growth in the collector row accumulation — query_store OOM cascades to Postgres commit-limit failures and process crash #1556

Closed

Description

@erikdarlingdata

Field report (24-server fleet, 16GB host, nightly 3.1.0-nightly.20260717 post-#1553): service memory grew 0 → ~13GB in under 9 minutes, OutOfMemoryException in query_store collection (caught, logged, growth continued), then system-wide commit-limit exhaustion — bundled Postgres threw could not reattach to shared memory (error code 1455) / database system is in recovery mode, RDP and SSM on the box degraded, and the service process died twice (exit 1067, no WER dump). Reproduced on two consecutive restarts.

Root cause (grounded)

  1. Unbounded accumulation across databases: DarlingCollectorRunner.RunAsync creates ONE List<TRow> per collector run and every database's rows accumulate into it; storage happens only after the full database loop (DarlingCollectorRunner.cs:262-278). A server's entire per-database haul is held in memory at once.
  2. The per-database catch retains the accumulation: the Failed to collect {Collector} from [{Database}] handler (:272-277 — the exact line the field log captured) swallows the OOM and CONTINUES the loop with the already-accumulated list fully retained.
  3. The query_store read is unbounded and plan-heavy: no TOP/row cap ("no size guard" per its own comment), and CONVERT(nvarchar(max), qsp.query_plan) is selected on EVERY runtime-stats interval row — the same plan XML duplicated per interval (QueryStoreCollector.cs:347-353, :430-445). The store-derived watermark makes each restart after an outage re-read everything since the last successful collect.
  4. Darling: bounded-parallel fire-and-track collection sweep + cadence jitter (#1553) #1553's 4-wide sweep multiplies the peak ×4 (four servers' hauls concurrently). Exacerbation, not cause — the reporter's hypothesis was exactly right.

Fix directions

  • Flush-and-clear rows per database (or per N rows) instead of accumulating the whole run — bounds peak to one batch.
  • Bound the query_store read (row cap per database per cycle, logged when hit) and stop duplicating plan XML per interval row (capture it once per plan per cycle — newest interval row only).
  • OOM-specific handling in the item loop: clear the accumulation and ABORT the collector run rather than continuing.
  • Cap watermark catch-up staleness so post-outage restarts do not re-read unbounded history.
  • Viewer: surface a store-unreachable indicator instead of an empty screen (reporter ask 5).
  • Investigate/document the no-WER-dump exit path (reporter ask 4; likely CLR fail-fast under commit exhaustion).

Immediate operator mitigation (no new build): "capturePlans": false in darling.json removes the dominant memory term (plan text becomes a NULL placeholder).

🤖 Generated with Claude Code

Activity

  1. added 2 commits that reference this issue on Jul 17, 2026
  2. erikdarlingdata commented on Jul 18, 2026

    @erikdarlingdata
    OwnerAuthor

    Fixed by #1557 (per-database byte budget + flush-per-database + OOM abort semantics + watermark clamp + launch guard), hardened further by #1558/#1559/#1565, and field-verified across two production soaks on the reporting fleet: the crash scenario replayed clean (service steady at 220-315MB where it previously hit ~13GB), the previously-fatal database hit the byte budget gracefully, and the no-filter read shape validated on a real primary at 974ms for 6.2k rows. On dev; ships in the next stable release (nightlies carry it now).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions