Skip to content

feat(operator): fire-and-forget mint sender with batch confirmation polling#94

Merged
dev-jodee merged 5 commits intomainfrom
feat/sender-fire-and-forget-deposit
Apr 10, 2026
Merged

feat(operator): fire-and-forget mint sender with batch confirmation polling#94
dev-jodee merged 5 commits intomainfrom
feat/sender-fire-and-forget-deposit

Conversation

@Huzaifa696
Copy link
Copy Markdown
Collaborator

@Huzaifa696 Huzaifa696 commented Apr 9, 2026

Summary

Decouples sendTransaction latency from the sender loop for Mint/InitializeMint transactions. Instead of blocking until each tx confirms, the sender signs and sends immediately, stores the signature, and moves on. A dedicated poll task batches all in-flight signatures into a single getSignatureStatuses call per interval.

  • Fire-and-forget send: Mint/InitializeMint only.
  • Batched confirmation: dedicated tokio task wakes on Notify, accumulates one poll interval, resolves all N signatures in one RPC call. Confirmed-success is handled entirely in the poll task; only errors and timeouts wake the sender loop.
  • Semaphore back-pressure: MAX_IN_FLIGHT = 1000 permits. select! recv arm guarded by available_permits() > 0; when exhausted, the processor channel backs up and the fetcher stops.
  • AccountNotFound now permanent: previously retried 5× with backoff (~1.5 s wasted per JIT mint lookup).
  • bench-tps setup: setup_deposit now initialises the Contra-side SPL mint upfront so the operator doesn't hit JIT init on the first deposit.

Files changed

File Change
sender/types.rs InFlightTx, InFlightQueue (Arc<Mutex<Vec>> + Notify), PollTaskResult
sender/transaction.rs spawn_fire_and_store, fire_and_store, run_poll_task, poll_in_flight; branches in handle_transaction_submission
sender/mod.rs Wires poll task; both exit paths stop poll task before drain_in_flight
sender/state.rs semaphore: Arc<Semaphore> added to SenderState
rpc_util.rs AccountNotFound → permanent error
bench-tps/ Contra-side mint init in setup; dedicated BENCH_CONTRA_RPC_URL env var

Tests

  • spawn_fire_and_store: cap exhausted returns false, permit consumed on success
  • run_poll_task: cancellation (idle + mid-sleep), closed result channel exit
  • drain_in_flight: empty queue no-op, 30 s timeout
  • InFlightQueue: push/drain roundtrip, capacity preserved across drain_all
  • is_permanent_rpc_error: Method not Found and AccountNotFound exit immediately

Impact

Deposit TPS: ~9 -> ~500
Screenshot from 2026-04-09 21-17-22

Coverage Report

Component Lines Hit Lines Total Coverage Artifact
Core 7,103 8,411 84.4% rust-unit-coverage-reports
Indexer 13,471 15,715 85.7% rust-unit-coverage-reports
Gateway 952 1,076 88.5% rust-unit-coverage-reports
Auth 541 596 90.8% rust-unit-coverage-reports
Withdraw Program - - - -
Escrow Program - - - -
E2E Integration 7,909 11,473 68.9% e2e-coverage-reports
Total 29,976 37,271 80.4%

Last updated: 2026-04-09 18:06:27 UTC by E2E Integration

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 9, 2026

Greptile Summary

This PR introduces a fire-and-forget mint sender with a dedicated batch-confirmation poll task (run_poll_task), decoupling transaction send from confirmation polling to improve throughput. It also adds a graceful-shutdown drain loop (drain_in_flight) and pre-initialises Contra mints during the bench-tps setup phase to avoid JIT initialisation overhead.

  • The comment at the cancellation-arm drain site incorrectly states "The poll task has already been cancelled" — poll_shutdown.cancel() is not called until after the loop exits, so the poll task races with drain_in_flight during shutdown.
  • DepositArgs::contra_rpc_url is bound to env = \"BENCH_RPC_URL\", which is the same env var already used by BenchArgs::rpc_url (default 8899) and WithdrawArgs::rpc_url, but with a different default value (8898), creating a confusing inconsistency.

Confidence Score: 4/5

Safe to merge after fixing the poll-task cancellation ordering in the shutdown path and the BENCH_RPC_URL env-var collision.

Two P1 findings: the poll task is not cancelled before drain_in_flight runs (incorrect comment + real race), and the BENCH_RPC_URL env var is reused with conflicting defaults. Both are actionable fixes. The core fire-and-forget architecture is sound, idempotency is preserved, and the DB recovery path handles the shutdown edge case. Tests are comprehensive.

indexer/src/operator/sender/mod.rs (shutdown ordering), bench-tps/src/args.rs (env var naming)

Vulnerabilities

No security concerns identified. The fire-and-forget path reuses the existing idempotency memo check before any send, preventing double-mint on re-delivery. The InFlightQueue mutex prevents concurrent data corruption. No new auth boundaries, secrets, or user-controlled inputs are introduced.

Important Files Changed

Filename Overview
indexer/src/operator/sender/mod.rs New drain_in_flight graceful-shutdown helper and sender main loop restructured; contains an incorrect comment and a real race between drain_in_flight and the still-running poll task.
indexer/src/operator/sender/transaction.rs Core fire-and-forget logic: fire_and_store, run_poll_task, poll_in_flight, and route_poll_results — logic is sound; multi-chunk RPC failure discards already-fetched statuses (acceptable, undocumented).
indexer/src/operator/sender/types.rs New InFlightQueue (Arc-wrapped Mutex+Vec+Notify) and InFlightTx/PollTaskResult types — design is clean and thread-safe.
bench-tps/src/args.rs New contra_rpc_url field added to DepositArgs but reuses BENCH_RPC_URL env var with a conflicting default value vs existing fields.
bench-tps/src/setup_deposit.rs Adds Contra mint pre-initialisation step (task 6b) using new contra_rpc_url parameter — logic is straightforward.
indexer/src/operator/utils/rpc_util.rs New tests for with_retry covering permanent errors (-32601, AccountNotFound) and backoff clamping — good coverage, no issues.
indexer/src/operator/utils/transaction_util.rs New mockito-based tests for check_transaction_status and sign_and_send_transaction — comprehensive, no issues.
indexer/src/operator/sender/state.rs Adds in_flight: InFlightQueue::new() to SenderState construction and test helpers; minor trailing blank line added before closing brace.

Sequence Diagram

sequenceDiagram
    participant P as Processor
    participant S as Sender Loop
    participant IFQ as InFlightQueue
    participant PT as Poll Task
    participant RPC as Solana RPC
    participant DB as Storage

    P->>S: TransactionBuilder (Mint/InitializeMint)
    S->>S: find_existing_mint_signature (idempotency)
    S->>S: handle_transaction_builder → InstructionWithSigners
    S->>RPC: sendTransaction (fire_and_store)
    RPC-->>S: Signature
    S->>IFQ: push(InFlightTx) + notify_one()

    loop Poll Task (dedicated task)
        IFQ-->>PT: notify.notified()
        PT->>PT: sleep(poll_interval_ms)
        PT->>IFQ: drain_all()
        PT->>RPC: getSignatureStatuses (batched, 256/call)
        RPC-->>PT: statuses[]
        alt Confirmed Success
            PT->>DB: TransactionStatusUpdate(Completed)
            PT->>S: PollTaskResult::ConfirmedSuccess(txn_id)
            S->>S: mint_builders.remove(txn_id)
        else On-chain Error / Timeout
            PT->>S: PollTaskResult::NeedsRouting(tx, status)
            S->>S: route_poll_results → handle_confirmation_result
        else Still Pending
            PT->>IFQ: push(tx) [poll_attempts++]
        end
    end

    note over S,PT: Graceful Shutdown
    S->>S: drain processor channel
    S->>S: drain_in_flight (poll_in_flight loop, 30s timeout)
    S->>PT: poll_shutdown.cancel()
    S->>PT: await poll_task_handle
Loading

Reviews (1): Last reviewed commit: "feat(operator): fire-and-forget mint sen..." | Re-trigger Greptile

Comment thread indexer/src/operator/sender/mod.rs Outdated
Comment thread bench-tps/src/args.rs
@Huzaifa696 Huzaifa696 changed the title feat(operator): fire-and-forget mint sender with batch confirmation p… feat(operator): fire-and-forget mint sender with batch confirmation polling Apr 9, 2026
  cargo-build-sbf bundles its own LLVM/Clang, and the litesvm-based
  integration tests vendor OpenSSL — no system packages are needed.
  The apt install was downloading ~60 MB (libclang-18-dev alone is
  28.8 MB) at ~30 KB/s on the CI runner, consistently hitting the
  20-minute job timeout before setup finished.
  Remove the .port_range((2000, 2200)) constraint added to TestValidatorGenesis
  and the unique_port_range_for_tests/find_available_port_in_range plumbing.
  Forcing all validator sockets (gossip, TPU, TVU) into a fixed 2000-2200
  range conflicted with occupied UDP ports on the CI runner, causing
  "Address already in use" failures.

  The OS kernel assigns port=0 sockets atomically — concurrent nextest
  validators never collide, so the port range isolation was solving a
  non-problem while introducing a real one.
@Huzaifa696 Huzaifa696 requested a review from dev-jodee April 9, 2026 19:20
@Huzaifa696 Huzaifa696 self-assigned this Apr 10, 2026
@dev-jodee dev-jodee merged commit 762bc33 into main Apr 10, 2026
9 checks passed
@dev-jodee dev-jodee deleted the feat/sender-fire-and-forget-deposit branch April 10, 2026 15:24
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