Skip to content

fix: batch notification queries to eliminate N+1 cascading#1679

Open
ionfwsrijan wants to merge 13 commits into
utksh1:mainfrom
ionfwsrijan:fix/1625-notification-n-plus-1
Open

fix: batch notification queries to eliminate N+1 cascading#1679
ionfwsrijan wants to merge 13 commits into
utksh1:mainfrom
ionfwsrijan:fix/1625-notification-n-plus-1

Conversation

@ionfwsrijan

Copy link
Copy Markdown
Contributor

Description

The notification delivery pipeline performed a cascading N+1 query pattern: for each finding, it fetched all rules, and for each rule it checked delivery history and recorded it — resulting in 1 + N×(1 + 2M) DB queries. With 50 findings and 5 rules, this was 551 DB queries plus 250 outbound HTTP calls in a single synchronous chain.

Changes

  1. backend/secuscan/notification_service.py:process_task_notifications() — Rewritten to batch all findings and all active rules upfront (2 queries). Evaluates (finding, rule) pairs in-memory. Dedup check uses a single batched query for all existing delivery history.
  2. backend/secuscan/notification_service.py:_deliver_rule_batch() — New helper that sends a single webhook per (rule, task) containing an array of findings instead of one webhook per (finding, rule). Reduces HTTP calls from N×M to M.
  3. backend/secuscan/executor.py:execute_task() — Notification dispatch is offloaded to asyncio.create_task() so slow webhook targets don't block task finalization.
  4. process_finding_notifications() and deliver_via_rule() are preserved for backward compatibility.

Impact

  • DB queries reduced from 1 + N×(1 + 2M) to 3 + M (findings + rules + dedup + M deliveries)
  • Webhook HTTP calls reduced from N×M to M (one per rule for all matching findings)
  • Task completion is no longer blocked by slow webhook targets
  • New webhook payload uses event: finding.alert.batch with a findings array

Files Changed

  • backend/secuscan/notification_service.py — Major rewrite of process_task_notifications, new _deliver_rule_batch helper
  • backend/secuscan/executor.py — 1 line changed (async dispatch via create_task)

Closes #1625

opencode-bot and others added 13 commits June 25, 2026 13:16
Add _in_transaction flag to Database class so execute() only
auto-commits when not inside an explicit transaction. Expand
transaction scope in _upsert_findings_and_report and
_upsert_findings_and_report_from_scanner to include the findings
INSERT loop, ensuring all writes (findings, structured_json, reports,
asset_services) succeed or roll back atomically.

Previously, each write auto-committed independently, leaving orphaned
findings, stale metadata, or permanently deleted asset services on
crash. The DELETE-before-INSERT in replace_asset_services is now
protected by the outer transaction.

Fixes utksh1#1274
…mit inside transactions

The execute() method was auto-committing after every query, which broke
the transaction semantics when used inside async with db.transaction()
blocks. The first execute() call would commit the BEGIN, and subsequent
operations would run outside the transaction.

- Add _in_transaction flag (initially False)
- begin() sets _in_transaction = True
- commit() and rollback() reset _in_transaction = False
- execute() only auto-commits when _in_transaction is False

This was described in the original commit message for efa59f0 but
the database.py changes were not included.
…icate anyio_backend fixture

- Revert validation.py hostname regex changes (out of PR scope)
- Revert ci.yml backend-tests condition rewrite (out of PR scope, use original)
- Remove duplicate anyio_backend fixture in conftest.py to fix event loop issues
replace_asset_services in platform_resources.py calls db.transaction(),
but _upsert_findings_and_report and _upsert_findings_and_report_from_scanner
already wrap the call in an outer transaction, causing the SQLite error
'cannot start a transaction within a transaction'.

fix: transaction()/begin()/commit()/rollback() all check _in_transaction
and become no-ops when already inside a transaction, allowing safe nesting.
…tandard_scanner

This call was added as part of the PR scope but does not exist on main.
ScannerBase._execute_command already handles this validation for modular
scanners. For standard scanners, command args are plugin-provided module
names (e.g. 'windows.pslist.PsList') that falsely trigger the hostname
regex and cause 'Hostname did not resolve' errors, breaking the
volatility integration test.
On main, _execute_standard_scanner does not accept safe_mode.
This param was added as part of the scope-creep validation call
which has now been removed. Restoring the original signature.
@ionfwsrijan ionfwsrijan force-pushed the fix/1625-notification-n-plus-1 branch from 9c0ac17 to b074126 Compare July 5, 2026 20:28
@ionfwsrijan

Copy link
Copy Markdown
Contributor Author

@utksh1 Please review this. The failing check is pre-exisiting

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Notification Processing Causes Cascading N+1 Database Queries and Webhook Bursts

1 participant