Skip to content

fix(ingestion): a stale webhook replay must not zero out a document's active version - #46

Merged
charan-rathore merged 8 commits into
mainfrom
fix/stale-replay-active-version
Oct 19, 2026
Merged

charan-rathore merged 8 commits into
mainfrom
fix/stale-replay-active-version

Conversation

@charan-rathore

Copy link
Copy Markdown
Owner

Problem

Delivery is at-least-once, so an old webhook payload can arrive after a newer version is already active. The dedupe check only compared against the active row, so the replay was treated as an update:

  1. _prepare_version_transition deactivates the newer active version (hash B),
  2. insert_versions runs ON CONFLICT (document_id, hash_payload) DO NOTHING, so the duplicate-hash row (hash A, already present as the inactive v1) is silently skipped,
  3. the transaction commits and ingestion returns success - with zero active versions (migration 003's single-active index caps at one; nothing floors at one).

Proof (real Postgres)

New test runs the real _ingest_single_payload against a real Postgres (pgserver) with the real repositories and migrations 001-005 applied:

ingest v1 (hash A)  -> registered, active = {A}
ingest v2 (hash B)  -> registered, active = {B}
replay v1 payload   -> BEFORE: success, active = {} (zero!)   AFTER: skipped, active = {B}

Verified RED on main (AssertionError: 0 != 1, "document must keep exactly one active version") and GREEN with the fix.

Fix

A payload hash that exists on any version row - active or not - is a duplicate delivery, not an update: document_repository.get_version_by_hash looks the hash up, and the pipeline returns skipped before any deactivation. Replays are now idempotent and the newer version always stays active.

Note: documents already in the zero-active state are not repaired by this change; re-ingesting a genuinely new payload for one now fails loudly on the version-index unique constraint instead of silently corrupting further - a repair pass is a separate follow-up.

Tests

  • apps/workers/app/tests/test_ingestion_replay_realdb.py (skips cleanly without pgserver/Postgres): the stale-replay scenario above, plus a control that re-ingesting the current payload still skips.
  • apps/workers + libs/connectors suites: 35 passed.

Signed-off-by: Charan Rathore 180254320+charan-rathore@users.noreply.github.com

… active version Dedupe only checked the ACTIVE row: a replayed old payload deactivated the newer version while the duplicate-hash insert silently no-opped, committing success with zero active versions. Any version row matching the hash now makes the delivery a skipped duplicate. Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>
…ran Rathore <180254320+charan-rathore@users.noreply.github.com>

Add a method to fetch document version by hash payload.
…ctive versions Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Add regression tests for stale webhook replays in Postgres.
@vercel

vercel Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
intellirag Ready Ready Preview Oct 19, 2026 9:21am UTC
intellirag-live-own-track Ready Ready Preview Oct 19, 2026 9:21am UTC
irag-fix-0904 Ready Ready Preview Oct 19, 2026 9:21am UTC

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-19T09:40:41.219920Z 6d463f5 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dc55a1d8bb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

import psycopg
import pytest

pgserver = pytest.importorskip("pgserver", reason="pgserver not installed")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Install pgserver so the regression test actually runs

In a standard project installation this line skips the entire new module because pgserver is absent from rag-platform/pyproject.toml and no workflow installs it. Running the targeted suite here produced 32 passed, 1 skipped, so the stale-replay regression is not enforced by the repository's declared test setup; add pgserver to a test extra and run that extra in CI rather than silently skipping the only end-to-end coverage.

Useful? React with 👍 / 👎.

Comment on lines +271 to +274
existing = repository.get_version_by_hash(
document.document_id, version.hash_payload, conn=conn
)
if existing is not None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Report inactive historical hashes as duplicates

When the incoming hash belongs to an inactive version, the preceding ingestion_dedupe_checked event has already recorded is_duplicate=False because it compares only with the active row, but this new lookup then classifies the same request as a duplicate and emits skipped_no_change. Metrics or audits consuming the documented is_duplicate field therefore undercount precisely the stale replays handled by this fix; perform this lookup before emitting the dedupe event and include its result.

Useful? React with 👍 / 👎.

Comment on lines +271 to +273
existing = repository.get_version_by_hash(
document.document_id, version.hash_payload, conn=conn
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Apply the stale-hash guard during stored-payload reprocessing

The public reprocess_github_payloads_to_postgres path still performs only the active-hash comparison at lines 700-714. For a document with multiple stored payloads, list_payloads_for_document returns the newest first, so it skips the current payload and then processes an older hash by deactivating the current version; insert_versions silently ignores that already-existing hash and the operation commits with zero active versions. Route reprocessing through this any-version lookup or the shared ingestion routine as well, otherwise the same corruption fixed for live webhooks remains guaranteed when reprocessing a versioned document.

Useful? React with 👍 / 👎.

Comment on lines +271 to +273
existing = repository.get_version_by_hash(
document.document_id, version.hash_payload, conn=conn
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Serialize the hash check with the version transition

When two workers ingest the same previously unseen payload concurrently, both transactions can pass this lookup; if the first commits the new active version after the second lookup but before the second calls deactivate_active_version, the second transaction deactivates that newly inserted version and its own insert_versions then hits ON CONFLICT (document_id, hash_payload) DO NOTHING, again committing with zero active versions. Lock the document or active-version row across the lookup and transition, or make the transition atomic, so concurrent at-least-once deliveries cannot reproduce the corruption this patch is intended to prevent.

Useful? React with 👍 / 👎.

…against stale hashes Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Refactor duplicate check logic to simplify handling of active and existing versions. Remove redundant checks for stale replays and streamline the deduplication process.
@vercel

vercel Bot commented Oct 19, 2026

Copy link
Copy Markdown

Deployment failed for project intellirag-live-own-track with the following error:

Resource is limited - try again in 24 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/own-track?upgradeToPro=build-rate-limit

@vercel

vercel Bot commented Oct 19, 2026

Copy link
Copy Markdown

Deployment failed for project irag-fix-0904 with the following error:

Resource is limited - try again in 24 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/own-track?upgradeToPro=build-rate-limit

@vercel

vercel Bot commented Oct 19, 2026

Copy link
Copy Markdown

Deployment failed for project intellirag with the following error:

Resource is limited - try again in 24 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/own-track?upgradeToPro=build-rate-limit

…ff-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Added a lock_document method to handle row-locking for document transactions, preventing concurrent version conflicts.
…sion active Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Added a test to ensure reprocessing stored payloads keeps the newer version active.
…haran Rathore <180254320+charan-rathore@users.noreply.github.com>

Added test dependencies for pytest and pgserver.
…haran Rathore <180254320+charan-rathore@users.noreply.github.com>
@charan-rathore

Copy link
Copy Markdown
Owner Author

@codex review

All four findings addressed:

  1. Concurrency (P1): documents are now row-locked (SELECT ... FOR UPDATE) across the duplicate check and version transition in both the live ingest and reprocessing paths, so two workers cannot both pass the lookup and leave zero active versions.
  2. Reprocessing path (P1): reprocess_github_payloads_to_postgres now uses the same any-version hash lookup - replaying an older stored payload is a skipped duplicate, not a transition that deactivates the newer version. New real-Postgres regression covers exactly this (newest-first walk: B skips as active, A skips as stale, active stays {B}).
  3. Dedupe event (P2): the any-version lookup now runs before dedupe_checked, so is_duplicate is true for stale replays too.
  4. pgserver (P2): declared in a new test extra in pyproject.toml; the unit-tests CI job installs .[eval,test] and now also runs apps/workers/app/tests/, so the real-Postgres regressions cannot silently skip.

RED/GREEN on real Postgres: the reprocess regression fails without the guard (0 != 1 active versions) and passes with it; workers+connectors 36 passed.

Signed-off-by: Charan Rathore 180254320+charan-rathore@users.noreply.github.com

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 6d463f5768

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@charan-rathore
charan-rathore merged commit 49cb62f into main Oct 19, 2026
3 of 6 checks passed
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