Skip to content

feat(solana-indexer) PR 7.1: wire tx-only decoder run loop#4591

Merged
squadgazzz merged 1 commit into
mainfrom
solana-indexer/PR7-dispatch
Jul 9, 2026
Merged

feat(solana-indexer) PR 7.1: wire tx-only decoder run loop#4591
squadgazzz merged 1 commit into
mainfrom
solana-indexer/PR7-dispatch

Conversation

@squadgazzz

@squadgazzz squadgazzz commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Description

With the account half of the stream gone (#4601), the decoder's run loop is transaction-only. It drains the channel from the ingester and, for each transaction, resolves the settlement and SolFlow instructions and routes them to per-program decoders. Those decoders are stubs for now: turning instruction bytes into typed events needs the settlement program's on-chain layout, which comes in a later PR.

Changes

  • Implement Decoder::run: drain the channel until the ingester drops the sender
  • Route each resolved settlement or SolFlow instruction to a stubbed per-program decoder

How to test

New unit test.

@squadgazzz squadgazzz changed the title feat(solana-indexer): wire decoder run loop, join halves, route instructions feat(solana-indexer) PR 7.1: wire decoder run loop, join halves, route instructions Jul 3, 2026
@squadgazzz squadgazzz marked this pull request as ready for review July 3, 2026 13:11
@squadgazzz squadgazzz requested a review from a team as a code owner July 3, 2026 13:11
@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Claude finished @squadgazzz's task in 3m 3s —— View job


PR Review: wire decoder run loop, join halves, route instructions

  • Read the PR diff and changed files
  • Understand surrounding decoder/ingester/watchdog context
  • Review for correctness, edge cases, style
  • Post inline comments for issues
  • Post summary

The plumbing reads cleanly — the run loop, the symmetric buffer-or-complete join, the signature guard, and the settlement/SolFlow routing are all reasonable, and the stubbed decoders match the stated scope. Two things worth a look, posted inline:

  1. Join assumes one account update per tx (decoder.rs) — the account subscription filters by owner, so a tx touching multiple program-owned accounts emits multiple updates sharing (slot, signature). They clobber each other in the map, and late-arriving ones re-buffer after the pair completes → future false TxUpdateMissing dead-letters. Latent today since the snapshot is dropped, but the singular-account model is baked in here. Worth confirming the assumption holds.
  2. Test coverage gap (tests.rs) — both completion tests send the tx first, so on_tx's account-completion arm (decoder.rs:98) is never exercised. A reverse-order test would cover it.

Nothing blocking; (1) is the one I'd resolve (even if just by confirming) before the account snapshot gets consumed.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request implements the main run loop for the Decoder struct, which joins transaction and account-update halves using a shared partials map and routes tracked instructions to stubbed per-program decoders. It also adds corresponding unit tests to verify the joining, buffering, and dropping behaviors. A critical issue was identified in the join logic: it assumes a strict 1:1 relationship between transactions and account updates, which does not hold true in Solana and can lead to memory bloat, false-positive dead letters, or lost account updates.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread crates/solana-indexer/src/indexer/decoder.rs Outdated
Comment thread crates/solana-indexer/src/indexer/decoder.rs Outdated
Comment thread crates/solana-indexer/src/indexer/decoder/tests.rs Outdated
@squadgazzz squadgazzz marked this pull request as draft July 3, 2026 15:01
@squadgazzz squadgazzz force-pushed the solana-indexer/PR7-dispatch branch 2 times, most recently from 97e3d14 to 3cb7a71 Compare July 6, 2026 10:40
@squadgazzz squadgazzz changed the title feat(solana-indexer) PR 7.1: wire decoder run loop, join halves, route instructions feat(solana-indexer) PR 7.1: wire tx-only decoder run loop Jul 6, 2026
@squadgazzz squadgazzz changed the base branch from solana-indexer/PR6-decoder to solana-indexer/PR6.2-drop-account-half July 6, 2026 10:41
@squadgazzz squadgazzz marked this pull request as ready for review July 6, 2026 14:27

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request implements the main loop (run) and routing logic (decode) in the Solana indexer's decoder, which drains the stream of transaction updates and routes relevant instructions to stubbed handlers for settlement and SolFlow programs. Unit tests were also added to verify that the decoder drains transactions until the channel is closed. No critical issues were found, so there is no feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@squadgazzz

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Claude finished @squadgazzz's task in 2m 13s —— View job


PR Review: wire tx-only decoder run loop

  • Read the PR diff and changed files
  • Understand decoder/ingester context
  • Review for correctness, edge cases, style
  • Post inline comments for issues
  • Post summary

The run loop and routing read cleanly and match the stated scope. run drains until the ingester drops the sender, and decode routes each resolved instruction to the correct per-program stub. The if program_id == settlement { … } else { … } split is sound given relevant_instructions only ever returns settlement- or SolFlow-owned instructions.

The prior join-logic concern is now moot. The earlier reviews (mine and Gemini's) flagged the one-account-per-tx assumption in the account/tx join. With the account half removed (#4601) and StreamUpdate now carrying only the Tx variant, that whole partials/on_account path is gone — nothing to resolve here.

No blocking issues. Two minor, non-actionable notes:

  • slot/signature are dropped in run (let StreamUpdate::Tx { inner, .. }). They'll be needed once the persist path lands (event identity + watermark), but that's explicitly out of scope for this PR.
  • The test asserts only is_ok(). Because the decode_* stubs are side-effect-free (tracing only), run_drains_transactions_until_the_sender_drops would still pass if run drained without ever calling decode — so it exercises draining, not routing. Fine for now given there's nothing observable to assert; worth tightening (e.g. via a spy Persistence) once the stubs produce events.
    • branch solana-indexer/PR7-dispatch

@tilacog tilacog 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.

lgtm, just commented on a potential optimization.

Comment on lines +59 to +61
while let Some(update) = self.rx.recv().await {
let StreamUpdate::Tx { inner, .. } = update;
self.decode(&inner);

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.

I'm inclined to propose this should be non-blocking, considering it will bump into database IO.

Something link that:

use tokio::{sync::mpsc, task::JoinSet};

async fn run(mut rx: mpsc::Receiver<Job>) {
    let mut set = JoinSet::new(); // holds handles to independently-running tasks

    loop {
        tokio::select! {
            // pull next job, spawn it to run on its own (not driven by this loop)
            Some(job) = rx.recv() => { set.spawn(process(job)); }

            // non-blocking check for finished tasks (doesn't advance anything itself)
            Some(_) = set.join_next(), if !set.is_empty() => {}

            // channel closed AND no tasks left in flight -> done
            else => break,
        }
    }
}

(consider pseudocode, bc I haven't checked for borrow issues, etc)

@tilacog tilacog Jul 7, 2026

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.

Just wanted to note: looking at how this turned out, the Decoder is currently little more than a dispatch layer for async tasks, which made me question whether the Ingester/Decoder split is still worth keeping.

I wouldn't undo it, though. The separation of concerns still has value in principle because it gates the complexity within each component, and the channel between them is a useful abstraction on its own. That boundary could later support multiple decoder workers, or evolve into a proper bus (NATS, for example).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

decode is a stub with no IO yet, and the blocking drain is our backpressure: when decode lags, the channel fills and blocks the ingester, which an unbounded JoinSet would remove. When it's genuinely IO-bound, I'd add a few decoder workers on the shared receiver rather than spawn per message.

.send(stream_tx(Slot(7), signature(3), settlement))
.await
.unwrap();
drop(sender);

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.

Because we drop the sender before the decoder even runs it's not clear what the test is actually verifying. Should we still see the event emitted before dropping the sender?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right, the test only proves the loop drains what's buffered and exits when the sender drops. It can't assert the event yet: Persistence is still a no-op stub and decode() drops its output until the store adapter lands, so there's nothing observable to check. I added a comment on the test marking that gap and where to assert the emitted event once persistence is wired (in #4612).

{
if instruction.program_id == self.settlement_program {
self.decode_settlement(&instruction);
} else {

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.

Should this not also be gated by an if? The new additional branch could then either have a comment explaining why this can't happen, or a log warning us if we encounter an unexpected program id.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

#4612 reworks this: instead of if settlement { ... } else { ... }, decode() now runs two explicit filters, program_id == settlement_program into decode_settlement and program_id == solflow_program into decode_solflow. So there's no more implicit "else means SolFlow".

@squadgazzz squadgazzz force-pushed the solana-indexer/PR6.2-drop-account-half branch from ffca95d to c126a11 Compare July 8, 2026 12:33
@squadgazzz squadgazzz force-pushed the solana-indexer/PR7-dispatch branch from 3cb7a71 to 546eb55 Compare July 8, 2026 12:37
@squadgazzz squadgazzz force-pushed the solana-indexer/PR6.2-drop-account-half branch from c126a11 to e902aef Compare July 9, 2026 10:28
@squadgazzz squadgazzz force-pushed the solana-indexer/PR7-dispatch branch from 546eb55 to 5ec1c32 Compare July 9, 2026 10:56
Base automatically changed from solana-indexer/PR6.2-drop-account-half to main July 9, 2026 11:01
@squadgazzz squadgazzz force-pushed the solana-indexer/PR7-dispatch branch from 5ec1c32 to d93e181 Compare July 9, 2026 11:01
@squadgazzz squadgazzz force-pushed the solana-indexer/PR7-dispatch branch from d93e181 to 2503510 Compare July 9, 2026 11:08
@squadgazzz squadgazzz enabled auto-merge July 9, 2026 11:14
@squadgazzz squadgazzz added this pull request to the merge queue Jul 9, 2026
Merged via the queue into main with commit 48fdc16 Jul 9, 2026
22 checks passed
@squadgazzz squadgazzz deleted the solana-indexer/PR7-dispatch branch July 9, 2026 11:32
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 9, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants