Skip to content

perf(api): ingest compliance overviews in a single transaction#11875

Open
pedrooot wants to merge 4 commits into
masterfrom
fix-compliance-overviews
Open

perf(api): ingest compliance overviews in a single transaction#11875
pedrooot wants to merge 4 commits into
masterfrom
fix-compliance-overviews

Conversation

@pedrooot

@pedrooot pedrooot commented Jul 7, 2026

Copy link
Copy Markdown
Member

Context

The compliance overview ingest (create_compliance_requirements) wrote each scan as a standalone DELETE transaction followed by 10,000-row COPY batches, each on a new admin connection with its own commit. Under midday scheduled-scan load this produced back-to-back 13–15s COPY statements, commit/fsync pile-ups, dead-row churn on retries (orphan recovery re-enqueues scan-compliance-overviews, deleting already-committed rows and re-inserting everything), and slow-statement log flooding — the workload behind the recent Aurora writer failovers.

Changes

  • One transaction per scan: the scan's DELETE and all COPY batches now run on a single admin connection with a single commit. A failed ingest rolls back entirely, so retries never find committed partial data to delete — removing the delete-then-reinsert dead-row churn.
  • Smaller, configurable batch size: new DJANGO_COMPLIANCE_COPY_BATCH_SIZE env var (default 2000, previously hardcoded 10000). Batches share the transaction, so this only bounds per-statement duration, client CSV buffer memory, and slow-query logging — tunable at runtime without a deploy.
  • Atomic ORM fallback: on COPY failure the whole ingest falls back to one RLS transaction (delete + bulk_create), re-streaming rows from a fresh iterator (rows_factory).

Steps to review

Please add a detailed description of how to review this PR.

Checklist

Community Checklist
  • This feature/issue is listed in here or roadmap.prowler.com
  • Is it assigned to me, if not, request it via the issue/feature in here or Prowler Community Slack

SDK/CLI

  • Are there new checks included in this PR? Yes / No
    • If so, do we need to update permissions for the provider? Please review this carefully.

UI

  • All issue/task requirements work as expected on the UI
  • If this PR adds or updates npm dependencies, include package-health evidence (maintenance, popularity, known vulnerabilities, license, release age) and explain why existing/native alternatives are insufficient.
  • Screenshots/Video of the functionality flow (if applicable) - Mobile (X < 640px)
  • Screenshots/Video of the functionality flow (if applicable) - Table (640px > X < 1024px)
  • Screenshots/Video of the functionality flow (if applicable) - Desktop (X > 1024px)
  • Ensure new entries are added to CHANGELOG.md, if applicable.

API

  • All issue/task requirements work as expected on the API
  • Endpoint response output (if applicable)
  • EXPLAIN ANALYZE output for new/modified queries or indexes (if applicable)
  • Performance test results (if applicable)
  • Any other relevant evidence of the implementation (if applicable)
  • Verify if API specs need to be regenerated.
  • Check if version updates are required (e.g., specs, uv, etc.).
  • Ensure new entries are added to CHANGELOG.md, if applicable.

License

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Summary by CodeRabbit

  • New Features

    • Compliance overview updates now load in a single transaction per scan, with batching to better handle large imports.
  • Bug Fixes

    • Improved reliability when compliance data is reprocessed, helping keep results consistent even if an import needs to retry.
    • Ensures stale compliance rows are cleared before new data is written, reducing duplicate or outdated entries.
  • Chores

    • Added a configurable batch size for compliance imports to help tune performance.

@pedrooot pedrooot requested a review from a team as a code owner July 7, 2026 16:00
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

✅ All required changelog fragments are present.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

No Conflicts

No conflict markers, and the branch merges cleanly into its base.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 76415ab3-9b37-4d19-b3fb-4384ca42c48f

📥 Commits

Reviewing files that changed from the base of the PR and between 7d568cb and 8f762c0.

📒 Files selected for processing (1)
  • api/changelog.d/compliance-overview-single-transaction.changed.md

📝 Walkthrough

Walkthrough

Reworks compliance requirement overview ingestion to run within a single per-scan database transaction using configurable batched COPY operations, replacing the previous single-shot COPY with a lazy rows_factory-based API, a transactional delete+insert flow, and an ORM fallback that fully re-ingests on failure. Tests and a changelog entry are updated accordingly.

Changes

Compliance requirement COPY/persist rework

Layer / File(s) Summary
Batch size configuration and imports
api/src/backend/tasks/jobs/scan.py
Adds Callable import and a COMPLIANCE_COPY_BATCH_SIZE env-driven constant bounding per-batch COPY size/memory.
CSV batching and transactional COPY
api/src/backend/tasks/jobs/scan.py
Adds a CSV buffer serialization helper and rewrites _copy_compliance_requirement_rows to delete existing scan rows and stream batched COPY statements within one transaction, committing once or rolling back on error.
Persist wrapper and ORM fallback
api/src/backend/tasks/jobs/scan.py
Replaces _persist_compliance_requirement_rows to accept a rows_factory callable, attempts COPY first, and falls back to scan-scoped ORM delete + bulk_create on failure; updates create_compliance_requirements call site and resets requirement_statuses on each generator iteration for consistent fallback re-ingestion.
Finding resource array construction
api/src/backend/tasks/jobs/scan.py
Reformats construction of resource_regions, resource_services, and resource_types lists during Finding creation.
Test suite updates for new COPY/persist signatures
api/src/backend/tasks/tests/test_scan.py
Updates all COPY/persist call sites and assertions for the new (tenant_id, scan_id, rows, batch_size) and (tenant_id, scan_id, rows_factory) signatures, adds scan-scoped ORM delete assertions, and adds a test verifying multiple batches share one transaction/commit.
Changelog entry
api/changelog.d/compliance-overview-single-transaction.changed.md
Documents the single-transaction, configurable-batch-size COPY ingestion behavior via DJANGO_COMPLIANCE_COPY_BATCH_SIZE (default 2000).

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Job as create_compliance_requirements
  participant Persist as _persist_compliance_requirement_rows
  participant Copy as _copy_compliance_requirement_rows
  participant DB as Database
  participant ORM as ComplianceRequirementOverview

  Job->>Persist: rows_factory
  Persist->>Copy: tenant_id, scan_id, rows_factory(), batch_size
  Copy->>DB: set tenant RLS config
  Copy->>DB: delete existing rows (tenant_id, scan_id)
  loop each batch
    Copy->>DB: copy_expert(CSV buffer)
  end
  alt COPY succeeds
    Copy->>DB: commit()
    Copy-->>Persist: total rows committed
  else COPY fails
    Copy->>DB: rollback()
    Persist->>ORM: filter(scan_id).delete()
    Persist->>ORM: bulk_create(rows_factory())
  end
Loading

Possibly related PRs

  • prowler-cloud/prowler#11591: Both PRs refactor scan.py's compliance-overviews ingest to use lazy/streamed row generation and batched COPY persistence within the same transactional flow for create_compliance_requirements/_persist/_copy_compliance_requirement_rows, updating related test expectations accordingly.

Suggested reviewers: josema-xyz

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: ingesting compliance overviews in a single transaction.
Description check ✅ Passed The description follows the template with context, changes, steps to review, checklist, and license sections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-compliance-overviews

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🔒 Container Security Scan

Image: prowler-api:cc6ca92
Last scan: 2026-07-08 10:05:06 UTC

✅ No Vulnerabilities Detected

The container image passed all security checks. No known CVEs were found.

📋 Resources:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/src/backend/tasks/jobs/scan.py`:
- Around line 102-106: Validate the configured COPY batch size at startup in
scan.py where COMPLIANCE_COPY_BATCH_SIZE is read with env.int. Reject zero or
negative values before they reach the batching path, ideally by enforcing a
positive integer or raising a startup error during module initialization so
compliance ingest cannot begin with an invalid batch size.
- Around line 437-443: The admin COPY path in scan.py bypasses RLS, so the batch
rows must be validated before serialization in the loop that calls
_compliance_requirement_rows_to_csv and cursor.copy_expert. In the
batch-processing logic, reject or filter any row whose tenant_id or scan_id does
not exactly match the function arguments for the current scan, and only pass
validated rows into the COPY buffer. Make this check as close as possible to the
batched(rows, batch_size) processing so mixed-tenant data cannot be inserted
through the raw admin path.
- Around line 1825-1828: The scan rematerialization path in scan.py leaves stale
ComplianceRequirementOverview rows behind when compliance_template is empty
because _persist_compliance_requirement_rows is only reached inside the
non-empty template branch. Update the scan persistence flow around
requirements_created so the scan-scoped delete still runs even when there are no
template requirements, while preserving the existing transactional behavior in
_persist_compliance_requirement_rows and the _copy_compliance_requirement_rows
path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0993c72c-22ba-451c-9656-4fefe659310e

📥 Commits

Reviewing files that changed from the base of the PR and between 76a2d7b and 7d568cb.

📒 Files selected for processing (3)
  • api/CHANGELOG.md
  • api/src/backend/tasks/jobs/scan.py
  • api/src/backend/tasks/tests/test_scan.py

Comment on lines +102 to +106
# Rows per COPY statement when ingesting compliance requirement overviews. All
# batches of a scan share one transaction/commit; the batch size only bounds the
# client-side CSV buffer and how long each individual COPY statement runs on the
# writer (memory footprint, lock time and slow-statement logging under load).
COMPLIANCE_COPY_BATCH_SIZE = env.int("DJANGO_COMPLIANCE_COPY_BATCH_SIZE", default=2000)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Validate the configured COPY batch size.

DJANGO_COMPLIANCE_COPY_BATCH_SIZE=0 or a negative value will flow into the batching path and can break every compliance ingest at runtime. Reject invalid values at startup.

Suggested fix
 COMPLIANCE_COPY_BATCH_SIZE = env.int("DJANGO_COMPLIANCE_COPY_BATCH_SIZE", default=2000)
+if COMPLIANCE_COPY_BATCH_SIZE < 1:
+    raise ValueError("DJANGO_COMPLIANCE_COPY_BATCH_SIZE must be >= 1")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Rows per COPY statement when ingesting compliance requirement overviews. All
# batches of a scan share one transaction/commit; the batch size only bounds the
# client-side CSV buffer and how long each individual COPY statement runs on the
# writer (memory footprint, lock time and slow-statement logging under load).
COMPLIANCE_COPY_BATCH_SIZE = env.int("DJANGO_COMPLIANCE_COPY_BATCH_SIZE", default=2000)
# Rows per COPY statement when ingesting compliance requirement overviews. All
# batches of a scan share one transaction/commit; the batch size only bounds the
# client-side CSV buffer and how long each individual COPY statement runs on the
# writer (memory footprint, lock time and slow-statement logging under load).
COMPLIANCE_COPY_BATCH_SIZE = env.int("DJANGO_COMPLIANCE_COPY_BATCH_SIZE", default=2000)
if COMPLIANCE_COPY_BATCH_SIZE < 1:
raise ValueError("DJANGO_COMPLIANCE_COPY_BATCH_SIZE must be >= 1")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/src/backend/tasks/jobs/scan.py` around lines 102 - 106, Validate the
configured COPY batch size at startup in scan.py where
COMPLIANCE_COPY_BATCH_SIZE is read with env.int. Reject zero or negative values
before they reach the batching path, ideally by enforcing a positive integer or
raising a startup error during module initialization so compliance ingest cannot
begin with an invalid batch size.

Comment on lines +437 to +443
for batch, _is_last in batched(rows, batch_size):
if not batch:
continue
batch_num += 1
csv_buffer = _compliance_requirement_rows_to_csv(batch)
try:
cursor.copy_expert(copy_sql, csv_buffer)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Validate batch rows before admin COPY bypasses RLS.

The raw admin COPY path deletes by tenant_id/scan_id, but inserts whatever tenant_id and scan_id are present in each row. Since this bypasses normal RLS enforcement, reject any row that does not match the function arguments before serializing the batch.

As per coding guidelines, “Never query across multiple tenants or trust client-provided tenant_id without validation.” As per path instructions, this is a “multi-tenant Row-Level Security (RLS)” API.

Suggested fix
                 for batch, _is_last in batched(rows, batch_size):
                     if not batch:
                         continue
                     batch_num += 1
+                    for row in batch:
+                        if (
+                            str(row.get("tenant_id")) != tenant_id
+                            or str(row.get("scan_id")) != scan_id
+                        ):
+                            raise ValueError(
+                                "Compliance COPY row tenant_id/scan_id mismatch"
+                            )
                     csv_buffer = _compliance_requirement_rows_to_csv(batch)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for batch, _is_last in batched(rows, batch_size):
if not batch:
continue
batch_num += 1
csv_buffer = _compliance_requirement_rows_to_csv(batch)
try:
cursor.copy_expert(copy_sql, csv_buffer)
for batch, _is_last in batched(rows, batch_size):
if not batch:
continue
batch_num += 1
for row in batch:
if (
str(row.get("tenant_id")) != tenant_id
or str(row.get("scan_id")) != scan_id
):
raise ValueError(
"Compliance COPY row tenant_id/scan_id mismatch"
)
csv_buffer = _compliance_requirement_rows_to_csv(batch)
try:
cursor.copy_expert(copy_sql, csv_buffer)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/src/backend/tasks/jobs/scan.py` around lines 437 - 443, The admin COPY
path in scan.py bypasses RLS, so the batch rows must be validated before
serialization in the loop that calls _compliance_requirement_rows_to_csv and
cursor.copy_expert. In the batch-processing logic, reject or filter any row
whose tenant_id or scan_id does not exactly match the function arguments for the
current scan, and only pass validated rows into the COPY buffer. Make this check
as close as possible to the batched(rows, batch_size) processing so mixed-tenant
data cannot be inserted through the raw admin path.

Sources: Coding guidelines, Path instructions

Comment on lines +1825 to +1828
# The delete of the scan's previous rows happens inside the same
# transaction as the inserts (see _copy_compliance_requirement_rows).
requirements_created = _persist_compliance_requirement_rows(
tenant_id, _iter_compliance_requirement_rows()
tenant_id_str, scan_id_str, _iter_compliance_requirement_rows

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clear stale rows when the compliance template is empty.

Persistence now owns the scan-scoped delete, but it is only called inside if compliance_template. If a scan is rematerialized with an empty template, old ComplianceRequirementOverview rows can survive while the result reports requirements_created == 0.

Suggested fix
             requirements_created = _persist_compliance_requirement_rows(
                 tenant_id_str, scan_id_str, _iter_compliance_requirement_rows
             )
+        else:
+            requirements_created = _persist_compliance_requirement_rows(
+                str(tenant_id), str(scan_instance.id), lambda: ()
+            )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# The delete of the scan's previous rows happens inside the same
# transaction as the inserts (see _copy_compliance_requirement_rows).
requirements_created = _persist_compliance_requirement_rows(
tenant_id, _iter_compliance_requirement_rows()
tenant_id_str, scan_id_str, _iter_compliance_requirement_rows
# The delete of the scan's previous rows happens inside the same
# transaction as the inserts (see _copy_compliance_requirement_rows).
requirements_created = _persist_compliance_requirement_rows(
tenant_id_str, scan_id_str, _iter_compliance_requirement_rows
)
else:
requirements_created = _persist_compliance_requirement_rows(
str(tenant_id), str(scan_instance.id), lambda: ()
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/src/backend/tasks/jobs/scan.py` around lines 1825 - 1828, The scan
rematerialization path in scan.py leaves stale ComplianceRequirementOverview
rows behind when compliance_template is empty because
_persist_compliance_requirement_rows is only reached inside the non-empty
template branch. Update the scan persistence flow around requirements_created so
the scan-scoped delete still runs even when there are no template requirements,
while preserving the existing transactional behavior in
_persist_compliance_requirement_rows and the _copy_compliance_requirement_rows
path.

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.13793% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 93.83%. Comparing base (6c5f548) to head (8f762c0).

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #11875   +/-   ##
=======================================
  Coverage   93.83%   93.83%           
=======================================
  Files         263      263           
  Lines       38692    38755   +63     
=======================================
+ Hits        36305    36367   +62     
- Misses       2387     2388    +1     
Flag Coverage Δ
api 93.83% <99.13%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
prowler ∅ <ø> (∅)
api 93.83% <99.13%> (+<0.01%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant