Skip to content

Flink 2.1: Enhance ZkLockFactory: add maxSleepTimeMs and retryPolicyName to support multiple retry policies. - #14243

Merged
pvary merged 1 commit into
apache:mainfrom
slfan1989:zklock-retry-policy-enhancement
Oct 21, 2025
Merged

pvary merged 1 commit into
apache:mainfrom
slfan1989:zklock-retry-policy-enhancement

Conversation

@slfan1989

@slfan1989 slfan1989 commented Oct 3, 2025 •

Copy link
Copy Markdown
Contributor

Summary

This PR enhances ZkLockFactory by introducing two new configuration options:

  • maxSleepTimeMs
  • retryPolicyName

Previously, ZkLockFactory only supported the EXPONENTIAL_BACKOFF retry 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

  • Added maxSleepTimeMs parameter to allow bounded exponential backoff and until-elapsed policies.
  • Added retryPolicyName parameter to select the retry strategy dynamically.
  • Refactored retry policy creation logic into a switch/factory method for better extensibility.
  • Maintained backward compatibility: if no retryPolicyName is provided, it defaults to EXPONENTIAL_BACKOFF.

Motivation

This improves flexibility and robustness of ZkLockFactory by giving users fine-grained control over retry behavior, rather than being limited to a single exponential backoff policy.

@github-actions github-actions Bot added the flink label Oct 3, 2025
@slfan1989
slfan1989 marked this pull request as ready for review October 3, 2025 03:28
@slfan1989

Copy link
Copy Markdown
Contributor Author

@pvary @mxm A minor enhancement that improves ZkLockFactory by adding support for all native ZooKeeper retry policies.

Could you please help review this PR? Thank you very much!

@pvary

pvary commented Oct 3, 2025

Copy link
Copy Markdown
Contributor

Cc: @Guosmilesmile

@slfan1989

Copy link
Copy Markdown
Contributor Author

@pvary @mxm A minor enhancement that improves ZkLockFactory by adding support for all native ZooKeeper retry policies.

Could you please help review this PR? Thank you very much!

@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:

  • ONE_TIME — Retries only once after the first failure; a simple and fast-failing strategy.
  • N_TIME — Keeps retrying until the maximum number of retries is reached.
  • BOUNDED_EXPONENTIAL_BACKOFF — Similar to exponential backoff, but capped by a maximum sleep time (maxSleepTimeMs).
  • UNTIL_ELAPSED — Keeps retrying until a specified total elapsed time limit is reached, then stops.

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 Guosmilesmile left a comment

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.

Thanks for the pr ! Can we also update the corresponding documentation?

Comment on lines +248 to +250
policy = ZKRetryPolicies.valueOf(retryPolicyName);
} catch (IllegalArgumentException e) {
policy = ZKRetryPolicies.EXPONENTIAL_BACKOFF;

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.

Can we add some log output? Swallowing exceptions directly can leave users puzzled.

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.

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);

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.

The maxSleepTimeMs here should be baseSleepTimeMs.

return new RetryNTimes(maxRetries, baseSleepTimeMs);

case BOUNDED_EXPONENTIAL_BACKOFF:
return new BoundedExponentialBackoffRetry(baseSleepTimeMs, maxSleepTimeMs, maxRetries);

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.

What would happen if baseSleepTimeMs equals maxSleepTimeMs and maxRetries is greater than 1?

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.

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:

https://github.com/apache/curator/blob/3bc3ea1904c62b7e3e0b94a079a13fd88790df1b/curator-client/src/main/java/org/apache/curator/retry/ExponentialBackoffRetry.java#L65-L73

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;
}

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.

Thanks , make sense.

1000,
3,
2000,
ZKRetryPolicies.EXPONENTIAL_BACKOFF.name());

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.

Can we add corresponding test cases to ensure that the chosen strategy actually works?

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.

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?

@github-actions github-actions Bot added the docs label Oct 14, 2025
@slfan1989

Copy link
Copy Markdown
Contributor Author

Thanks for the pr ! Can we also update the corresponding documentation?

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.md. If you have some time, I’d really appreciate it if you could take a look. Thanks a lot!

Comment thread docs/docs/flink-maintenance.md Outdated
| `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` |

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.

Maximum sleep time (ms) between retries.

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.

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);

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.

Thanks , make sense.

assertThat(className).isEqualTo("ExponentialBackoffRetry");
break;
default:
throw new IllegalStateException("Unhandled policy type: " + policy);

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.

If we get a error policy name, we will use ExponentialBackoffRetry in default?

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.

Yes, if the policy name is invalid, we’ll use ExponentialBackoffRetry as the default.

@Guosmilesmile

Copy link
Copy Markdown
Contributor

@pvary Peter , If you have time, please take some time to review it , thanks !

LOG.debug("ZkLockFactory already opened for lockId: {}.", lockId);
return;
}

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: unnecessary change

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.

Got it, I’ll revert this change.

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.

Please add back the new line. It is required after every block


ZKRetryPolicies policy;
try {
policy = ZKRetryPolicies.valueOf(retryPolicyName);

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.

Could we check sooner that the config is valid?

We can have an enum as an input instead of the string

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.

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()

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.

Could we use enumConfParser?

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.

Sure, that sounds good — I’ll update it to use enumConfParser.

@github-actions github-actions Bot added the INFRA label Oct 20, 2025
private final int baseSleepTimeMs;
private final int maxRetries;
private final String retryPolicyName;
private volatile ZKRetryPolicies retryPolicy;

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.

Why is this volatile?

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.

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() {

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.

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);
  }

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.

Yes, exactly — that looks good to me.
I’ll update the code accordingly.

Comment on lines +51 to +52
private transient RetryPolicy retryPolicy;
private final int maxSleepTimeMs;

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: Change the order please, so group the final and the transient attributes

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.

Thanks, updated the field order so that all final configuration fields are grouped together, followed by the transient runtime fields.

Comment on lines +117 to +121
if (retryPolicy == null) {
LOG.debug(
"RetryPolicy was null after deserialization. Recreating from enum: {}", zkRetryPolicy);
retryPolicy = createRetryPolicy(zkRetryPolicy);
}

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 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();

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.

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) {

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 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

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.

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.

Comment on lines +76 to +77
int maxSleepTimeMs,
ZKRetryPolicies retryPolicy) {

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.

not that important, but maybe follow the same order as the attributes, and the value settings? 😄

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.

Thanks, makes sense. I’ve reordered the parameters and assignments to match the field order for better readability.

@pvary pvary left a comment

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.

Small nits, and then +1
Pending tests

@Guosmilesmile Guosmilesmile left a comment

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.

LGTM, wait the ci fail fix.

@pvary

pvary commented Oct 21, 2025

Copy link
Copy Markdown
Contributor

@slfan1989: Do you happen to know why this commit is Unverified?
Screenshot 2025-10-21 at 12 26 32

The description shows this:
Screenshot 2025-10-21 at 12 26 40

Are you the one who set this verification request? Are you ok with me merging this PR?

@slfan1989

Copy link
Copy Markdown
Contributor Author

@slfan1989: Do you happen to know why this commit is Unverified? Screenshot 2025-10-21 at 12 26 32

The description shows this: Screenshot 2025-10-21 at 12 26 40

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 unverified because I haven't configured GPG verification after switching to a new computer. You can merge this PR, and I greatly appreciate it. I will complete the GPG setup as soon as possible to ensure future submissions are verified. Thank you for your understanding!

@slfan1989
slfan1989 force-pushed the zklock-retry-policy-enhancement branch from 8b1ae0f to 398f5a6 Compare October 21, 2025 10:51
@github-actions github-actions Bot added the hive label Oct 21, 2025
@slfan1989
slfan1989 force-pushed the zklock-retry-policy-enhancement branch from cc94322 to 3d6af71 Compare October 21, 2025 10:54
@slfan1989

Copy link
Copy Markdown
Contributor Author

I’ll disable Vigilant Mode first to avoid any misunderstandings.

image

@slfan1989
slfan1989 force-pushed the zklock-retry-policy-enhancement branch from 3d6af71 to 38eb18b Compare October 21, 2025 11:44
@slfan1989

Copy link
Copy Markdown
Contributor Author

The local GPG has been configured, I rebased the code and force-pushed it, and we can now see the "verified" badge.

@pvary
pvary merged commit 17cb11b into apache:main Oct 21, 2025
43 checks passed
@pvary

pvary commented Oct 21, 2025

Copy link
Copy Markdown
Contributor

Merged to main.
Thanks for the PR @slfan1989 and @Guosmilesmile for the review!

@slfan1989: Please backport the change to the other Flink versions.
This could be done with a command like this:

g diff HEAD^ flink/v2.1 |sed "s/v2.1/v2.0/g">/tmp/patch;g apply -3 -p1 /tmp/patch

Please in the backport PR mention if the backport was clean, or you needed to modify anything, so we can review accordingly.

Thanks!

@slfan1989

Copy link
Copy Markdown
Contributor Author

Merged to main. Thanks for the PR @slfan1989 and @Guosmilesmile for the review!

@slfan1989: Please backport the change to the other Flink versions. This could be done with a command like this:

g diff HEAD^ flink/v2.1 |sed "s/v2.1/v2.0/g">/tmp/patch;g apply -3 -p1 /tmp/patch

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.

pvary pushed a commit that referenced this pull request Oct 22, 2025
…ry to support multiple retry policies. (#14389)

backports #14243
thomaschow pushed a commit to thomaschow/iceberg that referenced this pull request Jan 19, 2026
thomaschow pushed a commit to thomaschow/iceberg that referenced this pull request Jan 19, 2026
talatuyarer pushed a commit to talatuyarer/iceberg that referenced this pull request Apr 1, 2026
talatuyarer pushed a commit to talatuyarer/iceberg that referenced this pull request Apr 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants