Skip to content

Refactor rollback handling to track both target and diff checkpoints - #1256

Merged
DZakh merged 4 commits into
mainfrom
claude/sharp-bell-ZdsKY
May 29, 2026
Merged

Refactor rollback handling to track both target and diff checkpoints#1256
DZakh merged 4 commits into
mainfrom
claude/sharp-bell-ZdsKY

Conversation

@DZakh

@DZakh DZakh commented May 29, 2026

Copy link
Copy Markdown
Member

Summary

Refactors the rollback mechanism to pass both the target checkpoint ID and the diff checkpoint ID through the persistence layer, enabling more precise filtering of rollback-diff changes during batch writes.

Key Changes

  • Rollback type consolidation: Replaced rollbackTargetCheckpointId: option<checkpointId> with rollback: option<Persistence.rollback> throughout the codebase, where rollback contains both targetCheckpointId and diffCheckpointId

  • Entity update simplification: Removed containsRollbackDiffChange field from Internal.inMemoryStoreEntityUpdate type. This boolean flag is now computed on-the-fly by comparing checkpoint IDs against the diff checkpoint ID

  • History tracking refactor: Simplified InMemoryTable.Entity.set function by:

    • Removing shouldSaveHistory and containsRollbackDiffChange parameters
    • Always maintaining history as a list of previous latestChange values
    • Eliminating the updates helper function (inlined at call sites)
  • Batch write logic improvement: Enhanced PgStorage.writeBatch to:

    • Extract diffCheckpointId from the rollback option
    • Filter out rollback-diff changes from history table writes while preserving them in entity table
    • Determine whether to backfill history based on actual checkpoint comparisons rather than a stored flag
  • Event processing cleanup: Removed shouldSaveHistory parameter from event handler execution chain (EventProcessing.runEventHandlerOrThrow, runHandlerOrThrow, runBatchHandlersOrThrow)

  • Storage interface update: Updated Persistence.storage.writeBatch signature to accept rollback: option<Persistence.rollback> instead of rollbackTargetCheckpointId

Implementation Details

The refactoring shifts from storing metadata about rollback-diff changes to computing it dynamically. When processing updates during batch writes, the code now:

  1. Checks if latestChange checkpoint matches the diff checkpoint
  2. Checks if the first history entry matches the diff checkpoint
  3. Skips adding changes to history/entity tables if they match the diff checkpoint ID

This approach reduces state complexity while maintaining the same filtering behavior for rollback-diff changes.

https://claude.ai/code/session_015wCRQBjfLhGoJGNHiaGMQM

Summary by CodeRabbit

  • Refactor

    • Simplified event/batch processing by removing an internal history flag and streamlining rollback handling
    • Redesigned in-memory entity history and update tracking for clearer, more consistent state transitions
    • Updated persistence layer to use a structured rollback object for more robust checkpoint handling
    • Revised storage write flow to align with the new rollback shape
  • Tests

    • Updated test fixtures and mocks to reflect the new storage and rollback APIs

Review Change Stack

claude added 2 commits May 29, 2026 09:37
…y flag

Replace the sticky `containsRollbackDiffChange` boolean threaded through
every `InMemoryTable.Entity.set` call with a single `rollback` record on
`InMemoryStore`, holding both the rollback target and diff checkpoint IDs.
Whether an in-memory update originated from a rollback diff is derived at
batch-assembly time by comparing change checkpoint IDs against the
recorded `diffCheckpointId`.

`InMemoryTable.Entity.set` no longer takes `~shouldSaveHistory` or
`~containsRollbackDiffChange`. `history` is now defined as changes
strictly older than `latestChange`: a new change with the same checkpoint
overwrites `latestChange`; a newer checkpoint demotes the previous
`latestChange` into `history`. Filtering by `shouldSaveHistory` and the
rollback-diff checkpoint happens once, in `InMemoryStore.writeBatch`,
when constructing the storage-bound shape.

The rollback-diff change is written to the entity table when it is still
`latestChange`, but never to the entity history table.
…ate type

`InMemoryStore.writeBatch` now passes raw `inMemoryStoreEntityUpdate`
records (`{latestChange, history}`) straight through to storage instead
of pre-computing a filtered shape with a `containsRollbackDiffChange`
flag. `Persistence.entityUpdate` is removed; `updatedEntity.updates` is
typed directly as `array<Internal.inMemoryStoreEntityUpdate>`.

PgStorage derives the diff classification locally from `~rollback` and
the change checkpoint IDs: skip backfill when the entity was touched by
the rollback diff (latestChange at diff, or history[0] at diff after
demotion), filter diff entries out of history-table writes, and skip the
latestChange history write when it equals the diff checkpoint. Behavior
is unchanged from the previous commit.

The `InMemoryTable.Entity.updates` helper is inlined at its single call
site in `InMemoryStore.writeBatch`. ClickHouse keeps its single-storage
signature without `~rollback` — it does not need rollback awareness;
follow-up work can let it iterate full history.
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 625c3660-c526-4b03-b920-55b97c4e5f72

📥 Commits

Reviewing files that changed from the base of the PR and between 2704708 and 71bcd79.

📒 Files selected for processing (1)
  • packages/envio/src/InMemoryTable.res

📝 Walkthrough

Walkthrough

This PR removes shouldSaveHistory threading from handler contexts and event processing, and refactors rollback from a single optional checkpoint id into a structured Persistence.rollback with target and diff checkpoint ids; updates in-memory history representation, persistence writeBatch plumbing, and related tests/mocks.

Changes

Rollback Representation & Entity History Refactor

Layer / File(s) Summary
Type contracts for entity updates and rollback
packages/envio/src/Internal.res, packages/envio/src/Persistence.res
inMemoryStoreEntityUpdate now carries latestChange and history instead of containsRollbackDiffChange boolean. New Persistence.rollback record type captures both targetCheckpointId and diffCheckpointId. Storage API updated to pass structured rollback instead of single checkpoint id.
Remove shouldSaveHistory from handler context
packages/envio/src/EventProcessing.res, packages/envio/src/UserContext.res
contextParams type removes shouldSaveHistory field. Handler invocation functions—runEventHandlerOrThrow, runHandlerOrThrow, runBatchHandlersOrThrow—no longer accept or thread ~shouldSaveHistory. Entity write and delete traps and preload contexts no longer populate shouldSaveHistory in payloads.
In-memory entity update tracking with history
packages/envio/src/InMemoryTable.res, packages/envio/src/InMemoryStore.res
Entity.set removes shouldSaveHistory and containsRollbackDiffChange parameters; now uses local emptyHistory and updates history based on checkpoint ID matching. Manual entity update collection replaces Entity.updates accessor in writeBatch. Entity.updates and Entity.values functions removed.
In-memory store rollback API refactor
packages/envio/src/InMemoryStore.res
rollbackTargetCheckpointId: option<checkpointId> replaced with rollback: option<Persistence.rollback>. make signature removes ~rollbackTargetCheckpointId parameter. prepareRollbackDiff now constructs Persistence.rollback with both target and diff ids. Rollback mutations and DCS Set payloads no longer set shouldSaveHistory or containsRollbackDiffChange flags.
Batch DCS staging without shouldSaveHistory
packages/envio/src/InMemoryStore.res, packages/envio/src/GlobalState.res
setBatchDcs signature removes ~shouldSaveHistory parameter; Set payloads for contract registration no longer include it. GlobalState.ProcessEventBatch stops computing shouldSaveHistory from config and isInReorgThreshold.
Persistence layer integration with structured rollback
packages/envio/src/PgStorage.res, packages/envio/src/TestIndexerProxyStorage.res
Storage writeBatch methods accept ~rollback: option<Persistence.rollback> instead of ~rollbackTargetCheckpointId. PgStorage.writeBatch derives diffCheckpointId from rollback object and refactors change classification (history backfill vs. history table) and rollback-diff placement. TestIndexerProxyStorage.serializableEntityUpdate carries latestChange and history instead of containsRollbackDiffChange.
Test helpers and setup aligned with new signatures
scenarios/test_codegen/test/EventOrigin_test.res, scenarios/test_codegen/test/helpers/MockIndexer.res
EventOrigin test removes shouldSaveHistory: false option from PgStorage.makePersistenceFromConfig. MockIndexer removes shouldSaveHistory from entity set payloads and updates writeBatch mock signature to accept ~rollback.

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • enviodev/hyperindex#1247: Both PRs touch the InMemoryStore write/rollback pipeline; this PR refines rollback diff and history flag representation on top of changes introduced in #1247.

Suggested reviewers

  • JonoPrest
  • JasoonS
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main refactoring objective: changing rollback handling to track both target and diff checkpoints through a structured rollback record instead of a single ID.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/envio/src/InMemoryStore.res (1)

99-105: ⚡ Quick win

Keep entity-update collection behind InMemoryTable.Entity.

writeBatch now reaches into .entities and row.status directly, which leaks Entity.t internals into InMemoryStore. Restoring a tiny updates accessor/iterator would keep this refactor localized and avoid the next Entity layout change rippling into callers.

♻️ Possible shape
// packages/envio/src/InMemoryTable.res
+ let updates = (inMemTable: t) => {
+   let acc = []
+   inMemTable.entities->Utils.Dict.forEach(row =>
+     switch row.status {
+     | Updated(update) => acc->Array.push(update)
+     | Loaded => ()
+     }
+   )
+   acc
+ }

// packages/envio/src/InMemoryStore.res
-      let updates = []
-      (inMemoryStore->getInMemTable(~entityConfig)).entities->Utils.Dict.forEach(row =>
-        switch row.status {
-        | Updated(update) => updates->Array.push(update)
-        | Loaded => ()
-        }
-      )
+      let updates = inMemoryStore->getInMemTable(~entityConfig)->InMemoryTable.Entity.updates
🤖 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 `@packages/envio/src/InMemoryStore.res` around lines 99 - 105, The writeBatch
code is accessing Entity internals (.entities and row.status) directly; restore
an accessor on InMemoryTable.Entity that yields Updated entries so callers don't
depend on Entity.t layout. Add or re-enable a method like
InMemoryTable.Entity.getUpdates or an iterator on InMemoryTable.Entity that
returns only the update payloads, then change writeBatch to call
inMemoryStore->getInMemTable(~entityConfig)->Entity.getUpdates (or equivalent)
instead of iterating .entities and matching row.status; update references in
writeBatch to use that accessor and keep Entity.t internals encapsulated.
🤖 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.

Nitpick comments:
In `@packages/envio/src/InMemoryStore.res`:
- Around line 99-105: The writeBatch code is accessing Entity internals
(.entities and row.status) directly; restore an accessor on InMemoryTable.Entity
that yields Updated entries so callers don't depend on Entity.t layout. Add or
re-enable a method like InMemoryTable.Entity.getUpdates or an iterator on
InMemoryTable.Entity that returns only the update payloads, then change
writeBatch to call
inMemoryStore->getInMemTable(~entityConfig)->Entity.getUpdates (or equivalent)
instead of iterating .entities and matching row.status; update references in
writeBatch to use that accessor and keep Entity.t internals encapsulated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 10020c0c-2f28-479f-befc-0c4630847f12

📥 Commits

Reviewing files that changed from the base of the PR and between c317395 and 2704708.

📒 Files selected for processing (11)
  • packages/envio/src/EventProcessing.res
  • packages/envio/src/GlobalState.res
  • packages/envio/src/InMemoryStore.res
  • packages/envio/src/InMemoryTable.res
  • packages/envio/src/Internal.res
  • packages/envio/src/Persistence.res
  • packages/envio/src/PgStorage.res
  • packages/envio/src/TestIndexerProxyStorage.res
  • packages/envio/src/UserContext.res
  • scenarios/test_codegen/test/EventOrigin_test.res
  • scenarios/test_codegen/test/helpers/MockIndexer.res
💤 Files with no reviewable changes (3)
  • packages/envio/src/UserContext.res
  • packages/envio/src/Internal.res
  • scenarios/test_codegen/test/EventOrigin_test.res

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.

2 participants