-
Notifications
You must be signed in to change notification settings - Fork 16
refactor(backfill): Backfill Plugin Major Refactor And Improvements #2006
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First pass on this one, will need at least another one. Have not yet looked at tests as well.
I leave general comments for cleanup and questions.
I feel, however, that we are really missing out on not using the ranged sets and instead we are using a list of ranges and manually manipulating them. I do see usages of merging ranges, this is something the ranged sets will do automatically in a more performant fashion. I guess I just do not see why we need to use a list and complicating some of the things we do, instead of just using the ranged sets. Since we are doing such a big rework, now is the time to make decisions. Something worth thinking about.
...de/backfill/src/main/java/org/hiero/block/node/backfill/client/proto/block_node_source.proto
Outdated
Show resolved
Hide resolved
block-node/backfill/src/main/java/org/hiero/block/node/backfill/client/BlockNodeClient.java
Outdated
Show resolved
Hide resolved
block-node/backfill/src/main/java/org/hiero/block/node/backfill/client/BlockNodeClient.java
Show resolved
Hide resolved
block-node/backfill/src/main/java/org/hiero/block/node/backfill/client/BlockNodeClient.java
Outdated
Show resolved
Hide resolved
block-node/backfill/src/main/java/org/hiero/block/node/backfill/BackfillConfiguration.java
Show resolved
Hide resolved
block-node/backfill/src/main/java/org/hiero/block/node/backfill/BackfillPersistenceAwaiter.java
Show resolved
Hide resolved
block-node/backfill/src/main/java/org/hiero/block/node/backfill/BackfillRunner.java
Outdated
Show resolved
Hide resolved
block-node/backfill/src/main/java/org/hiero/block/node/backfill/GapDetector.java
Show resolved
Hide resolved
...k-node/backfill/src/main/java/org/hiero/block/node/backfill/PriorityHealthBasedStrategy.java
Outdated
Show resolved
Hide resolved
...k-node/backfill/src/main/java/org/hiero/block/node/backfill/PriorityHealthBasedStrategy.java
Show resolved
Hide resolved
|
@ata-nas Thank you for reviewing my PR, really appreciate and value all your input and time effort made into reviewing it 🙏 I've addressed all your comments mostly in a positive outcome. And for your general notes suggestion:
I've ponder this up and decided that the benefits do not outweigh the effort and added complexity. The use case we need is simpler as opposed to the BlockRangeSet impls and prefer to keep it simple as a native List. |
0d1abe4 to
b5048d2
Compare
Introduce typed gaps to classify detected block gaps as HISTORICAL or LIVE_TAIL for routing to appropriate schedulers. GapDetector now returns TypedGap instances with proper boundary detection. Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
Replace single scheduler with two independent schedulers (historical and live-tail) so live blocks never wait for historical backfill. Each scheduler has bounded queue with discard-on-full semantics. Remove unused BackfillScheduler wrapper and BackfillTask status tracking. Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
Update BackfillPlugin to orchestrate two independent schedulers with dedicated executors. Add high-water mark deduplication for live-tail gaps. Add configuration for queue capacities and health penalty settings. Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
Merge gRPC client functionality into BackfillFetcher. Use configurable health penalty and backoff settings. Remove redundant BackfillGrpcClient. Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
Add @timeout annotations to all test classes (30s for integration, 5s for unit tests) to fail fast if tests hang instead of blocking indefinitely. Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
Update configuration docs with new queue capacity and health settings. Update design docs to reflect dual scheduler architecture. Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
…gic into BackfillPersistenceAwaiter class. Improved logs overall for the plugin. Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
Simplified TypedGap and GapType into nested classes of GapDetector for readability and code reduction Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
block-node/backfill/src/main/java/org/hiero/block/node/backfill/BackfillConfiguration.java
Show resolved
Hide resolved
jsync-swirlds
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
About half way through, mostly logging issues so far.
block-node/backfill/src/main/java/org/hiero/block/node/backfill/BackfillConfiguration.java
Show resolved
Hide resolved
block-node/backfill/src/main/java/org/hiero/block/node/backfill/BackfillFetcher.java
Outdated
Show resolved
Hide resolved
block-node/backfill/src/main/java/org/hiero/block/node/backfill/BackfillFetcher.java
Outdated
Show resolved
Hide resolved
block-node/backfill/src/main/java/org/hiero/block/node/backfill/BackfillFetcher.java
Outdated
Show resolved
Hide resolved
block-node/backfill/src/main/java/org/hiero/block/node/backfill/BackfillFetcher.java
Outdated
Show resolved
Hide resolved
block-node/backfill/src/main/java/org/hiero/block/node/backfill/BackfillPersistenceAwaiter.java
Outdated
Show resolved
Hide resolved
block-node/backfill/src/main/java/org/hiero/block/node/backfill/BackfillPersistenceAwaiter.java
Outdated
Show resolved
Hide resolved
block-node/backfill/src/main/java/org/hiero/block/node/backfill/BackfillPersistenceAwaiter.java
Show resolved
Hide resolved
...de/backfill/src/main/java/org/hiero/block/node/backfill/client/proto/block_node_source.proto
Outdated
Show resolved
Hide resolved
ata-nas
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My second review of the PR.
This is only reviewing tests! (Pt.1, still have a couple of test classes to go through).
This took a very long time to review thoroughly.
As a general comment, I think we will really benefit of not using any mocks, but develop simple test fixtures. Moreover, for things such as metrics, we already have an approach to create them for tests w/o any mocks.
As mentioned in my previous review, this major rework is the best time to make the best decisions.
| private static BlockUnparsed createTestBlock(long blockNumber) { | ||
| Block block = new Block(Arrays.asList(SimpleTestBlockItemBuilder.createSimpleBlockWithNumber(blockNumber))); | ||
| return BlockUtils.toBlockUnparsed(block); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SimpleTestBlockItemBuilder should have methods to create unparsed blocks directly, if I am not mistaken.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good Catch!! 💯
✅ Done!
| return new BackfillPlugin.MetricsHolder( | ||
| mock(Counter.class), // backfillGapsDetected | ||
| mock(Counter.class), // backfillFetchedBlocks | ||
| mock(Counter.class), // backfillBlocksBackfilled | ||
| mock(Counter.class), // backfillFetchErrors | ||
| mock(Counter.class), // backfillRetries | ||
| mock(LongGauge.class), // backfillStatus | ||
| mock(LongGauge.class), // backfillPendingBlocksGauge | ||
| mock(LongGauge.class)); // backfillInFlightGauge |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should not use mocks here at all. We already have the test helper which creates metrics which we can actually call to assert on. Please refer to: TestUtils#createMetrics and it's sample usages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like using Mocks for metrics for their simplicity and versatility for assertions, but if we already have a testing framework I agree we should keep it standard.
I have made the refactor.
✅ Done!
| final BackfillFetcher client = newClient(lowPriority, highPriority); | ||
|
|
||
| // Selects earliest start even if higher priority | ||
| var selection = client.selectNextChunk( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Non-blocking nit: it would be much better, in my opinion, to never use var, considered everywhere. Optional, however.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I have made the changes too.
✅ Done!
|
|
||
| @Test | ||
| @Timeout(value = 10, unit = TimeUnit.SECONDS) | ||
| @DisplayName("returns blocks on success, retries on failure, returns empty on mismatch") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: judging by the display name, these are 3 separate tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Separated tests, there is a trade-off here since I wanted to use a single test for several different assertions to make the review easier due to the amount of lines of code was already quite a lot, so tried to compact and save some lines on testing, but lets split them so they are isolated and better practice overall 👍
✅ Done!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, the reason here is that if something fails then the rest will not run and we will not know if that logic is still incorrect. If we split the tests, then we give the chance for everything to run.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed, hence why I did the proposed changes in all the occurrences.
| class SelectNextChunkTests { | ||
|
|
||
| @Test | ||
| @DisplayName("selects earliest start, breaks ties by priority, returns empty when no match") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: judging by the display name, these are 3 separate tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above, splited into multiple tests
✅ Done!
| @Test | ||
| @DisplayName("should adjust start block to available range start") | ||
| void shouldAdjustStartToAvailableRange() { | ||
| BackfillSourceConfig node = createNode("localhost", 8080, 1); | ||
| // Available from block 50, but we need from 0 | ||
| Map<BackfillSourceConfig, List<LongRange>> availability = Map.of(node, List.of(new LongRange(50, 100))); | ||
|
|
||
| Optional<NodeSelection> result = strategy.select(0, 100, availability); | ||
|
|
||
| assertTrue(result.isPresent()); | ||
| assertEquals(50, result.get().startBlock()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Q: In cases like this, is the behavior that we desire this? Should we return a partial range that is available or should we return empty, as in "what you requested is not available to be provided"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty is the correct behavior. The caller asked "give me a node that can serve blocks starting at X". If no node can serve starting at X, returning empty accurately answers "what you requested is not available". Returning a partial range starting at Y would be misleading - the caller would need to handle the gap between X and Y separately anyway. The strategy's job is to match the request exactly or report no match.
Maybe we can revisit this as a further optimization in the future, since I do see your point of 2 is better than 0.
| class PrioritySelectionTests { | ||
|
|
||
| @Test | ||
| @DisplayName("should select node with lowest priority number (highest priority)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally, this display name is a bit confusing to me. We could just say "select node with highest priority".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed display name from "lowestPriorityNumber" to "should select node with highest priority" (clearer intent)
✅ Done!
| @Test | ||
| @DisplayName("should return empty when best priority node is in backoff (no fallback to lower priority)") | ||
| void shouldReturnEmptyWhenBestPriorityInBackoff() { | ||
| // Note: The algorithm filters by best priority FIRST, then removes backoff nodes. | ||
| // It does NOT fall back to lower priority nodes if best priority is in backoff. | ||
| BackfillSourceConfig highPriority = createNode("high", 8080, 1); | ||
| BackfillSourceConfig lowPriority = createNode("low", 8081, 10); | ||
| healthProvider.setInBackoff(highPriority, true); | ||
| Map<BackfillSourceConfig, List<LongRange>> availability = new HashMap<>(); | ||
| availability.put(highPriority, List.of(new LongRange(0, 100))); | ||
| availability.put(lowPriority, List.of(new LongRange(0, 100))); | ||
|
|
||
| Optional<NodeSelection> result = strategy.select(0, 100, availability); | ||
|
|
||
| // Returns empty because only priority-1 node exists, and it's in backoff | ||
| assertTrue(result.isEmpty()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Q: Interesting! What is the user story behind this? Not returning a node config if it exists, but is not the highest priority available, because that one is in back-off?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Returning empty triggers the retry mechanism in backfillGap of BackfillRunner class. The while loop calls fetchNextChunk which, on empty selection, replans availability by re-querying all nodes. If the backoff expired, the node becomes available and we retry. This keeps retrying until the preferred node recovers or we truly exhaust all options - rather than immediately falling back to lower priority nodes. This prevents from shifting the load to a lower priority node for a hiccup or transient error that quickly recovers on a high priority node, it also allows for returning to a high priority node faster since will Retry every max backoff periods if higher priority node is available.
Priority reflects prefference, a BN with higher priority could be cheaper or faster, so we want to make sure we always exhaust all the lower priority nodes, and that we come back to the highest priority nodes as soon as possible once they become available again.
| @DisplayName("should select healthier node when priorities are equal") | ||
| void shouldSelectHealthierNodeWhenSamePriority() { | ||
| BackfillSourceConfig unhealthy = createNode("unhealthy", 8080, 1); | ||
| BackfillSourceConfig healthy = createNode("healthy", 8081, 1); | ||
| healthProvider.setHealthScore(unhealthy, 10.0); | ||
| healthProvider.setHealthScore(healthy, 1.0); | ||
| Map<BackfillSourceConfig, List<LongRange>> availability = new HashMap<>(); | ||
| availability.put(unhealthy, List.of(new LongRange(0, 100))); | ||
| availability.put(healthy, List.of(new LongRange(0, 100))); | ||
|
|
||
| Optional<NodeSelection> result = strategy.select(0, 100, availability); | ||
|
|
||
| assertTrue(result.isPresent()); | ||
| assertEquals(healthy, result.get().nodeConfig()); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Q: In this test, are we absolutely sure that we get the healthier node because of it's health, or simply because it is in the right place?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add comments explaining that selection is by health score, not insertion order
And yes I doubled checked that is the case.
✅ Done!
| @Test | ||
| @DisplayName("respects end cap and Long.MAX_VALUE") | ||
| void respectsEndCap() { | ||
| // Caps gap at endCap | ||
| var gaps = detector.findTypedGaps(List.of(new LongRange(0, 4)), 0, 10, 7); | ||
| assertEquals(1, gaps.size()); | ||
| assertEquals(new LongRange(5, 7), gaps.getFirst().range()); | ||
|
|
||
| // No trailing gap for Long.MAX_VALUE | ||
| gaps = detector.findTypedGaps(List.of(new LongRange(0, 10)), 0, 5, Long.MAX_VALUE); | ||
| assertTrue(gaps.isEmpty()); | ||
|
|
||
| // Skips gaps where start > endCap | ||
| gaps = detector.findTypedGaps(List.of(new LongRange(0, 5), new LongRange(100, 200)), 0, 10, 3); | ||
| assertTrue(gaps.isEmpty()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here, please note that as of now, the LongRange class does NOT support Long.MAX_VALUE as an end to the range! The reason being is the correct working of the size method thereof.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch on noting this! The code already handles this case - in GapDetector.findGaps() line 58, there's an explicit check endBlock != Long.MAX_VALUE that prevents creating a LongRange with MAX_VALUE as the end. The test at line 96 passes Long.MAX_VALUE as the endCap parameter (not as a LongRange end), and the guard ensures we never actually construct an invalid range.
…formatter with this file. - Applied logging performance convention, changed all logging to use built-in string interpolation within the logger. Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
…der, also improved the logging as per jsync suggestion to use the comments on the PBJ generated classes as javadocs Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
…ualityCheck for now as the current PBJ is improve logging on final failed retries to a peer Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
- Use unparsed block builder directly - Replace mock metrics with real TestUtils.createMetrics() - Replace var with explicit types - Split multi-scenario tests into focused single-behavior tests Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
| final String failedToFetchBlocksMsg = | ||
| "Failed to fetch blocks [%s->%s] from node [%s] (attempt %d/%d): %s-%s" | ||
| .formatted( | ||
| blockRange.start(), | ||
| blockRange.end(), | ||
| nodeConfig.address(), | ||
| attempt, | ||
| maxRetries, | ||
| e.getMessage(), | ||
| e.getCause()); | ||
| LOGGER.log(INFO, failedToFetchBlocksMsg, e); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because you're trying to not log stack trace, you may want to prefer the following:
| final String failedToFetchBlocksMsg = | |
| "Failed to fetch blocks [%s->%s] from node [%s] (attempt %d/%d): %s-%s" | |
| .formatted( | |
| blockRange.start(), | |
| blockRange.end(), | |
| nodeConfig.address(), | |
| attempt, | |
| maxRetries, | |
| e.getMessage(), | |
| e.getCause()); | |
| LOGGER.log(INFO, failedToFetchBlocksMsg, e); | |
| final String format = "Failed to fetch blocks [{0}->{1}] from node [{2}] (attempt {3}/{4}) due to {5}{6}."; | |
| final String cause = e.getCause() != null ? ", " + e.getCause() : ""; | |
| LOGGER.log(INFO, message, blockRange.start(), blockRange.end(), nodeConfig.address(), attempt, maxRetries, e, cause); |
This is a bit clearer, avoids formatting if not necessary, and avoids logging any stack traces.
For even more succinct logging, you might consider making this log statement DEBUG level.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Applied recommended changes! Thank you!
✅ Done!
| if (attempt == maxRetries) { | ||
| markFailure(nodeConfig); | ||
| // Only log exception stack trace on final failure to prevent log spam | ||
| LOGGER.log(TRACE, "Final failure stack trace:", e); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be INFO level, as this represents actual failure to fetch data...
| LOGGER.log(TRACE, "Final failure stack trace:", e); | |
| LOGGER.log(TRACE, "Final failure stack trace.\n", e); |
Also replaced the : with \n as it's often easier to read the logs when stack trace starts on the next line.
| * Valid range: 100ms to 300,000ms | ||
| */ | ||
| uint32 ping_timeout = 11; | ||
| } No newline at end of file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing newline at EOF.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! Thanks!
✅ Done!
| node.address(), | ||
| node.port(), | ||
| node.priority(), | ||
| tuning != null ? "custom" : "defaults"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very minor: you could also include the tuning details as follows:
| tuning != null ? "custom" : "defaults"); | |
| tuning == null ? "defaults" : tuning); |
The logger message formatting will call tuning.toString() if needed; and PBJ-generated classes all include a pretty decent toString() method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the PBJ generated toString, we lose some flexibility but we gain simplicity and readability of the codebase, seems like an acceptable trade-off. we get the needed information which is what matters in the end.
✅ Done!
…tenceAwaiter Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
…t names, use assertSame for reference checks Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
- Simplify node logging using PBJ toString() - Use log format replacement instead of .formatted() - Add missing newline at EOF (block_node_source.proto) Signed-off-by: Alfredo Gutierrez Grajeda <[email protected]>
| // We use the https://github.com/palantir/palantir-java-format formatter. | ||
| // @ConfigProperty(defaultValue = "10") – force a line break after the annotation. | ||
| // It is a non-configurable format without complete documentation | ||
| // we might consider switching to another formatter in the future. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line isn't true; we're pretty locked to the Palantir formatter for all Hiero projects.
| nodeConfig.address(), | ||
| attempt, | ||
| maxRetries, | ||
| e.getMessage(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still advocate using e in preference to e.getMessage() in log messages. Too many standard exceptions have no message (or an incomplete message) and rely on the type name (which is included in the toString() output) to make the "message" useable.
Non-blocking, however.
Summary
Refactors the
BackfillPluginfrom a monolithic ~650-line class into a modular architecture of focused, testable components.Key change: Introduces a dual-scheduler design that separates historical and live-tail backfill processing—ensuring recent gaps are never blocked by long-running historical catch-up.
Architecture Overview
BackfillPluginnow acts as an orchestrator coordinating three stages:1. Detect —
GapDetector2. Schedule —
BackfillTaskScheduler3. Execute —
BackfillRunnerPriorityHealthBasedStrategyNew Components
BackfillRunnerBackfillTaskSchedulerBackfillPersistenceAwaiterGapDetectorPriorityHealthBasedStrategyNodeSelectionStrategyConfiguration
backfill.historicalQueueCapacitybackfill.liveTailQueueCapacitybackfill.healthPenaltyPerFailurebackfill.maxBackoffMsOther Changes
BlockNodeSourceproto with optionalNodeIdandNamefields (non-breaking)Review guide (recommended order)
If you want the fastest mental model, I recommend reviewing in this order:
PR Stats
Related Issues
Fixes #1977
Fixes #1550
Fixes #1502
Fixes #1778