You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(nuq-postgres): batch group cleanup and add predicate-matching indexes (firecrawl#3468)
* fix(nuq-postgres): batch group cleanup and add predicate-matching indexes
Cap nuq_group_crawl_clean at 10000 groups per run with FOR UPDATE SKIP
LOCKED so the cascading deletes can't outrun the 5min schedule. Add
partial indexes that match the standalone (group_id IS NULL) cleaners'
predicates so they stop seq-scanning the 18M-row queue_scrape table on
every tick.
Co-Authored-By: mogery <mogery@sideguide.dev>
* fix(nuq-postgres): schedule REINDEX for standalone partial indexes
Co-Authored-By: mogery <mogery@sideguide.dev>
* fix(nuq-postgres): add plain group_id indexes and victim-selection index
EXPLAIN on the cascading DELETE in nuq_group_crawl_clean showed a seq scan
over 21.8M rows of queue_scrape -- ~7s for just 100 group_ids -- because
every existing (group_id, ...) index was filtered by mode='single_urls' or
status, so none covered DELETE WHERE group_id IN (...). queue_crawl_finished
had no group_id index at all. group_crawl had no index for the
status='completed' AND expires_at < now() victim selection (the existing
partial is on status='active', the opposite predicate).
Add three plain indexes plus matching REINDEX schedules.
Co-Authored-By: mogery <mogery@sideguide.dev>
* fix(nuq-postgres): drop nuq_group_crawl_clean LIMIT to 2000
10000 was too aggressive given the variance in group sizes (p50=9 jobs/group
but max=8495). One outlier group landing in a batch was enough to blow past
the 4min statement_timeout even with the new group_id index. Steady state
arrival is ~1600 groups/tick, so 2000 leaves margin while keeping worst-case
row counts safe.
Co-Authored-By: mogery <mogery@sideguide.dev>
* fix(nuq-postgres): cleaner runs every minute with LIMIT 500 and 90s timeout
Per-row delete cost on queue_scrape (12 indexes, scattered heap pages, ~86
rows/s measured) is the ceiling, not group selection. Bigger batches just
let one heavy outlier group blow the timeout. Smaller batches at faster
cadence get steady-state runs into ~10s and bound worst-case under the
tighter 90s timeout. If a tick still fails, the next minute retries instead
of holding a 4min transaction.
Co-Authored-By: mogery <mogery@sideguide.dev>
* fix(nuq-postgres): drop unused completed-created-at index on queue_scrape
Mirror prod: nuq_queue_scrape_completed_created_at_idx had ~9k scans vs
650M+ on its peers (3 orders of magnitude less) and was 367 MB. Replaced
for the standalone cleaner by the new _standalone_ partial index. Dropped
on prod after confirming no caller; remove from schema and unschedule its
REINDEX cron.
Co-Authored-By: mogery <mogery@sideguide.dev>
---------
Co-authored-by: firecrawl-spring[bot] <254786068+firecrawl-spring[bot]@users.noreply.github.com>
Co-authored-by: mogery <mogery@sideguide.dev>
Copy file name to clipboardExpand all lines: apps/nuq-postgres/nuq.sql
+57-8Lines changed: 57 additions & 8 deletions
Original file line number
Diff line number
Diff line change
@@ -72,7 +72,19 @@ SET (autovacuum_vacuum_scale_factor = 0.01,
72
72
CREATEINDEXIF NOT EXISTS queue_scrape_active_locked_at_idx ONnuq.queue_scrape USING btree (locked_at) WHERE (status ='active'::nuq.job_status);
73
73
CREATEINDEXIF NOT EXISTS nuq_queue_scrape_queued_optimal_2_idx ONnuq.queue_scrape (priority ASC, created_at ASC, id) WHERE (status ='queued'::nuq.job_status);
74
74
CREATEINDEXIF NOT EXISTS nuq_queue_scrape_failed_created_at_idx ONnuq.queue_scrape USING btree (created_at) WHERE (status ='failed'::nuq.job_status);
75
-
CREATEINDEXIF NOT EXISTS nuq_queue_scrape_completed_created_at_idx ONnuq.queue_scrape USING btree (created_at) WHERE (status ='completed'::nuq.job_status);
75
+
76
+
-- Predicate-matching partial indexes for the standalone (group_id IS NULL)
77
+
-- cleaners. In production virtually every row has a group_id, so these
78
+
-- indexes stay tiny and turn the standalone cleaners into fast no-ops instead
79
+
-- of seq scans over the whole 18M-row table.
80
+
CREATEINDEXIF NOT EXISTS nuq_queue_scrape_completed_standalone_created_at_idx ONnuq.queue_scrape USING btree (created_at) WHERE (status ='completed'::nuq.job_statusAND group_id IS NULL);
81
+
CREATEINDEXIF NOT EXISTS nuq_queue_scrape_failed_standalone_created_at_idx ONnuq.queue_scrape USING btree (created_at) WHERE (status ='failed'::nuq.job_statusAND group_id IS NULL);
82
+
83
+
-- Plain group_id index for the cascading DELETE in nuq_group_crawl_clean.
84
+
-- The other partial (group_id, ...) indexes are all filtered by mode or status
85
+
-- so DELETE WHERE group_id IN (...) seq-scans the whole 18M-row table without
86
+
-- this. EXPLAIN confirmed ~7s/100 group_ids before adding this.
87
+
CREATEINDEXIF NOT EXISTS nuq_queue_scrape_group_id_idx ONnuq.queue_scrape (group_id) WHERE group_id IS NOT NULL;
76
88
77
89
-- Indexes for crawl-status.ts queries
78
90
-- For getGroupAnyJob: query by group_id, owner_id, and data->>'mode' = 'single_urls'
SELECTcron.schedule('nuq_reindex_queue_scrape_backlog_group_id', '0 6 * * *', $$REINDEX INDEX CONCURRENTLY nuq.idx_queue_scrape_backlog_group_id;$$);
153
164
SELECTcron.schedule('nuq_reindex_queue_scrape_backlog_times_out_at', '20 6 * * *', $$REINDEX INDEX CONCURRENTLY nuq.nuq_queue_scrape_backlog_times_out_at_idx;$$);
154
165
166
+
SELECTcron.schedule('nuq_reindex_queue_scrape_completed_standalone', '40 6 * * *', $$REINDEX INDEX CONCURRENTLY nuq.nuq_queue_scrape_completed_standalone_created_at_idx;$$);
167
+
SELECTcron.schedule('nuq_reindex_queue_scrape_failed_standalone', '40 8 * * *', $$REINDEX INDEX CONCURRENTLY nuq.nuq_queue_scrape_failed_standalone_created_at_idx;$$);
168
+
SELECTcron.schedule('nuq_reindex_queue_scrape_group_id', '40 9 * * *', $$REINDEX INDEX CONCURRENTLY nuq.nuq_queue_scrape_group_id_idx;$$);
169
+
155
170
-- Watchdog: cancel any nuq REINDEX CONCURRENTLY that has been running > 18 min.
156
171
-- Acts as the safety net since statement_timeout cannot be set inline with
157
172
-- REINDEX CONCURRENTLY. Slots are 20 min apart, so the watchdog must run
@@ -195,6 +210,15 @@ CREATE INDEX IF NOT EXISTS nuq_queue_crawl_finished_queued_optimal_2_idx ON nuq.
195
210
CREATEINDEXIF NOT EXISTS nuq_queue_crawl_finished_failed_created_at_idx ONnuq.queue_crawl_finished USING btree (created_at) WHERE (status ='failed'::nuq.job_status);
196
211
CREATEINDEXIF NOT EXISTS nuq_queue_crawl_finished_completed_created_at_idx ONnuq.queue_crawl_finished USING btree (created_at) WHERE (status ='completed'::nuq.job_status);
197
212
213
+
-- Predicate-matching partial indexes for the standalone (group_id IS NULL)
214
+
-- cleaners; see note on queue_scrape above.
215
+
CREATEINDEXIF NOT EXISTS nuq_queue_crawl_finished_completed_standalone_created_at_idx ONnuq.queue_crawl_finished USING btree (created_at) WHERE (status ='completed'::nuq.job_statusAND group_id IS NULL);
216
+
CREATEINDEXIF NOT EXISTS nuq_queue_crawl_finished_failed_standalone_created_at_idx ONnuq.queue_crawl_finished USING btree (created_at) WHERE (status ='failed'::nuq.job_statusAND group_id IS NULL);
217
+
218
+
-- Plain group_id index for the cascading DELETE in nuq_group_crawl_clean.
219
+
-- See note on queue_scrape above.
220
+
CREATEINDEXIF NOT EXISTS nuq_queue_crawl_finished_group_id_idx ONnuq.queue_crawl_finished (group_id) WHERE group_id IS NOT NULL;
0 commit comments