Skip to content

Track superseded mempool errors separately#4385

Open
tilacog wants to merge 34 commits into
mainfrom
mempool-metric-superseded
Open

Track superseded mempool errors separately#4385
tilacog wants to merge 34 commits into
mainfrom
mempool-metric-superseded

Conversation

@tilacog
Copy link
Copy Markdown
Contributor

@tilacog tilacog commented May 5, 2026

Description

Mempools::execute() runs all configured mempools concurrently and returns the first one that succeeds. Previously, errors from mempools that lost the race were counted as real failures, even though the overall submission was successful. Dropped mempools were never recorded.

This skewed mempool counts by both:

  • counting errors alongside a successful submission, and
  • omitting counts for dropped mempools.

This PR keeps the racing behavior but changes how observation works.

Changes

Behavior

  • On first success, emit mempool_succeeded for the winner and mempool_superseded for every other configured mempool.
  • On all-failed, emit mempool_failed for each one of them.

Code specific

  • Replace select_ok with FuturesUnordered in Mempools::execute so the consumer can observe each completion (crates/driver/src/domain/mempools.rs).
  • Split observe::mempool_executed into mempool_succeeded(&SubmissionSuccess) and mempool_failed(&mempools::Error), dropping the Result<&S, &E> indirection now that each call site already knows which branch it is on. Behavior and emitted metrics are unchanged by the split.
  • Add mempool_superseded(&Mempool, winner: &Mempool, &Settlement) which increments driver_mempool_submission with result="Superseded".

How to test

Existing driver unit tests cover the race semantics; this PR does not change the externally observable submission outcome, only how observation is sequenced and labeled. To verify manually:

  1. Run the driver against a config with at least two mempools.
  2. Trigger a settlement that succeeds via the public mempool.
  3. Confirm Prometheus shows one result="Success" increment for the winner and one result="Superseded" increment for the loser; no Revert/Expired/Other from the loser.
  4. Trigger a settlement that fails on every mempool and confirm each mempool gets its own non-Superseded failure label.

Alert query update needed when deploying

Per-mempool success counts both wins and races-lost (so happy and failure paths both emit N events for N configured mempools, keeping the ratio symmetric). Superseded stays as a separate label so dashboards can still distinguish wins from race-losses per mempool.

sum by (network) (increase(driver_mempool_submission{cow_fi_environment="prod",result=~"Success|Superseded"}[2h]))
/
sum by (network) (increase(driver_mempool_submission{cow_fi_environment="prod",result!="Disabled"}[2h])) < 0.6

@tilacog tilacog force-pushed the mempool-metric-superseded branch 2 times, most recently from a798fc3 to 9905e6e Compare May 6, 2026 15:39
@tilacog

This comment was marked as outdated.

tilacog added 7 commits May 6, 2026 12:54
When `Mempools::execute()` runs mempools in parallel, errors from mempools
whose results were discarded after another mempool succeeded were still
recorded against `driver_mempool_submission`, biasing the per-mempool
success ratio with timing-dependent shadowed failures.

Replace `select_ok` with `FuturesUnordered` + manual loop so observation
runs in the consuming context. Errors that occur before another mempool
succeeds are now recorded under a new `Superseded` label via
`observe::mempool_superseded`, which also records the winning mempool in
the trace fields. Errors in the all-failed case keep their existing
labels (Revert / Expired / Other / Disabled).

Alert query update needed when deploying:

    sum by (network) (increase(driver_mempool_submission{cow_fi_environment="prod",result="Success"}[2h]))
    /
    sum by (network) (increase(driver_mempool_submission{cow_fi_environment="prod",result!~"Disabled|Superseded"}[2h])) < 0.6
`mempool_executed` took a `Result<&SubmissionSuccess, &mempools::Error>`
and re-matched the same discriminant several times to pick the log level,
metric label, and block-passed labels. Replace it with two functions,
`mempool_succeeded(&SubmissionSuccess)` and `mempool_failed(&mempools::Error)`,
so each branch is straight-line and call sites pick the correct observer
directly. Behavior and emitted metrics are unchanged.
@tilacog tilacog force-pushed the mempool-metric-superseded branch from 9905e6e to d9fb0cb Compare May 6, 2026 15:55
@cowprotocol cowprotocol deleted a comment from github-actions Bot May 6, 2026
Copy link
Copy Markdown
Contributor

@fleupold fleupold left a comment

Choose a reason for hiding this comment

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

Is there a reason you are not using the PR template for the description?

I agree with the change, however I'd like to suggest that we interpret "superseeded" events as success wrt. how you envision to change the metric. A superseeded submission should be considered a successful one.

This way we receive N (# of mempool) events in the happy case, and N events in the failure case allowing us to keep our alert metric as a ratio of successful to failed ones (otherwise failed events would be weighted N times more than successful ones).

Every loser in a mempool race is now marked Superseded, whether it
failed before the winner finished or was still in flight when the
winner landed. The old code only labelled already-failed losers as
superseded and quietly dropped ones still in flight; the
shadowed_errors accumulator that carried their errors across is gone.

Minor cleanup:

- Error::blocks_passed on the domain type returns the block delta
from submission to the terminal event for variants that carry
block-level timing. This replaces the inline match in mempool_failed.
- error_label is shared between mempool_failed and the per-attempt
counter so the Prometheus labels stay in sync.

The all-failed path also swaps the expect for an explicit
Error::Other fallback instead of panicking on the (currently
unreachable) empty-errors case.
@tilacog
Copy link
Copy Markdown
Contributor Author

tilacog commented May 8, 2026

Is there a reason you are not using the PR template for the description?

Apologies, I was in a rush and didn't account for that. I've updated the description to match the template.

I agree with the change, however I'd like to suggest that we interpret "superseeded" events as success wrt. how you envision to change the metric. A superseeded submission should be considered a successful one.

This way we receive N (# of mempool) events in the happy case, and N events in the failure case allowing us to keep our alert metric as a ratio of successful to failed ones (otherwise failed events would be weighted N times more than successful ones).

Agree. I've adjusted the suggested metric.

@tilacog tilacog marked this pull request as ready for review May 8, 2026 19:33
@tilacog tilacog requested a review from a team as a code owner May 8, 2026 19:33
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

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 refactors the mempool execution logic to use FuturesUnordered, enabling more detailed tracking of success, failure, and superseded states. It also adds a blocks_passed method to the Error enum for improved block-level timing metrics. A high-severity logic error was identified where disabled mempools are incorrectly reported as 'Superseded' if another mempool wins the race, which would artificially inflate success rate metrics. A correction was suggested to preserve the 'Disabled' status during the racing process.

Comment thread crates/driver/src/domain/mempools.rs Outdated
@tilacog tilacog enabled auto-merge May 11, 2026 11:45
Comment thread crates/driver/src/domain/mempools.rs Outdated
Comment thread crates/driver/src/domain/mempools.rs Outdated
Comment thread crates/driver/src/domain/mempools.rs Outdated
Comment thread crates/driver/src/domain/mempools.rs Outdated
Comment thread crates/driver/src/domain/mempools.rs
Comment thread crates/driver/src/domain/mempools.rs Outdated
Comment thread crates/driver/src/domain/mempools.rs Outdated
Comment thread crates/driver/src/domain/mempools.rs Outdated
tilacog and others added 7 commits May 11, 2026 09:59
Disabled is a configuration skip, not a submission failure. Split it into
its own observer so failure-rate metrics aren't polluted.
Co-authored-by: José Duarte <15343819+jmg-duarte@users.noreply.github.com>
tilacog added 2 commits May 11, 2026 10:38
Destructure the inner `&Mempool` in the future builder so pending futures
no longer borrow `enabled`, freeing it for mutation. Pop the winner with
`swap_remove(idx)` and iterate the remainder to record superseded
observations.
@tilacog tilacog requested a review from jmg-duarte May 11, 2026 13:51
Comment thread crates/driver/src/domain/mempools.rs Outdated
Comment thread crates/driver/src/infra/observe/mod.rs Outdated
Comment thread crates/driver/src/infra/observe/mod.rs Outdated
Comment thread crates/driver/src/infra/observe/mod.rs Outdated
Comment thread crates/driver/src/infra/observe/mod.rs Outdated
@tilacog tilacog requested review from MartinquaXD and jmg-duarte May 11, 2026 17:06
Comment thread crates/driver/src/infra/observe/mod.rs Outdated
Comment on lines +418 to +421
let label = err.metric_label();
metrics::get()
.mempool_submission
.with_label_values(&[mempool.to_string().as_str(), result])
.with_label_values(&[mempool.to_string().as_str(), label])
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.

nit

Suggested change
let label = err.metric_label();
metrics::get()
.mempool_submission
.with_label_values(&[mempool.to_string().as_str(), result])
.with_label_values(&[mempool.to_string().as_str(), label])
metrics::get()
.mempool_submission
.with_label_values(&[mempool.to_string().as_str(), err.metric_label()])

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.

Fixed in 501bff3

Comment thread crates/driver/src/infra/observe/mod.rs Outdated
mempools::Error::Disabled => {
tracing::debug!(
%mempool,
"sending transaction via mempool disabled",
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.

this log doesnt make a lot of sense

Suggested change
"sending transaction via mempool disabled",
"mempool disabled, not sending transaction",

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.

Reworded in 7af8c8b

@tilacog tilacog added this pull request to the merge queue May 12, 2026
@jmg-duarte jmg-duarte removed this pull request from the merge queue due to a manual request May 12, 2026
Copy link
Copy Markdown
Contributor

@MartinquaXD MartinquaXD left a comment

Choose a reason for hiding this comment

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

The idea to fix the metrics makes sense to me but the logic seems more complicated than necessary.

Wouldn't it also work to construct a hashmap mapping all mempools to a submission outcome?

  1. initialize all mempools as superseeded
  2. run new FuturesUnordered logic
  3. match on each submit result and update the hashmap accordingly

At the end all submission futures that finished will have updated their own entry in the mapping and all futures that didn't finish were cancelled because some other pool was successful first so the original superseeded label is correct.

@tilacog
Copy link
Copy Markdown
Contributor Author

tilacog commented May 12, 2026

I agree this got more complex than it should.

At the end all submission futures that finished will have updated their own entry in the mapping and all futures that didn't finish were cancelled because some other pool was successful first so the original superseeded label is correct.

If I'm reading this right, I think this arrangement still leaves us with a possible final state of [winner(1), errored(N), superseeded(M)] (out of [1+N+M] mempools).

The wrinkle is that the Disabled state still requires special handling for separating it from its sibling variants in mempool::Error, so we don't end up with skewed metrics.

I'll try to simplify the current code a bit.

tilacog added 3 commits May 12, 2026 12:42
Helper added little abstraction value — only one caller, short body.
Inlining keeps the disabled-filter rationale next to the race loop it
guards.
Comment on lines +85 to +90
impl PartialEq for Mempool {
fn eq(&self, other: &Self) -> bool {
self.config == other.config
}
}

Copy link
Copy Markdown
Contributor Author

@tilacog tilacog May 12, 2026

Choose a reason for hiding this comment

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

I hope this is correct/appropriate, because deriving PartialEq on Mempool simplifies the filtering in the race post-processing.

Config carries several fields, most importantly the Mempool's Url and Name.

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.

4 participants