Skip to content

Feat/hang ports - #11

Open
S0c5 wants to merge 11 commits into
mainfrom
feat/hang-ports
Open

Feat/hang ports#11
S0c5 wants to merge 11 commits into
mainfrom
feat/hang-ports

Conversation

@S0c5

@S0c5 S0c5 commented Jul 17, 2026

Copy link
Copy Markdown
Member

No description provided.

S0c5 and others added 11 commits June 26, 2026 20:10
Co-authored-by: Cursor <cursoragent@cursor.com>
Webhooks are now persisted to a new webhook_deliveries table and delivered
by a background worker instead of inline in the monitor/sweeper. Failed
deliveries (non-2xx, including 503) are retried with configurable limits,
and a lease prevents duplicate POSTs. Adds POST /admin/retry_webhooks to
re-queue permanently failed deliveries.

Co-authored-by: Cursor <cursoragent@cursor.com>
Polygon fell thousands of blocks behind head because catch_up scanned one
block at a time, each costing a full get_block_by_number plus a chain-wide
(unfiltered) eth_getLogs call, with get_logs_with_retry treating an empty
result as a failure and retrying up to 30x.

- get_logs_with_retry now returns immediately on any Ok (including empty)
  and retries only on Err.
- eth_getLogs is filtered to the chain's allowed token addresses instead of
  scanning every Transfer event on the chain.
- When the gap between last-processed and head exceeds 10 blocks, catch_up
  switches to a batched path: one ranged, address-filtered eth_getLogs call
  per catch_up_chunk_size blocks (default 500), bisecting the range on
  provider errors (e.g. Alchemy's response-size cap) instead of retrying the
  identical request, plus concurrent eth_getBlockByNumber fetches (up to
  block_fetch_concurrency) for native transfers. Checkpoints once per chunk;
  replay is idempotent via the existing deposit-recording dedup.
- Adds catch_up_chunk_size and block_fetch_concurrency chain config options.

Covered by 7 new tests (regression, address filter, batch happy path,
bisection, bisection floor, concurrent native fetch, idempotent replay).

Co-authored-by: Cursor <cursoragent@cursor.com>
The monitor/sweeper/webhook-retry loops called rusqlite (via Db) directly
from async fns with no spawn_blocking, and the r2d2 read pool had no
connection_timeout (defaulting to 30s). During a large catch-up backlog,
this let a busy chain pin a Tokio worker thread for tens of seconds,
stalling everything else sharing that runtime.

- Add Db::blocking, which offloads a Db closure onto spawn_blocking; route
  every DB call reached from async code (monitor, sweeper, webhook,
  register) through it.
- Set an explicit 5s r2d2 connection_timeout instead of the 30s default.
- Add tokio::task::yield_now() between blocks/logs in the batch catch-up
  path so one chunk can't monopolize a worker thread across many
  non-yielding blocking calls.

Co-authored-by: Cursor <cursoragent@cursor.com>
The r2d2 read pool was built with an explicit 5s connection_timeout
(da43b62) but never set .max_size(), so it silently used r2d2's
default of 10 connections shared by every chain's monitor/sweeper/
webhook-retry loop plus inbound registrations. With multiple chains
configured, that fixed ceiling can still be exhausted under a catch-up
backlog or a flaky RPC provider, producing sustained "timed out
waiting for connection" errors even with the fast-fail timeout.

- Add Db::with_pool_size(url, max_size), with Db::new delegating to it
  using DEFAULT_READ_POOL_MAX_SIZE (10) so existing callers/tests are
  unaffected.
- Add Config::db_read_pool_size, loaded from DB_READ_POOL_SIZE (default
  20), and wire HotWalletService::new to use it.
- Add a regression test proving the configured max_size is actually
  enforced by the pool.

Co-authored-by: Cursor <cursoragent@cursor.com>
Make SQLite read-pool size configurable
Under high concurrency, the shared std::sync::Mutex<Connection> guarding
all SQLite writes was a hard bottleneck: background monitor/sweeper/
webhook-retry loops and inbound HTTP writes all serialized on the same
lock, and slow fsyncs on EFS (synchronous=FULL) meant a single blocked
writer could stall requests for tens of seconds, surfacing as 60s
timeouts upstream.

- Replace the mutex with a dedicated writer thread fed by a two-lane
  queue (interactive: try-send + timeout, fail-fast; background:
  blocking send, batched into grouped transactions with early-commit
  when an interactive command arrives).
- Route register_account, retry_erc20_deposit/retry_native_deposit,
  retry_webhook_delivery, and set_last_processed_block (interactive
  path) through the interactive lane so user-facing calls don't queue
  behind background batches.
- Set PRAGMA synchronous=NORMAL (safe under WAL) to cut fsync overhead
  on EFS-backed storage.
- Make queue capacities, batch size, and write timeout configurable via
  env vars, and log queue depth / interactive wait time when thresholds
  are exceeded.
- On writer-thread death, flip a health flag surfaced via health() and
  abort the process rather than silently wedging.
- Fix a P0 address-collision bug in register(): derivation index was a
  DefaultHasher hash of the account id (collision-prone and unstable
  across Rust versions); switch to a persisted sequential next_index
  counter allocated atomically on the writer thread
  (register_account_auto), seeded above the current max index by a new
  migration.
- Add WriteQueueError so callers/HTTP layer can distinguish
  backpressure/timeout/writer-gone from generic internal errors.

Covered by 15 new tests (pragma, batching/early-commit, timeout +
late execution, saturation/backpressure on both lanes, writer death,
priority bypass, collision fix, migration seeding, E2E saturation).

Co-authored-by: Cursor <cursoragent@cursor.com>
A restart alone didn't recover from the 2026-07 write-queue-saturation
incident because the writer never force-checkpoints: it just reopens
the same oversized -wal file and resumes fighting auto-checkpoint
attempts against it on every commit.

- checkpoint_startup: force PRAGMA wal_checkpoint(TRUNCATE) after
  migrations, before the writer starts serving traffic, so every
  restart begins from a small WAL regardless of prior growth.
- maybe_checkpoint: opportunistic, throttled PRAGMA
  wal_checkpoint(PASSIVE) after background batch commits, gated by a
  new EVM_CHECKPOINT_INTERVAL_SECS knob (default 30s) and skipped
  whenever an interactive write is waiting, so it never adds latency
  to a real request.
- Log WAL byte size and the checkpoint's (busy, log_frames,
  checkpointed_frames) at both checkpoint points for CloudWatch
  visibility going forward.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace single-writer mutex with priority-lane writer actor
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