Skip to content

fix(viewer): roll back a payload that fails partway through ingest 🤖🤖🤖 - #274

Open
sushant-mishra-dtu wants to merge 1 commit into
NVIDIA-NeMo:mainfrom
sushant-mishra-dtu:fix/viewer-ingest-partial-write
Open

fix(viewer): roll back a payload that fails partway through ingest 🤖🤖🤖#274
sushant-mishra-dtu wants to merge 1 commit into
NVIDIA-NeMo:mainfrom
sushant-mishra-dtu:fix/viewer-ingest-partial-write

Conversation

@sushant-mishra-dtu

@sushant-mishra-dtu sushant-mishra-dtu commented Sep 5, 2026

Copy link
Copy Markdown

_ingest_one() writes as it goes and leaves the commit to its caller (otlp_store.py:571-755). Nothing rolls back when it raises, so the rows it already wrote stay in the connection's open transaction and are committed by the next commit on that connection. There is no rollback() anywhere in the module.

Both batch callers catch the exception and log "Failed to process one payload, skipping" (:837, :872), and ingest_batch_write()'s docstring promises that "one malformed payload cannot prevent the rest of the batch from being committed". The payload is not skipped: its partial write is committed with the rest.

_ingest_one() updates the sessions row before it inserts the spans, so the common shape is a session whose span_count counts spans that were never written. Reproduced against the public API -- one good payload, then one malformed payload for the same session:

sessions.span_count = 2
rows in spans       = 1
list_sessions() reports span_count = 2
get_session_spans() returns        = 1 spans

The viewer's own two endpoints disagree, and the row survives a fresh connection.

ingest() is worse, because it has no catch at all: it raises, leaves the partial write in the open transaction, and the next unrelated ingest() commits it. A rejected request for one session is persisted on the back of a later, successful request for a different one:

request 1 (malformed, sess-A): raised, in_transaction=True
request 2 (valid, sess-B):     succeeded
-> sess-A: span_count=2, actual spans=1   (committed by request 2)

Wrap _ingest_one() in a SAVEPOINT and roll back to it on failure. A savepoint rather than rollback() because the batch path must keep the payloads that already succeeded -- rolling back the whole transaction would break the batching behaviour the docstring documents.

What does this PR do?

Related issues

Checklist

  • Code follows the project style (uv run ruff check . and uv run ruff format --check . pass)
  • Tests added/updated and passing (uv run pytest)
  • Docs updated if behavior or public APIs changed — nothing to update: _ingest_one_atomic() is a private helper, the three public entry points keep their signatures and their documented behaviour (the batch docstring's promise is what this PR makes true), and docs/ does not document otlp_store's API — docs/concepts/tracing.md describes the viewer only as a process to start.
  • New source files carry an SPDX license header — no new files: both paths are modifications (git diff --name-status reports M, M). scripts/hooks/check_spdx.py exits 0 on this branch and scripts/check_license_headers.py reports "all 929 source Python files carry an SPDX header".

Re-verified on ec2eb12 (Windows 11 / CPython 3.13.6): uv run ruff check .All checks passed!, uv run ruff format --check .926 files already formatted, and tests/viewer154 passed, 2 deselected against 153 passed, 2 deselected on main. The added test is non-vacuous by fault injection: point ingest() back at _ingest_one() and it fails with assert 2 == 1 — exactly the span_count 2 / 1 stored span mismatch described above. A full uv run pytest does not complete on this machine (src/nooa/storage/sqlite.py:10 imports fcntl), so the run is scoped to tests/viewer.

Separate, single-claim fix from #270/#271 and independent of the connection-lifecycle fix on fix/viewer-close-trace-db, opened since as #273 — please review on its own.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Failed telemetry payloads no longer leave behind partial session or span data.
    • Subsequent valid payloads can be ingested successfully after an ingestion error.
  • Tests

    • Added regression coverage to verify failed payloads do not inflate span counts or commit incomplete data.

_ingest_one() writes as it goes and leaves the commit to its caller
(otlp_store.py:571-755). Nothing rolls back when it raises, so the rows it
already wrote stay in the connection's open transaction and are committed by
the next commit on that connection. There is no rollback() anywhere in the
module.

Both batch callers catch the exception and log "Failed to process one payload,
skipping" (:837, :872), and ingest_batch_write()'s docstring promises that "one
malformed payload cannot prevent the rest of the batch from being committed".
The payload is not skipped: its partial write is committed with the rest.

_ingest_one() updates the sessions row before it inserts the spans, so the
common shape is a session whose span_count counts spans that were never
written. Reproduced against the public API -- one good payload, then one
malformed payload for the same session:

    sessions.span_count = 2
    rows in spans       = 1
    list_sessions() reports span_count = 2
    get_session_spans() returns        = 1 spans

The viewer's own two endpoints disagree, and the row survives a fresh
connection.

ingest() is worse, because it has no catch at all: it raises, leaves the
partial write in the open transaction, and the next unrelated ingest() commits
it. A rejected request for one session is persisted on the back of a later,
successful request for a different one:

    request 1 (malformed, sess-A): raised, in_transaction=True
    request 2 (valid, sess-B):     succeeded
    -> sess-A: span_count=2, actual spans=1   (committed by request 2)

Wrap _ingest_one() in a SAVEPOINT and roll back to it on failure. A savepoint
rather than rollback() because the batch path must keep the payloads that
already succeeded -- rolling back the whole transaction would break the
batching behaviour the docstring documents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: sushant-mishra-dtu <sushant.arh@gmail.com>
@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 38830d6a-b5cb-461d-8169-5e1698f6fefc

📥 Commits

Reviewing files that changed from the base of the PR and between e137e1b and ec2eb12.

📒 Files selected for processing (2)
  • src/nooa/viewer/otlp_store.py
  • tests/viewer/test_otlp_store.py

Included review availability: Your plan provides up to 12 included reviews per hour; 10 remain after this review.


📝 Walkthrough

Walkthrough

The change isolates each OTLP payload with a SQLite savepoint. Direct and batch ingestion paths use the atomic wrapper. Regression coverage verifies that failed payloads do not retain partial session updates.

Changes

OTLP atomic ingestion

Layer / File(s) Summary
Savepoint isolation and ingestion routing
src/nooa/viewer/otlp_store.py
Adds _ingest_one_atomic and routes direct, raw-byte batch, and dictionary batch ingestion through per-payload savepoints.
Partial-write regression coverage
tests/viewer/test_otlp_store.py
Verifies that a failed span insertion raises sqlite3.Error without inflating the failed session’s stored span count.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to ec2eb

OTLP payload ingestion now discards partial writes from failed payloads while preserving successful payloads in the surrounding batch, preventing inconsistent session and span counts. The change is ready to merge.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 2 files.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: rolling back a payload when ingestion fails partway through. The emoji adds noise but does not prevent the title from being specific and understandable.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

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.

1 participant