Skip to content

Reliability fixes: score 500, DB pooling, async CVE enrichment, thread-safe rate limit - #162

Merged
Vishnu2707 merged 9 commits into
devfrom
fix/150-reliability
Jul 11, 2026
Merged

Vishnu2707 merged 9 commits into
devfrom
fix/150-reliability

Conversation

@TFT444

@TFT444 TFT444 commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #150 (FIX 2: Reliability).

  • REL-001 (/api/score always 500): removed a duplicate GROUP BY severity in get_score() that caused a SQL syntax error on every call.
  • REL-002 (fragile interval query): replaced the string-substituted INTERVAL '%s minutes' with make_interval(mins => %s) in recover_stale_scans().
  • REL-003 (new connection per request): DatabaseManager now borrows connections from a shared psycopg2.pool.ThreadedConnectionPool per DSN (size configurable via DB_POOL_MAX_CONN, default 10) instead of opening a new PostgreSQL connection on every request.
  • REL-004 (CVE enrichment times out on large scans): POST /api/scans/<id>/enrich now runs enrichment in a background thread and returns 202 immediately; clients poll GET /api/scans/<id> (cve_enrichment_status) for completion, instead of the request blocking on NVD's ~7s-per-lookup rate limit until it times out.
  • REL-005 (NVD rate-limit guard not thread-safe): _wait_for_rate_limit() is now lock-guarded so concurrent callers (e.g. background enrichment threads) can't both read a stale timestamp and fire requests too close together.

Bug found and fixed during self-review

A failed enrichment write (e.g. in update_cve_fields) could leave the DB connection in an aborted-transaction state. The follow-up call to mark the scan FAILED reused that same connection before returning it to the pool, so it silently raised InFailedSqlTransaction and got swallowed — leaving the scan stuck showing ENRICHING forever with no way to tell it had actually failed. Fixed by rolling back before that follow-up write, with regression tests (test_background_enrichment_rolls_back_before_marking_failed).

Known trade-offs (not fixed, out of scope for this issue)

  • If the whole process crashes mid-enrichment, there's no recover_stale_scans-style recovery job for a scan stuck at ENRICHING (only scan execution has that today). Worth a follow-up issue if needed.
  • No cap on concurrent enrichment threads, but they're naturally serialized by the NVD rate-limit lock anyway.

Test plan

  • tests/test_database_manager_reliability.py — single GROUP BY, correct score deduction, make_interval SQL, pool acquire/return/reuse-per-DSN, broken-connection discard
  • tests/test_scans_enrich.py — 202 + background thread scheduling, already-completed/in-progress/missing-scan/no-findings branches, success/failure background outcomes, rollback-before-FAILED regression
  • tests/test_nvd_client.py — new TestRateLimitThreadSafety proving concurrent callers are serialized, not interleaved
  • tests/smoke_test.py — TC-33/34 updated for the async contract, plus a new polling assertion (TC-34b) that verifies enrichment actually reaches COMPLETED, not just that it was accepted
  • Full suite: 144 passed, 2 skipped, 2 pre-existing failures unrelated to this branch (confirmed identical on a clean dev checkout)

…t, thread-safe rate limit

Closes #150.

- REL-001: remove duplicate GROUP BY in get_score() that made /api/score always return 500
- REL-002: replace fragile INTERVAL '%s minutes' string substitution with make_interval(mins => %s)
- REL-003: DatabaseManager now borrows connections from a shared psycopg2 connection pool per DSN (DB_POOL_MAX_CONN, default 10) instead of opening a new connection per request
- REL-004: POST /api/scans/<id>/enrich now runs CVE enrichment in a background thread and returns 202 immediately, instead of blocking on synchronous NVD lookups until the request times out
- REL-005: lock-guard the NVD client's rate-limit check so concurrent callers can't fire requests closer together than the allowed gap
- Fix a bug found during review: a failed enrichment write could leave the connection in an aborted-transaction state, silently swallowing the follow-up FAILED status update and leaving the scan stuck at ENRICHING forever; now rolls back first
Comment thread tests/test_database_manager_reliability.py Fixed
Comment thread tests/test_nvd_client.py Fixed
Comment thread scanner/nvd_client.py Fixed
@TFT444 TFT444 self-assigned this Jul 5, 2026
TFT444 added 2 commits July 5, 2026 16:30
…-quality bot

Both test_database_manager_reliability.py and test_nvd_client.py imported
scanner/api modules with both 'import X as x' and 'from X import y' forms.
Consolidated to one form each; test_nvd_client.py switches to string-path
mock.patch() targets instead of importing the module object, avoiding the
need to touch ~15 pre-existing call sites.
…obal false positive

Replaces the bare module-level _last_request_time global (mutated via the
global keyword) with a small _RateLimiter class holding the timestamp and
lock as instance state. Same behavior, but removes the pattern the
code-quality bot's static analysis was misreading as an unused global
(it couldn't see the value is read by the next invocation of the same
function via module state).
@TFT444
TFT444 requested review from H-Sihan and Vishnu2707 July 5, 2026 15:43
@Vishnu2707

Copy link
Copy Markdown
Collaborator

@TFT444 , kindlly fix the conflicts please before merging.

@Vishnu2707

Copy link
Copy Markdown
Collaborator

@TFT444 , CI is failing, touchbase with this PR pls.

@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

TFT444 added 2 commits July 10, 2026 01:33
The "mark failed" query lost its COALESCE(attempt_count, 1) >= %s clause
during an earlier merge, while still passing 2 params to a query with
only 1 placeholder — this would raise at runtime against a real database
and would mark stale scans failed on their first timeout instead of
after max_attempts retries. Restore the clause to match the tested,
correct version. Also drop the now-superseded REL-002 test, whose
single-query assumptions predate the retry-attempt feature; equivalent
coverage now lives in tests/test_async_scan_persistence.py.
A previous merge auto-resolved cleanly across files but left this call
site stranded: api/models/finding.py dropped run_migrations() (schema
is now Alembic-managed, applied via startup.sh) while api/app.py still
called it whenever DATABASE_URL was set. Git's merge saw no conflict
because the removal and the call site were in different files, but the
result crashed create_app() with AttributeError in any environment with
a real DATABASE_URL configured (e.g. CI's Postgres service), which is
exactly why this passed locally (no DATABASE_URL set) but failed in CI.
@TFT444

TFT444 commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator Author

@Vishnu2707
Only one check is actually failing Container Scan (Trivy). That's a pre-existing issue on the dev base image and is not related to any changes in this PR. We're not touching the Dockerfile or any dependencies so it would fail identically on a clean dev checkout.

The other two that show as failing Backend Tests and CI Summary both actually passed individually. They appear red on the PR page because when any job in a workflow run fails (Trivy in this case), GitHub marks the entire workflow run as failed and surfaces all jobs under it with a red indicator, even the ones that passed.

So the code is clean, all tests pass, lint is green. The only thing left is for reviewers to re-review and update their review status.

@Vishnu2707 Vishnu2707 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved. All the changes are addressed.

@Vishnu2707
Vishnu2707 merged commit 418bcba into dev Jul 11, 2026
16 of 17 checks passed
@Vishnu2707
Vishnu2707 deleted the fix/150-reliability branch July 11, 2026 23:10
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.

[FIX 2] Reliability: score endpoint 500, DB pooling, CVE timeout, query safety, thread safety

2 participants