Repository navigation
Flink 2.1: Enhance ZkLockFactory: add maxSleepTimeMs and retryPolicyName to support multiple retry policies. - #14243
Conversation
|
Cc: @Guosmilesmile |
@pvary When you have some time, Could you please help review this PR? Thank you very much! The main purpose of this change is to enhance the ZooKeeper lock factory (ZkLockFactory) by introducing configurable retry policies. Here’s a brief summary of the newly added retry mechanisms:
These policies make the retry behavior of ZkLockFactory more flexible and adaptable across different deployment environments, while maintaining backward compatibility with the existing implementation. |
Guosmilesmile
left a comment
There was a problem hiding this comment.
Thanks for the pr ! Can we also update the corresponding documentation?
| policy = ZKRetryPolicies.valueOf(retryPolicyName); | ||
| } catch (IllegalArgumentException e) { | ||
| policy = ZKRetryPolicies.EXPONENTIAL_BACKOFF; |
There was a problem hiding this comment.
Can we add some log output? Swallowing exceptions directly can leave users puzzled.
There was a problem hiding this comment.
Thank you very much for reviewing my code! I’ll make improvements based on your suggestions as soon as possible.
|
|
||
| case EXPONENTIAL_BACKOFF: | ||
| default: | ||
| return new ExponentialBackoffRetry(maxSleepTimeMs, maxRetries); |
There was a problem hiding this comment.
The maxSleepTimeMs here should be baseSleepTimeMs.
| return new RetryNTimes(maxRetries, baseSleepTimeMs); | ||
|
|
||
| case BOUNDED_EXPONENTIAL_BACKOFF: | ||
| return new BoundedExponentialBackoffRetry(baseSleepTimeMs, maxSleepTimeMs, maxRetries); |
There was a problem hiding this comment.
What would happen if baseSleepTimeMs equals maxSleepTimeMs and maxRetries is greater than 1?
There was a problem hiding this comment.
After reviewing the ExponentialBackoffRetry implementation, if baseSleepTimeMs == maxSleepTimeMs and maxRetries > 1, the retry interval calculation
sleepTimeMs = Math.min(maxSleepTimeMs, baseSleepTimeMs * 2^retryCount)
will always result in sleepTimeMs = maxSleepTimeMs.
In this case, the exponential backoff effectively degrades into a fixed-interval retry strategy.
The relevant code is as follows:
protected long getSleepTimeMs(int retryCount, long elapsedTimeMs) {
// copied from Hadoop's RetryPolicies.java
long sleepMs = (long) baseSleepTimeMs * Math.max(1, random.nextInt(1 << (retryCount + 1)));
if (sleepMs > maxSleepMs) {
log.warn("Sleep extension too large ({}). Pinning to {}", sleepMs, maxSleepMs);
sleepMs = maxSleepMs;
}
return sleepMs;
}
There was a problem hiding this comment.
Thanks , make sense.
| 1000, | ||
| 3, | ||
| 2000, | ||
| ZKRetryPolicies.EXPONENTIAL_BACKOFF.name()); |
There was a problem hiding this comment.
Can we add corresponding test cases to ensure that the chosen strategy actually works?
There was a problem hiding this comment.
Thank you for the suggestion! I’ve added some unit tests. Since these retry strategies are implemented and maintained by Apache Curator, I think it should be sufficient to verify that the configuration parameters can be correctly parsed and mapped. Does that sound reasonable to you?
Sorry for the late reply! I’ve updated the code according to your suggestions. For the documentation part, I’ve added the new parameters to |
| | `flink-maintenance.lock.zookeeper.connection-timeout-ms` | Connection timeout (ms) | `15000` | | ||
| | `flink-maintenance.lock.zookeeper.max-retries` | Max retries | `3` | | ||
| | `flink-maintenance.lock.zookeeper.base-sleep-ms` | Base sleep between retries (ms) | `3000` | | ||
| | `flink-maintenance.lock.zookeeper.max-sleep-ms` | Maximum sleep time (in milliseconds) between retries. Caps the exponential backoff delay. | `10000` | |
There was a problem hiding this comment.
Maximum sleep time (ms) between retries.
There was a problem hiding this comment.
Thank you for your review and suggestions! I have made improvements based on your feedback.
| return new RetryNTimes(maxRetries, baseSleepTimeMs); | ||
|
|
||
| case BOUNDED_EXPONENTIAL_BACKOFF: | ||
| return new BoundedExponentialBackoffRetry(baseSleepTimeMs, maxSleepTimeMs, maxRetries); |
There was a problem hiding this comment.
Thanks , make sense.
| assertThat(className).isEqualTo("ExponentialBackoffRetry"); | ||
| break; | ||
| default: | ||
| throw new IllegalStateException("Unhandled policy type: " + policy); |
There was a problem hiding this comment.
If we get a error policy name, we will use ExponentialBackoffRetry in default?
There was a problem hiding this comment.
Yes, if the policy name is invalid, we’ll use ExponentialBackoffRetry as the default.
|
@pvary Peter , If you have time, please take some time to review it , thanks ! |
| LOG.debug("ZkLockFactory already opened for lockId: {}.", lockId); | ||
| return; | ||
| } | ||
|
|
There was a problem hiding this comment.
Got it, I’ll revert this change.
There was a problem hiding this comment.
Please add back the new line. It is required after every block
|
|
||
| ZKRetryPolicies policy; | ||
| try { | ||
| policy = ZKRetryPolicies.valueOf(retryPolicyName); |
There was a problem hiding this comment.
Could we check sooner that the config is valid?
We can have an enum as an input instead of the string
There was a problem hiding this comment.
That makes sense. I’ll update the code to validate the config earlier and switch to using an enum instead of a string.
| /** Gets the Zookeeper retry policy configuration. */ | ||
| public String zkRetryPolicy() { | ||
| return confParser | ||
| .stringConf() |
There was a problem hiding this comment.
Could we use enumConfParser?
There was a problem hiding this comment.
Sure, that sounds good — I’ll update it to use enumConfParser.
| private final int baseSleepTimeMs; | ||
| private final int maxRetries; | ||
| private final String retryPolicyName; | ||
| private volatile ZKRetryPolicies retryPolicy; |
There was a problem hiding this comment.
volatile is unnecessary because retryPolicy is immutable after construction.
There’s no concurrent modification or visibility issue, so final would be more appropriate.
| } | ||
|
|
||
| @Test | ||
| void testAllRetryPoliciesCreationAndType() { |
There was a problem hiding this comment.
Something like this?
private static Stream<Arguments> retryPolicyProvider() {
return Stream.of(
Arguments.of(ZKRetryPolicies.ONE_TIME, RetryOneTime.class),
Arguments.of(ZKRetryPolicies.N_TIME, RetryNTimes.class),
Arguments.of(ZKRetryPolicies.EXPONENTIAL_BACKOFF, ExponentialBackoffRetry.class),
Arguments.of(
ZKRetryPolicies.BOUNDED_EXPONENTIAL_BACKOFF, BoundedExponentialBackoffRetry.class),
Arguments.of(ZKRetryPolicies.UNTIL_ELAPSED, RetryUntilElapsed.class));
}
@ParameterizedTest
@MethodSource("retryPolicyProvider")
void testRetryPolicyCreationAndType(
ZKRetryPolicies policy, Class<? extends RetryPolicy> expectedClass) {
ZkLockFactory factory =
new ZkLockFactory("localhost:2181", "test", 3000, 3000, 1000, 3, 2000, policy);
RetryPolicy retryPolicy = factory.createRetryPolicy();
assertThat(retryPolicy)
.as("RetryPolicy should not be null for policy %s", policy)
.isNotNull()
.as("Expected %s for policy %s", expectedClass.getSimpleName(), policy)
.isInstanceOf(expectedClass);
}
There was a problem hiding this comment.
Yes, exactly — that looks good to me.
I’ll update the code accordingly.
| private transient RetryPolicy retryPolicy; | ||
| private final int maxSleepTimeMs; |
There was a problem hiding this comment.
nit: Change the order please, so group the final and the transient attributes
There was a problem hiding this comment.
Thanks, updated the field order so that all final configuration fields are grouped together, followed by the transient runtime fields.
| if (retryPolicy == null) { | ||
| LOG.debug( | ||
| "RetryPolicy was null after deserialization. Recreating from enum: {}", zkRetryPolicy); | ||
| retryPolicy = createRetryPolicy(zkRetryPolicy); | ||
| } |
There was a problem hiding this comment.
This will effectively be always null. And called only once, since the open is called only once.
I think the previous version was better, like:
this.client =
CuratorFrameworkFactory.builder()
.connectString(connectString)
.sessionTimeoutMs(sessionTimeoutMs)
.connectionTimeoutMs(connectionTimeoutMs)
.retryPolicy(new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries))
.retryPolicy(createPolicy())
.build();
There was a problem hiding this comment.
Good point — you’re right, the retryPolicy field isn’t reused and would always be null before open().
I’ll simplify it by removing the field and directly calling createRetryPolicy() in the builder.
| } | ||
|
|
||
| @VisibleForTesting | ||
| RetryPolicy createRetryPolicy(ZKRetryPolicies policy) { |
There was a problem hiding this comment.
This parametrization is strange.
Either:
- Use a static method and add all of the required inputs
- Use a non-static method, then don't add parameters which are already available in an attribute
There was a problem hiding this comment.
Thanks — agreed, the current method doesn’t need extra parameters since all required values are already stored in the instance.
I’ll keep it as a non-static method that directly uses the existing attributes.
| int maxSleepTimeMs, | ||
| ZKRetryPolicies retryPolicy) { |
There was a problem hiding this comment.
not that important, but maybe follow the same order as the attributes, and the value settings? 😄
There was a problem hiding this comment.
Thanks, makes sense. I’ve reordered the parameters and assignments to match the field order for better readability.
pvary
left a comment
There was a problem hiding this comment.
Small nits, and then +1
Pending tests
Guosmilesmile
left a comment
There was a problem hiding this comment.
LGTM, wait the ci fail fix.
|
@slfan1989: Do you happen to know why this commit is Are you the one who set this verification request? Are you ok with me merging this PR? |
@pvary Thank you for your question! I believe the submission was marked as |
8b1ae0f to
398f5a6
Compare
cc94322 to
3d6af71
Compare
…ame to support multiple retry policies.
3d6af71 to
38eb18b
Compare
|
The local GPG has been configured, I rebased the code and force-pushed it, and we can now see the "verified" badge. |
|
Merged to main. @slfan1989: Please backport the change to the other Flink versions. Please in the backport PR mention if the backport was clean, or you needed to modify anything, so we can review accordingly. Thanks! |
@pvary @Guosmilesmile Thank you very much for helping review the code! I will backport this PR to Flink 2.0 and Flink 1.20. |
…port multiple retry policies. (apache#14243)
…ry to support multiple retry policies. (apache#14389) backports apache#14243
…port multiple retry policies. (apache#14243)
…ry to support multiple retry policies. (apache#14389) backports apache#14243



Summary
This PR enhances ZkLockFactory by introducing two new configuration options:
Previously,
ZkLockFactoryonly supported theEXPONENTIAL_BACKOFFretry strategy. With this change, users can now configure different retry policies via retryPolicyName (e.g.,ONE_TIME,N_TIME,BOUNDED_EXPONENTIAL_BACKOFF,UNTIL_ELAPSED).Changes
maxSleepTimeMsparameter to allow bounded exponential backoff and until-elapsed policies.retryPolicyNameparameter to select the retry strategy dynamically.retryPolicyNameis provided, it defaults toEXPONENTIAL_BACKOFF.Motivation
This improves flexibility and robustness of
ZkLockFactoryby giving users fine-grained control over retry behavior, rather than being limited to a single exponential backoff policy.