Repository navigation
Add partition-route and routing-miss counters - #111
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The required metrics design documentation was not updated.
1 open finding
What changed in this PR
Adds routing observability without changing routing behavior.
Changes:
- Adds partition-route and routing-miss counters.
- Propagates route classification through assignment and demotion.
- Tests metric classification and reconciliation.
| File | Description |
|---|---|
pkg/wgo/client.go |
Records routing outcomes. |
pkg/wgo/client_test.go |
Tests metric reconciliation. |
pkg/wgo/demoter.go |
Preserves route classification. |
pkg/wgo/demoter_test.go |
Tests classification preservation. |
pkg/wgo/metrics.go |
Defines the new counters. |
pkg/wgo/metrics_test.go |
Tests metric registration and labels. |
pkg/wgo/partition_assignment.go |
Classifies assignment outcomes. |
pkg/wgo/partition_assignment_test.go |
Tests outcome classification. |
pkg/wgo/route_records_test.go |
Tests route and miss counting. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
aldernero
left a comment
There was a problem hiding this comment.
Three things on the new routing counters. Routing itself looks right; these are about records being counted under the wrong label, plus one test gap.
| return nil | ||
| return nil, routeMissEmptyPool | ||
| } | ||
| if _, topicKnown := s.knownTopics[topic]; !topicKnown { |
There was a problem hiding this comment.
A topic whose partitions all have Leader<0 never reaches knownTopics. In buildLeadersAndTopicIDs both kept and excluded stay 0 for it, so it goes into neither leaders nor topicsWithNoLiveLeader, even though noLeader and partitionCounts[T] are filled in.
Example: Metadata returns T with partitions 0..3, all Leader=-1. A record for T/9 misses noLeader, then misses knownTopics, and comes back as routeMissUnknownTopic. So warpstream_routing_misses_total{reason="unknown_topic"} goes up for a topic the snapshot knows, when it should be partition_out_of_range.
Routing is unaffected, since it returns nil either way, but the label is wrong. Building knownTopics from partitionCounts, or adding the topics from noLeader, would fix it.
| type routingMissReason int8 | ||
|
|
||
| const ( | ||
| routingMissEmptyPool routingMissReason = iota |
There was a problem hiding this comment.
The zero value of routingMissReason is routingMissEmptyPool. If a future rejectedTopicPartitionRecords{...} literal, or a new rejection path, leaves miss unset, those records get counted as reason="empty_pool", which sends operators looking for an empty agent pool that doesn't exist.
routeOutcome already keeps its zero value for "unclassified". Doing the same here, with routingMissOther as the iota zero value, means a forgotten field shows up as other instead.
There was a problem hiding this comment.
routingMissOther is now the zero value, so an unset miss shows up as other.
| m.partitionRoutes[routeSourceStandIn].Add(3) | ||
| m.routingMisses[routingMissPartitionOutOfRange].Inc() | ||
| require.NoError(t, testutil.GatherAndCompare(reg, strings.NewReader(` | ||
| # HELP warpstream_partition_routes_total `+routeHelp(t, reg, "warpstream_partition_routes_total")+` |
There was a problem hiding this comment.
routeHelp reads the expected HELP string back from the registry under test, so the HELP lines in this GatherAndCompare always match. If someone empties or garbles the help text for either counter, this test still passes. I'd put the literal help strings here so the test checks them.
aldernero
left a comment
There was a problem hiding this comment.
Two questions about what these counters are meant to show. Neither is a correctness issue.
| } | ||
|
|
||
| return candidates | ||
| return candidates, route |
There was a problem hiding this comment.
Question: what is source meant to answer? Right now, when the Demoter skips a demoted leader and the record goes to a healthy alternate, it's still counted as source="leader", because route comes from the inner lookup. The help text says so and the "a demoted leader replaced by an alternate keeps the classification" test locks it in, so this looks deliberate.
The side effect is that during a demotion wave, leader + stand_in still looks like ~100% leader routing while most records actually go to alternates. If the counter is meant to show how the partition was resolved, this is fine. If it's also meant to show how often we route away from the named leader, it can't do that yet. That would take a third source (e.g. demoted_alternate), or a pointer to another metric that covers it. Which did you have in mind?
There was a problem hiding this comment.
source describes how the assignment strategy resolved the partition: leader means the named leader was available in the snapshot; stand_in means its leader entry was missing and the strategy picked a live agent.
The Demoter applies health policy to those candidates and may select an alternate. We preserve the original classification so this counter continues to measure metadata stand-in usage. Each record is counted once at initial routing.
You're right that it doesn't show how many records the Demoter redirects. I'd address that in a follow-up PR with a separate selection="original|alternate" label, preserving the leader-versus-stand-in distinction. The existing demotion and probe metrics provide context but don't count redirected records. I reworded the PR table to clarify the current meaning.
|
|
||
| // candidatesOf looks up candidates with the strategy's classification when it | ||
| // reports one. | ||
| func candidatesOf(s PartitionAssignmentStrategy, topic string, partition int32, maxCandidates int) ([]Agent, routeOutcome) { |
There was a problem hiding this comment.
Question: is the custom-strategy case reachable? WarpstreamClient always wires NewDemoter(NewLazyPartitionAssignmentStrategy(pool.Strategy), ...) (client.go:160-161). pool.Strategy always returns a *DefaultPartitionAssignmentStrategy, and Config has no option for a custom one. As far as I can tell, only test mocks reach routeUnclassified, this type assertion's fallback, and reason="other", so other is always 0 in production.
Are you planning to expose a custom strategy later? If not, it might be simpler to drop the routeClassifier assertion (it runs on every Demoter and Hedger lookup) and the other label, or at least change the help text so it doesn't describe a custom PartitionAssignmentStrategy users can't configure.
There was a problem hiding this comment.
You're right: WarpstreamClient always uses the default strategy, and there's no option to change it. I kept the optional classifier to preserve support for arbitrary PartitionAssignmentStrategy implementations in the exported Demoter, Hedger and lazy-strategy components, and kept other as the fallback for an unclassified miss.
I changed the help text and PR description so they no longer imply that custom strategies are configurable on the client. other now describes a lookup that found no agent without a classified reason; it isn't expected with the default strategy.
terryxsun
left a comment
There was a problem hiding this comment.
Looks reasonable to me, approving so we can include this in the next Mimir update.

Summary
wgo now routes a record to a live agent when its partition's leader is missing from metadata (#88, #96), and rejects an out-of-range partition (#107). The existing metrics cannot show how often either happens. This PR adds two counters so we can measure the stand-in fallback and see which routing gaps it still does not cover.
It does not change routing, hedging, timing or retry behaviour.
warpstream_partition_routes_totalsource:leader,stand_inleader(the named leader was in the snapshot) orstand_in(its leader entry was missing, so a live agent was picked). This measures stand-in use from Metadata, not the agent finally chosen.warpstream_routing_misses_totalreason:empty_pool,unknown_topic,no_leader,partition_out_of_range,other(no reason set, not expected with the default strategy)Things to know
stand_inmeans the leader entry was missing for a known topic, so it covers every such hole, not only an excluded leader.leaderorstand_inlabel.PartitionAssignmentStrategycannot report a source, so its records are not counted as routes and its misses count asother.warpstream_produce_records_totalequalsproduce_records_rejected_total{reason="record_too_large"}plus both routes plus all misses, and the sum of the misses equalsproduce_records_rejected_total{reason="no_agent_assigned"}. A test checks this. A custom strategy's successful routes are inproduce_records_totalbut in neither routes nor misses, so the equality does not hold for it.unknown_topic. A separatetopic_errorreason is not included.