This is an automated email from the ASF dual-hosted git repository.

gaborgsomogyi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git


The following commit(s) were added to refs/heads/master by this push:
     new 6afaa60978c [FLINK-40008][security] Replace fixed delegation token 
renewal retry backoff with exponential backoff
6afaa60978c is described below

commit 6afaa60978cf5d1044caff7557e24f27e1fa6d18
Author: Gabor Somogyi <[email protected]>
AuthorDate: Tue Jun 30 15:22:13 2026 +0200

    [FLINK-40008][security] Replace fixed delegation token renewal retry 
backoff with exponential backoff
---
 .../generated/security_configuration.html          | 12 +++-
 .../security_delegation_token_section.html         | 12 +++-
 .../flink/configuration/SecurityOptions.java       | 26 +++++---
 .../token/DefaultDelegationTokenManager.java       | 56 +++++++++++++++---
 .../token/DefaultDelegationTokenManagerTest.java   | 69 ++++++++++++++++++++++
 5 files changed, 153 insertions(+), 22 deletions(-)

diff --git a/docs/layouts/shortcodes/generated/security_configuration.html 
b/docs/layouts/shortcodes/generated/security_configuration.html
index fdf554c6670..d4fc20d1981 100644
--- a/docs/layouts/shortcodes/generated/security_configuration.html
+++ b/docs/layouts/shortcodes/generated/security_configuration.html
@@ -27,10 +27,16 @@
             <td>Indicates whether to start delegation tokens system for 
external services.</td>
         </tr>
         <tr>
-            <td><h5>security.delegation.tokens.renewal.retry.backoff</h5></td>
-            <td style="word-wrap: break-word;">1 h</td>
+            
<td><h5>security.delegation.tokens.renewal.retry.initial.backoff</h5></td>
+            <td style="word-wrap: break-word;">10 s</td>
             <td>Duration</td>
-            <td>The time period how long to wait before retrying to obtain new 
delegation tokens after a failure.</td>
+            <td>Initial backoff time when retrying to obtain delegation tokens 
after a failure. Doubles on each subsequent failure up to 
security.delegation.tokens.renewal.retry.max.backoff.</td>
+        </tr>
+        <tr>
+            
<td><h5>security.delegation.tokens.renewal.retry.max.backoff</h5></td>
+            <td style="word-wrap: break-word;">5 min</td>
+            <td>Duration</td>
+            <td>Maximum backoff time between retries when obtaining delegation 
tokens fails. Retry delays start at 
security.delegation.tokens.renewal.retry.initial.backoff and double on each 
failure up to this cap.</td>
         </tr>
         <tr>
             <td><h5>security.delegation.tokens.renewal.time-ratio</h5></td>
diff --git 
a/docs/layouts/shortcodes/generated/security_delegation_token_section.html 
b/docs/layouts/shortcodes/generated/security_delegation_token_section.html
index e81865a4d07..0aa8ab815d7 100644
--- a/docs/layouts/shortcodes/generated/security_delegation_token_section.html
+++ b/docs/layouts/shortcodes/generated/security_delegation_token_section.html
@@ -15,10 +15,16 @@
             <td>Indicates whether to start delegation tokens system for 
external services.</td>
         </tr>
         <tr>
-            <td><h5>security.delegation.tokens.renewal.retry.backoff</h5></td>
-            <td style="word-wrap: break-word;">1 h</td>
+            
<td><h5>security.delegation.tokens.renewal.retry.initial.backoff</h5></td>
+            <td style="word-wrap: break-word;">10 s</td>
             <td>Duration</td>
-            <td>The time period how long to wait before retrying to obtain new 
delegation tokens after a failure.</td>
+            <td>Initial backoff time when retrying to obtain delegation tokens 
after a failure. Doubles on each subsequent failure up to 
security.delegation.tokens.renewal.retry.max.backoff.</td>
+        </tr>
+        <tr>
+            
<td><h5>security.delegation.tokens.renewal.retry.max.backoff</h5></td>
+            <td style="word-wrap: break-word;">5 min</td>
+            <td>Duration</td>
+            <td>Maximum backoff time between retries when obtaining delegation 
tokens fails. Retry delays start at 
security.delegation.tokens.renewal.retry.initial.backoff and double on each 
failure up to this cap.</td>
         </tr>
         <tr>
             <td><h5>security.delegation.tokens.renewal.time-ratio</h5></td>
diff --git 
a/flink-core/src/main/java/org/apache/flink/configuration/SecurityOptions.java 
b/flink-core/src/main/java/org/apache/flink/configuration/SecurityOptions.java
index 32d8ee7a1e6..3cfdca5c596 100644
--- 
a/flink-core/src/main/java/org/apache/flink/configuration/SecurityOptions.java
+++ 
b/flink-core/src/main/java/org/apache/flink/configuration/SecurityOptions.java
@@ -148,30 +148,40 @@ public class SecurityOptions {
             key("security.delegation.tokens.enabled")
                     .booleanType()
                     .defaultValue(true)
-                    
.withDeprecatedKeys("security.kerberos.fetch.delegation-token")
                     .withDescription(
                             "Indicates whether to start delegation tokens 
system for external services.");
 
     @Documentation.Section(value = 
Documentation.Sections.SECURITY_DELEGATION_TOKEN, position = 2)
-    public static final ConfigOption<Duration> 
DELEGATION_TOKENS_RENEWAL_RETRY_BACKOFF =
-            key("security.delegation.tokens.renewal.retry.backoff")
+    public static final ConfigOption<Duration> 
DELEGATION_TOKENS_RENEWAL_RETRY_INITIAL_BACKOFF =
+            key("security.delegation.tokens.renewal.retry.initial.backoff")
                     .durationType()
-                    .defaultValue(Duration.ofHours(1))
-                    
.withDeprecatedKeys("security.kerberos.tokens.renewal.retry.backoff")
+                    .defaultValue(Duration.ofSeconds(10))
                     .withDescription(
-                            "The time period how long to wait before retrying 
to obtain new delegation tokens after a failure.");
+                            "Initial backoff time when retrying to obtain 
delegation tokens after a failure. "
+                                    + "Doubles on each subsequent failure up 
to "
+                                    + 
"security.delegation.tokens.renewal.retry.max.backoff.");
 
     @Documentation.Section(value = 
Documentation.Sections.SECURITY_DELEGATION_TOKEN, position = 3)
+    public static final ConfigOption<Duration> 
DELEGATION_TOKENS_RENEWAL_RETRY_MAX_BACKOFF =
+            key("security.delegation.tokens.renewal.retry.max.backoff")
+                    .durationType()
+                    .defaultValue(Duration.ofMinutes(5))
+                    
.withDeprecatedKeys("security.delegation.tokens.renewal.retry.backoff")
+                    .withDescription(
+                            "Maximum backoff time between retries when 
obtaining delegation tokens fails. "
+                                    + "Retry delays start at 
security.delegation.tokens.renewal.retry.initial.backoff "
+                                    + "and double on each failure up to this 
cap.");
+
+    @Documentation.Section(value = 
Documentation.Sections.SECURITY_DELEGATION_TOKEN, position = 4)
     public static final ConfigOption<Double> 
DELEGATION_TOKENS_RENEWAL_TIME_RATIO =
             key("security.delegation.tokens.renewal.time-ratio")
                     .doubleType()
                     .defaultValue(0.75)
-                    
.withDeprecatedKeys("security.kerberos.tokens.renewal.time-ratio")
                     .withDescription(
                             "Ratio of the tokens's expiration time when new 
credentials should be re-obtained.");
 
     @Documentation.SuffixOption(DELEGATION_TOKEN_PROVIDER_PREFIX)
-    @Documentation.Section(value = 
Documentation.Sections.SECURITY_DELEGATION_TOKEN, position = 4)
+    @Documentation.Section(value = 
Documentation.Sections.SECURITY_DELEGATION_TOKEN, position = 5)
     public static final ConfigOption<Boolean> 
DELEGATION_TOKEN_PROVIDER_ENABLED =
             key("enabled")
                     .booleanType()
diff --git 
a/flink-runtime/src/main/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManager.java
 
b/flink-runtime/src/main/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManager.java
index e050ee1e643..d1abfefd866 100644
--- 
a/flink-runtime/src/main/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManager.java
+++ 
b/flink-runtime/src/main/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManager.java
@@ -27,6 +27,7 @@ import 
org.apache.flink.core.security.token.DelegationTokenProvider;
 import org.apache.flink.core.security.token.DelegationTokenReceiver;
 import org.apache.flink.util.FlinkRuntimeException;
 import org.apache.flink.util.InstantiationUtil;
+import org.apache.flink.util.TimeUtils;
 import org.apache.flink.util.concurrent.ScheduledExecutor;
 
 import org.slf4j.Logger;
@@ -36,6 +37,7 @@ import javax.annotation.Nullable;
 import javax.annotation.concurrent.GuardedBy;
 
 import java.time.Clock;
+import java.time.Duration;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -44,11 +46,13 @@ import java.util.ServiceLoader;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 import java.util.stream.Stream;
 
-import static 
org.apache.flink.configuration.SecurityOptions.DELEGATION_TOKENS_RENEWAL_RETRY_BACKOFF;
+import static 
org.apache.flink.configuration.SecurityOptions.DELEGATION_TOKENS_RENEWAL_RETRY_INITIAL_BACKOFF;
+import static 
org.apache.flink.configuration.SecurityOptions.DELEGATION_TOKENS_RENEWAL_RETRY_MAX_BACKOFF;
 import static 
org.apache.flink.configuration.SecurityOptions.DELEGATION_TOKENS_RENEWAL_TIME_RATIO;
 import static 
org.apache.flink.configuration.SecurityOptions.DELEGATION_TOKEN_PROVIDER_ENABLED;
 import static org.apache.flink.util.Preconditions.checkNotNull;
@@ -80,7 +84,13 @@ public class DefaultDelegationTokenManager implements 
DelegationTokenManager {
 
     private final double tokensRenewalTimeRatio;
 
-    private final long renewalRetryBackoffPeriod;
+    private final long renewalRetryInitialBackoff;
+
+    private final long renewalRetryMaxBackoff;
+
+    @VisibleForTesting long currentRetryBackoff;
+
+    @VisibleForTesting long lastKnownNextRenewal = Long.MAX_VALUE;
 
     @VisibleForTesting final Map<String, DelegationTokenProvider> 
delegationTokenProviders;
 
@@ -106,8 +116,11 @@ public class DefaultDelegationTokenManager implements 
DelegationTokenManager {
         this.configuration = checkNotNull(configuration, "Flink configuration 
must not be null");
         this.pluginManager = pluginManager;
         this.tokensRenewalTimeRatio = 
configuration.get(DELEGATION_TOKENS_RENEWAL_TIME_RATIO);
-        this.renewalRetryBackoffPeriod =
-                
configuration.get(DELEGATION_TOKENS_RENEWAL_RETRY_BACKOFF).toMillis();
+        this.renewalRetryInitialBackoff =
+                
configuration.get(DELEGATION_TOKENS_RENEWAL_RETRY_INITIAL_BACKOFF).toMillis();
+        this.renewalRetryMaxBackoff =
+                
configuration.get(DELEGATION_TOKENS_RENEWAL_RETRY_MAX_BACKOFF).toMillis();
+        this.currentRetryBackoff = renewalRetryInitialBackoff;
         this.delegationTokenProviders = loadProviders();
         this.delegationTokenReceiverRepository =
                 new DelegationTokenReceiverRepository(configuration, 
pluginManager);
@@ -321,6 +334,8 @@ public class DefaultDelegationTokenManager implements 
DelegationTokenManager {
             }
 
             if (nextRenewal.isPresent()) {
+                lastKnownNextRenewal = nextRenewal.get();
+                currentRetryBackoff = renewalRetryInitialBackoff;
                 long renewalDelay =
                         calculateRenewalDelay(Clock.systemDefaultZone(), 
nextRenewal.get());
                 synchronized (tokensUpdateFutureLock) {
@@ -330,7 +345,9 @@ public class DefaultDelegationTokenManager implements 
DelegationTokenManager {
                                     renewalDelay,
                                     TimeUnit.MILLISECONDS);
                 }
-                LOG.info("Tokens update task started with {} ms delay", 
renewalDelay);
+                LOG.info(
+                        "Tokens update task started with {} delay",
+                        
TimeUtils.formatWithHighestUnit(Duration.ofMillis(renewalDelay)));
             } else {
                 LOG.warn(
                         "Tokens update task not started because either no 
tokens obtained or none of the tokens specified its renewal date");
@@ -339,16 +356,17 @@ public class DefaultDelegationTokenManager implements 
DelegationTokenManager {
             // Ignore, may happen if shutting down.
             LOG.debug("Interrupted", e);
         } catch (Exception e) {
+            long delay = calculateRetryDelay(Clock.systemDefaultZone());
             synchronized (tokensUpdateFutureLock) {
                 tokensUpdateFuture =
                         scheduledExecutor.schedule(
                                 () -> 
ioExecutor.execute(this::startTokensUpdate),
-                                renewalRetryBackoffPeriod,
+                                delay,
                                 TimeUnit.MILLISECONDS);
             }
             LOG.warn(
-                    "Failed to update tokens, will try again in {} ms",
-                    renewalRetryBackoffPeriod,
+                    "Failed to update tokens, will try again in {}",
+                    TimeUtils.formatWithHighestUnit(Duration.ofMillis(delay)),
                     e);
         }
     }
@@ -363,6 +381,28 @@ public class DefaultDelegationTokenManager implements 
DelegationTokenManager {
         }
     }
 
+    @VisibleForTesting
+    long calculateRetryDelay(Clock clock) {
+        long nowMillis = clock.millis();
+        long effectiveMax;
+        if (lastKnownNextRenewal != Long.MAX_VALUE) {
+            long remaining = lastKnownNextRenewal - nowMillis;
+            // If a failure occurs close to token expiry there may not be 
enough time for the
+            // normal exponential backoff to complete before the token becomes 
invalid. Capping
+            // each retry delay to one third of the remaining valid window 
ensures a retry always
+            // happens while the token is still live, regardless of how close 
to expiry the
+            // failure occurred.
+            effectiveMax = remaining > 0 ? remaining / 3 : 
renewalRetryMaxBackoff;
+        } else {
+            effectiveMax = renewalRetryMaxBackoff;
+        }
+        long base = Math.min(currentRetryBackoff, effectiveMax);
+        long jitter = (long) ((ThreadLocalRandom.current().nextDouble() - 0.5) 
* base);
+        long delay = Math.max(0, Math.min(base + jitter, effectiveMax));
+        currentRetryBackoff = Math.min(currentRetryBackoff * 2, 
renewalRetryMaxBackoff);
+        return delay;
+    }
+
     @VisibleForTesting
     long calculateRenewalDelay(Clock clock, long nextRenewal) {
         long now = clock.millis();
diff --git 
a/flink-runtime/src/test/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManagerTest.java
 
b/flink-runtime/src/test/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManagerTest.java
index c7a4cf6f958..5f78c79a89d 100644
--- 
a/flink-runtime/src/test/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManagerTest.java
+++ 
b/flink-runtime/src/test/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManagerTest.java
@@ -29,6 +29,7 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import java.time.Clock;
+import java.time.Duration;
 import java.time.ZoneId;
 import java.util.Collections;
 import java.util.HashMap;
@@ -39,6 +40,8 @@ import java.util.concurrent.atomic.AtomicInteger;
 
 import static java.time.Instant.ofEpochMilli;
 import static 
org.apache.flink.configuration.ConfigurationUtils.getBooleanConfigOption;
+import static 
org.apache.flink.configuration.SecurityOptions.DELEGATION_TOKENS_RENEWAL_RETRY_INITIAL_BACKOFF;
+import static 
org.apache.flink.configuration.SecurityOptions.DELEGATION_TOKENS_RENEWAL_RETRY_MAX_BACKOFF;
 import static 
org.apache.flink.configuration.SecurityOptions.DELEGATION_TOKENS_RENEWAL_TIME_RATIO;
 import static 
org.apache.flink.core.security.token.DelegationTokenProvider.CONFIG_PREFIX;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -231,4 +234,70 @@ public class DefaultDelegationTokenManagerTest {
         Clock constantClock = Clock.fixed(ofEpochMilli(100), 
ZoneId.systemDefault());
         assertEquals(50, 
delegationTokenManager.calculateRenewalDelay(constantClock, 200));
     }
+
+    @Test
+    public void calculateRetryDelayShouldDoubleOnConsecutiveFailures() {
+        long initialMs = Duration.ofSeconds(10).toMillis();
+        Configuration configuration = new Configuration();
+        configuration.set(getBooleanConfigOption(CONFIG_PREFIX + 
".throw.enabled"), false);
+        configuration.set(DELEGATION_TOKENS_RENEWAL_RETRY_INITIAL_BACKOFF, 
Duration.ofSeconds(10));
+        configuration.set(DELEGATION_TOKENS_RENEWAL_RETRY_MAX_BACKOFF, 
Duration.ofMinutes(5));
+        DefaultDelegationTokenManager manager =
+                new DefaultDelegationTokenManager(configuration, null, null, 
null);
+
+        Clock clock = Clock.fixed(ofEpochMilli(0), ZoneId.systemDefault());
+        long delay1 = manager.calculateRetryDelay(clock);
+        long delay2 = manager.calculateRetryDelay(clock);
+        long delay3 = manager.calculateRetryDelay(clock);
+
+        // Each delay should be within [0, 2 * initial * 2^(n-1)] accounting 
for ±50% jitter.
+        assertTrue(delay1 >= 0 && delay1 <= initialMs * 2);
+        assertTrue(delay2 >= 0 && delay2 <= initialMs * 4);
+        assertTrue(delay3 >= 0 && delay3 <= initialMs * 8);
+        // The base must have doubled: currentRetryBackoff after 3 calls is 
min(80s, 5min) = 80s.
+        assertEquals(Duration.ofSeconds(80).toMillis(), 
manager.currentRetryBackoff);
+    }
+
+    @Test
+    public void calculateRetryDelayShouldResetAfterSuccess() {
+        long initialMs = Duration.ofSeconds(10).toMillis();
+        Configuration configuration = new Configuration();
+        configuration.set(getBooleanConfigOption(CONFIG_PREFIX + 
".throw.enabled"), false);
+        configuration.set(DELEGATION_TOKENS_RENEWAL_RETRY_INITIAL_BACKOFF, 
Duration.ofSeconds(10));
+        configuration.set(DELEGATION_TOKENS_RENEWAL_RETRY_MAX_BACKOFF, 
Duration.ofMinutes(5));
+        DefaultDelegationTokenManager manager =
+                new DefaultDelegationTokenManager(configuration, null, null, 
null);
+
+        // Ramp up the backoff via two failures.
+        Clock clock = Clock.fixed(ofEpochMilli(0), ZoneId.systemDefault());
+        manager.calculateRetryDelay(clock);
+        manager.calculateRetryDelay(clock);
+        // Simulate success: reset currentRetryBackoff (as startTokensUpdate() 
would).
+        manager.currentRetryBackoff = initialMs;
+
+        long delayAfterReset = manager.calculateRetryDelay(clock);
+        assertTrue(delayAfterReset >= 0 && delayAfterReset <= initialMs * 2);
+        assertEquals(initialMs * 2, manager.currentRetryBackoff);
+    }
+
+    @Test
+    public void calculateRetryDelayShouldCapToTtlBound() {
+        Configuration configuration = new Configuration();
+        configuration.set(getBooleanConfigOption(CONFIG_PREFIX + 
".throw.enabled"), false);
+        configuration.set(DELEGATION_TOKENS_RENEWAL_RETRY_INITIAL_BACKOFF, 
Duration.ofSeconds(10));
+        configuration.set(DELEGATION_TOKENS_RENEWAL_RETRY_MAX_BACKOFF, 
Duration.ofMinutes(5));
+        DefaultDelegationTokenManager manager =
+                new DefaultDelegationTokenManager(configuration, null, null, 
null);
+
+        // Simulate a failure close to token expiry (30 s remaining). The 
delay must be capped
+        // so that the retry happens while the token is still valid (at most 
30 s / 3 = 10 s).
+        Clock clock = Clock.fixed(ofEpochMilli(0), ZoneId.systemDefault());
+        manager.lastKnownNextRenewal = Duration.ofSeconds(30).toMillis();
+
+        long delay = manager.calculateRetryDelay(clock);
+
+        // Delay must not exceed the TTL cap (30 s / 3 = 10 s), with jitter 
the max is 10 s.
+        assertTrue(delay <= Duration.ofSeconds(10).toMillis());
+        assertTrue(delay >= 0);
+    }
 }

Reply via email to