Core: Make REST scan planning poll retries and backoff configurable - #17846
Core: Make REST scan planning poll retries and backoff configurable#17846Bartekszost wants to merge 3 commits into
Conversation
Allow catalogs to set rest-scan-planning.poll-num-retries so long-running remote plans are not stopped by the default retry count. Generated-by: Cursor
There was a problem hiding this comment.
Pull request overview
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Adds a new REST catalog property to make async REST scan planning poll retry count configurable, replacing a previously hardcoded retry limit and expanding tests around async planning behavior.
Changes:
- Introduce
rest-scan-planning.poll-num-retries(default 10) and validate it is non-negative. - Use the configured retry value in
RESTTableScanpolling and include it in timeout diagnostics. - Add unit tests covering configured retry behavior, success path, and invalid configuration.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| core/src/main/java/org/apache/iceberg/rest/RESTCatalogProperties.java | Adds new catalog property constants for configurable poll retry count. |
| core/src/main/java/org/apache/iceberg/rest/RESTTableScan.java | Reads/validates the new property, uses it in the poll loop, and improves timeout messaging. |
| core/src/test/java/org/apache/iceberg/rest/TestRESTScanPlanning.java | Adds tests for honoring retry config, custom retries, and rejecting invalid values. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| catalogWithAdapter.catalog.initialize( | ||
| "test-custom-retries", | ||
| ImmutableMap.of( | ||
| CatalogProperties.FILE_IO_IMPL, | ||
| "org.apache.iceberg.inmemory.InMemoryFileIO", | ||
| RESTCatalogProperties.SCAN_PLANNING_MODE, | ||
| RESTCatalogProperties.ScanPlanningMode.SERVER.modeName(), | ||
| RESTCatalogProperties.REST_SCAN_PLANNING_POLL_NUM_RETRIES, | ||
| "10")); | ||
|
|
||
| RESTTable table = restTableFor(catalogWithAdapter.catalog, "custom_retries_success"); | ||
| setParserContext(table); | ||
| assertThat(table.newScan().planFiles()).hasSize(1); |
There was a problem hiding this comment.
Good catch. The success path was using the default (10), so it would not fail if the property were ignored. Updated it to a non-default value (25), matching the timeout test which uses 30000 rather than the 5-minute default.
| // With 0 retries and a server that never completes, planFiles should fail after one attempt | ||
| assertThatThrownBy(scan::planFiles) | ||
| .isInstanceOf(RemotePlanTimeoutException.class) | ||
| .hasMessageContaining("did not complete within configured limits"); |
| "rest-scan-planning.poll-timeout-ms"; | ||
| public static final long REST_SCAN_PLANNING_POLL_TIMEOUT_MS_DEFAULT = | ||
| TimeUnit.MINUTES.toMillis(5); | ||
|
|
There was a problem hiding this comment.
Added a short comment on the constant: this is extra poll attempts after the first fetch, must be >= 0, and 0 means a single fetch with no retries.
| private static final long MIN_SLEEP_MS = 1000; // Initial delay | ||
| private static final long MAX_SLEEP_MS = 60 * 1000; // Max backoff delay (1 minute) | ||
| private static final int MAX_RETRIES = 10; // Max number of poll retries | ||
| private static final double SCALE_FACTOR = 2.0; // Exponential scale factor |
There was a problem hiding this comment.
Thinking further, at this point, wouldn't it make sense to make the other 3 retry parameters configurable as well? 😄
There was a problem hiding this comment.
Good call — the remaining backoff knobs are now catalog properties as well (poll-min-wait-ms, poll-max-wait-ms, poll-scale-factor), with the same defaults as the old constants, validation, tests, and docs.
Expose min wait, max wait, and scale factor alongside poll retries so long-running remote plans can tune the full backoff policy, not only the retry count. Generated-by: Cursor
| // With 0 retries and a server that never completes, planFiles should fail after one attempt | ||
| assertThatThrownBy(scan::planFiles) | ||
| .isInstanceOf(RemotePlanTimeoutException.class) | ||
| .hasMessageContaining("did not complete within configured limits"); |
There was a problem hiding this comment.
The assertion .hasMessageContaining("did not complete within configured limits") does not verify that the configured retry count appears in the RemotePlanTimeoutException message. The PR specifically advertises that the configured value is surfaced in the exception for diagnosability (format: "(timeout=%d ms, maxRetries=%d)"); with poll-num-retries=0 configured, adding .hasMessageContaining("maxRetries=0") would guard against regression in that interpolation.
There was a problem hiding this comment.
Good catch, asyncPlanningRespectsConfigurablePollRetries now also asserts maxRetries=0 in the RemotePlanTimeoutException message.
Lock the RemotePlanTimeoutException message to include maxRetries so a dropped interpolation would fail the test. Generated-by: Cursor
Summary
RESTTableScan, so deployments that support longer-running server-side scan plans can raise the previous hardcoded limits (see also Core : Make REST scan planning poll timeout configurable #15863)rest-scan-planning.poll-num-retriesbounds extra poll attempts after the first fetch (default remains 10;0means a single fetch with no retries)rest-scan-planning.poll-min-wait-ms(default 1s),rest-scan-planning.poll-max-wait-ms(default 1 min), andrest-scan-planning.poll-scale-factor(default 2.0) control exponential backoff between pollsRemotePlanTimeoutExceptionmessagerest-scan-planning.poll-timeout-ms) in catalog propertiesTest plan
./gradlew :iceberg-core:test --tests org.apache.iceberg.rest.TestRESTScanPlanning— passed (existing cases plus the new ones below; 0 failures)asyncPlanningRespectsConfigurablePollRetries:poll-num-retries=0against a server that never completes; poll loop stops after one fetchasyncPlanningSucceedsWithCustomRetries: non-defaultpoll-num-retries=25; async planning completesasyncPlanningRejectsInvalidRetries: negative retries rejected withIllegalArgumentExceptionasyncPlanningSucceedsWithCustomBackoff: non-default min wait, max wait, and scale factor; async planning completesasyncPlanningRejectsInvalidPollBackoff: invalid min wait, max wait, and scale factor rejectedasyncPlanningRejectsMinWaitGreaterThanMaxWait: min wait > max wait rejectedAI Disclosure