feat(solana-indexer): PR 3 — add traits module#4508
Conversation
Introduces the traits/ module per the PR-sequence plan. The trait surface is intentionally narrow: it covers only what the consumer components need, not the full surface of the underlying library. traits/store.rs: - Store (PostgreSQL boundary) traits/solana_client.rs: - SolanaClient (RPC boundary) We don't need a trait to represent the Yelllowstone gRPC system boundary because that can be represented by a trait bound in the (upcoming) Ingester struct implementation.
There was a problem hiding this comment.
Code Review
This pull request introduces the SolanaClient and Store traits to abstract external dependencies (Solana RPC and PostgreSQL persistence) for the solana-indexer crate, along with updating dependency configurations and refactoring recovery-flow types. Feedback on the Store trait suggests returning Result<(), StoreError> instead of () for write_dead_letter and record_lost_slot_range to ensure robust error handling for database operations.
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.
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
traits module
There was a problem hiding this comment.
Code Review
This pull request introduces the SolanaClient and Store traits to abstract external Solana RPC calls and PostgreSQL persistence for the indexer. It also refactors related types and dependencies. The feedback suggests using std::ops::RangeInclusive<u64> instead of std::ops::Range<u64> in the Store trait to avoid potential off-by-one errors when recording lost slot ranges.
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.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
This pull request has been marked as stale because it has been inactive a while. Please update this pull request or it will be automatically closed. |
| /// Record a slot checkpoint. Rejects downward writes. | ||
| async fn write_watermark(&self, slot: u64) -> Result<(), StoreError>; |
There was a problem hiding this comment.
What does "reject downward writes" mean?
There was a problem hiding this comment.
It means it should reject slot values smaller than what's already stored, so we never rewind the watermark by accident.
This is also something we're planning to enforce as database constraints.
| }; | ||
|
|
||
| /// PostgreSQL persistence. Used by Decoder, Watchdog, and FinalizationWorker. | ||
| pub trait Store { |
There was a problem hiding this comment.
Can you elaborate a bit on why making this a trait is good?
Is the intention to implement a custom persistence layer for testing purposes or something?
There was a problem hiding this comment.
I mostly wanted to follow dependency inversion so the pieces stay decoupled.
It also makes it easier to use mocks in our tests, which is a nice plus.
I am only planning on using Postgres for now, so it is really just about keeping the logic separate from the storage layer. (This applies to all the traits here, btw)
There was a problem hiding this comment.
And, to be fair, I noticed this specific trait is getting too crowded w/ methods.
I thought about splitting it into smaller sub-traits, culminating into a Store: A + B + C super- trait, but I was worried about making it more complicated than it needs to be.
I think we should refine that idea before merging this
There was a problem hiding this comment.
I guess the main reason for that is mocked tests, right? But do we really need them? Can just spawn a pg instance like it is done for DB tests?
traits moduletraits module
- openssl 0.10.75 → 0.10.80 (fixes CVE-2026-41676/78/81/98, CVE-2026-42327, CVE-2026-44662, CVE-2026-45784, CVE-2026-41677) - rand 0.8.5 → 0.8.6 (fixes GHSA-cq8v-f236-94qc)
| /// Primary promotion pass: fetch `confirmed` rows whose `slot` is at or | ||
| /// above the finalization-window threshold (`slot >= newer_than_slot`). | ||
| /// `limit` caps the batch at 256 (RPC batch size). Returns `Err` on | ||
| /// backend failure so the caller can back off rather than | ||
| /// silently stall on a dead store. | ||
| async fn get_confirmed_rows( | ||
| &self, | ||
| newer_than_slot: u64, | ||
| limit: usize, | ||
| ) -> Result<Vec<UnfinalizedRow>, StoreError>; |
There was a problem hiding this comment.
This returns the newest rows (slot >= newer_than_slot), but do we want the oldest here? A transaction takes ~32 slots to finalize, so polling anything newer just wastes RPC calls. Should the bound be slot <= threshold?
There was a problem hiding this comment.
You're correct.
We expect the finalization pass to manage the newest rows, so that method should only care for the slots that weren't handled by that.
I think it this would be better off like this:
/// Primary promotion pass: fetch `confirmed` rows whose `slot` is old enough
/// to be finalized (typically `slot <= tip - 32`) but new enough to still
/// be tracked by the RPC retention horizon.
///
/// `limit` should honor the RPC max batch size of 256.
async fn get_confirmed_rows(
&self,
max_slot: u64, // <-- renamed after `newer_than_slot`
limit: usize,
) -> Result<Vec<UnfinalizedRow>, StoreError>;While we're on this, I believe this should now use the Slot type introduced in the previous PR
There was a problem hiding this comment.
Also, I think that limit might not be a good fit here, because it's a constraint on the RPC boundary, not related the Store trait itself.
Naturally, we expect Store implementors to limit their queries, but I don't think that detail should be represented at this spot.
🤔 🤔 🤔
There was a problem hiding this comment.
Makes sense. Switched it to max_slot with slot <= max_slot and the Slot type.
On limit: I'd keep it, but change what it means. You're right that the 256 is the getSignatureStatuses cap, and that's an RPC concern, not a Store one. But the Store still shouldn't hand back thousands of confirmed rows at once, so a limit as a plain page size is still worth keeping. So only the doc changes: stop calling it the "256 RPC batch", call it a page size, and let the worker split the rows into batches of 256 when it calls getSignatureStatuses. That way, the RPC limit stays on the RPC side. Wdyt?
…tstrap Resolve conflicts from the updated PR2 base: - adopt Slot/OrderUid newtypes in channel.rs and recovery.rs - drop observe/prometheus deps (metrics.rs removed on PR2), keep solana-client - make Store/SolanaClient pub(crate) to match PR2's type visibility (a pub trait over pub(crate) types is E0446)
Flip the bound from slot >= newer_than_slot to slot <= max_slot so the promotion pass fetches rows old enough to have finalized, not the freshest ones. Reframe limit as a DB page size (the RPC 256-batch cap lives on the SolanaClient side). Per review thread.
Switch the two traits to #[async_trait] with a Send + Sync supertrait so the components can hold them as Arc<dyn Store> / Arc<dyn SolanaClient> instead of propagating generic type params (review feedback). Drops the allow(async_fn_in_trait) crate attr.
Trivy matches CVE-2020-36460 (RUSTSEC-2021-0103, data races in the crates.io model crate) by name and version against our own workspace crate crates/model (also named model, version 0.1.0), which is unrelated and not affected. It surfaces on this PR because the Cargo.lock diff vs main is large enough that Trivy attributes the baseline alert to the PR.
Description
Adds the
traits/module as laid out in the PR sequence plan, carrying over the two traits that sit on the indexer's external boundaries.The trait surface stays small on purpose: each one only covers what the consumer components actually need, not the full API of the underlying library. That keeps the abstractions thin and tied to real call sites.
Changes
traits/store.rs:Storetrait, the PostgreSQL boundary.traits/solana_client.rs:SolanaClienttrait, the Solana RPC boundary.GrpcConnectortrait bound on theIngesterstruct (coming in the next PR).How to test
cargo check -p solana-indexer.traitsmodule is exported and both traits are reachable from the crate root.This is a follow up PR to #4506