gaborgsomogyi commented on code in PR #28639:
URL: https://github.com/apache/flink/pull/28639#discussion_r3534880121


##########
flink-runtime/src/main/java/org/apache/flink/runtime/resourcemanager/ResourceManager.java:
##########
@@ -427,6 +430,14 @@ public CompletableFuture<RegistrationResponse> 
registerJobMaster(
                             jobMasterIdFuture,
                             (JobMasterGateway jobMasterGateway, JobMasterId 
leadingJobMasterId) -> {
                                 if (Objects.equals(leadingJobMasterId, 
jobMasterId)) {
+                                    // Register with the delegation token 
manager first; a
+                                    // provider failure rejects this 
registration so the job
+                                    // never starts without the tokens it 
requires.
+                                    try {
+                                        
delegationTokenManager.registerJob(jobId, jobConfiguration);
+                                    } catch (Exception e) {
+                                        return new 
RegistrationResponse.Failure(e);
+                                    }

Review Comment:
   This is fine but I just want to say it out loud. Since the actual plans are 
scheduling re-obtain async there will be time windows from job perspective 
where no crentials exist so the jobs must survive somehow🙂
   The reason why I'm writing is under some rare circumstances IO threads can 
be under heavy pressure which can lead to several seconds or minutes of delay.



##########
flink-runtime/src/main/java/org/apache/flink/runtime/resourcemanager/ResourceManager.java:
##########
@@ -427,6 +430,14 @@ public CompletableFuture<RegistrationResponse> 
registerJobMaster(
                             jobMasterIdFuture,
                             (JobMasterGateway jobMasterGateway, JobMasterId 
leadingJobMasterId) -> {
                                 if (Objects.equals(leadingJobMasterId, 
jobMasterId)) {
+                                    // Register with the delegation token 
manager first; a
+                                    // provider failure rejects this 
registration so the job
+                                    // never starts without the tokens it 
requires.
+                                    try {
+                                        
delegationTokenManager.registerJob(jobId, jobConfiguration);
+                                    } catch (Exception e) {
+                                        return new 
RegistrationResponse.Failure(e);

Review Comment:
   Can we see this exception somehwere? I'm pretty sure if this happens oncall 
guys want to know what happened.



##########
flink-runtime/src/main/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManager.java:
##########
@@ -416,13 +531,114 @@ long calculateRenewalDelay(Clock clock, long 
nextRenewal) {
         return renewalDelay;
     }
 
+    @VisibleForTesting
+    void setClock(Clock clock) {
+        this.clock = clock;
+    }
+
     /** Stops re-occurring token obtain task. */
     @Override
     public void stop() {
         LOG.info("Stopping credential renewal");
 
-        stopTokensUpdate();
+        synchronized (tokensUpdateFutureLock) {
+            // Mark stopped, cancel the pending cycle, and reset on-demand 
re-obtain bookkeeping
+            // atomically, so a concurrent reobtainDelegationTokens() cannot 
leave a live future
+            // orphaned after stop and a later start() does not inherit stale 
state.
+            stopped = true;
+            stopTokensUpdate();
+            reobtainScheduled = false;
+            lastReobtainAtMillis = NO_PREVIOUS_REOBTAIN;
+        }
+
+        for (DelegationTokenProvider provider : 
delegationTokenProviders.values()) {

Review Comment:
   The manager is already stopped and start can be called from the other side 
from another thread. I think that providers will stuck in unkown state under 
the following circumstances:
   - All providers started
   - Stop called but providers not yet stopped
   - Start called from another thread and start some/all of them
   - Stop continues and stop some/all of them
   - Realize that even if functions called in proper order some of the 
providers are stopped
   
   I think we must support all situations like this because the manager is the 
spine of authentication.



##########
flink-runtime/src/main/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManager.java:
##########
@@ -416,13 +531,114 @@ long calculateRenewalDelay(Clock clock, long 
nextRenewal) {
         return renewalDelay;
     }
 
+    @VisibleForTesting
+    void setClock(Clock clock) {
+        this.clock = clock;
+    }
+
     /** Stops re-occurring token obtain task. */
     @Override
     public void stop() {
         LOG.info("Stopping credential renewal");
 
-        stopTokensUpdate();
+        synchronized (tokensUpdateFutureLock) {
+            // Mark stopped, cancel the pending cycle, and reset on-demand 
re-obtain bookkeeping
+            // atomically, so a concurrent reobtainDelegationTokens() cannot 
leave a live future
+            // orphaned after stop and a later start() does not inherit stale 
state.
+            stopped = true;
+            stopTokensUpdate();
+            reobtainScheduled = false;
+            lastReobtainAtMillis = NO_PREVIOUS_REOBTAIN;
+        }
+
+        for (DelegationTokenProvider provider : 
delegationTokenProviders.values()) {
+            try {
+                provider.stop();
+            } catch (Throwable t) {
+                LOG.error("Failed to stop delegation token provider {}", 
provider.serviceName(), t);
+            }
+        }
 
         LOG.info("Stopped credential renewal");
     }
+
+    @Override
+    public void reobtainDelegationTokens() {
+        synchronized (tokensUpdateFutureLock) {
+            if (scheduledExecutor == null || ioExecutor == null) {
+                LOG.debug(
+                        "A re-obtain of delegation tokens was requested but 
the manager was "
+                                + "constructed without executors (one-shot 
obtain path); the "
+                                + "request is ignored.");
+                return;
+            }
+            if (stopped) {
+                LOG.debug(
+                        "A re-obtain of delegation tokens was requested after 
the manager was "
+                                + "stopped; the request is ignored.");
+                return;
+            }
+            // Dedupe: if an on-demand re-obtain is already scheduled and has 
not started yet, the
+            // newly registered job(s) will be covered by it, so coalesce this 
request into it.
+            if (reobtainScheduled) {
+                LOG.debug("A re-obtain of delegation tokens is already 
scheduled; coalescing.");
+                return;
+            }
+            // Cooldown: bound how often on-demand re-obtains can run by 
deferring this cycle until
+            // at least reobtainCooldownMillis have passed since the previous 
on-demand re-obtain.
+            long now = clock.millis();
+            long delayMillis =
+                    lastReobtainAtMillis == NO_PREVIOUS_REOBTAIN
+                            ? 0L
+                            : Math.max(0L, lastReobtainAtMillis + 
reobtainCooldownMillis - now);
+            // Only bring the next cycle forward: if a cycle (e.g. the 
periodic renewal) is still
+            // pending and scheduled to fire sooner than the cooldown-deferred 
time, fire at that
+            // earlier time instead of pushing it later — otherwise a 
short-lived token could expire
+            // before it is renewed. The nextScheduledAtMillis > now guard 
skips an already-fired
+            // future that has not yet been re-armed, so this never bypasses 
the cooldown.
+            if (tokensUpdateFuture != null
+                    && nextScheduledAtMillis > now
+                    && nextScheduledAtMillis - now < delayMillis) {
+                delayMillis = nextScheduledAtMillis - now;
+            }
+            lastReobtainAtMillis = now;
+            reobtainScheduled = true;
+            LOG.debug(
+                    "Re-obtain of delegation tokens requested; scheduling an 
obtain cycle in {}",
+                    
TimeUtils.formatWithHighestUnit(Duration.ofMillis(delayMillis)));
+            scheduleRenewalLocked(delayMillis);
+        }
+    }
+
+    @Override
+    public void registerJob(JobID jobId, Configuration jobConfiguration) 
throws Exception {
+        try {
+            for (DelegationTokenProvider provider : 
delegationTokenProviders.values()) {
+                provider.registerJob(jobId, jobConfiguration);
+            }
+        } catch (Exception e) {
+            // If any of the providers fail to register, then unregister the 
job from them all.
+            // unregisterJob is idempotent, so it is safe to call it for 
providers that were never
+            // (or only partially) registered for this job before the failure. 
The rollback must
+            // never mask the original failure, so swallow any rollback 
exception.
+            try {
+                unregisterJob(jobId);
+            } catch (Exception rollbackException) {
+                LOG.error("Failed to roll back registration of job {}", jobId, 
rollbackException);
+            }
+            LOG.error("Failed to register job {}", jobId, e);
+            throw e;
+        }
+    }
+
+    @Override
+    public void unregisterJob(JobID jobId) throws Exception {
+        for (DelegationTokenProvider provider : 
delegationTokenProviders.values()) {
+            try {
+                provider.unregisterJob(jobId);
+            } catch (Exception e) {
+                LOG.error("Failed to unregister job for provider {}", 
provider.serviceName(), e);

Review Comment:
   Here the poor developer has no idea what is the `JobID` so it would be good 
to be added.



##########
flink-runtime/src/main/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManager.java:
##########
@@ -310,64 +361,127 @@ public void start(Listener listener) throws Exception {
         this.listener = checkNotNull(listener, "Listener must not be null");
         synchronized (tokensUpdateFutureLock) {
             checkState(tokensUpdateFuture == null, "Manager is already 
started");
+            stopped = false;
         }
 
         startTokensUpdate();
     }
 
     @VisibleForTesting
     void startTokensUpdate() {
-        try {
-            LOG.info("Starting tokens update task");
-            DelegationTokenContainer container = new 
DelegationTokenContainer();
-            Optional<Long> nextRenewal = 
obtainDelegationTokensAndGetNextRenewal(container);
-
-            if (container.hasTokens()) {
-                
delegationTokenReceiverRepository.onNewTokensObtained(container);
-
-                LOG.info("Notifying listener about new tokens");
-                checkNotNull(listener, "Listener must not be null");
-                
listener.onNewTokensObtained(InstantiationUtil.serializeObject(container));
-                LOG.info("Listener notified successfully");
-            } else {
-                LOG.warn("No tokens obtained so skipping notifications");
+        synchronized (tokensUpdateFutureLock) {
+            // The obtain cycle is starting: clear the dedupe flag so later 
on-demand requests can
+            // schedule a fresh cycle.
+            reobtainScheduled = false;
+            // If stop() ran before this cycle (already handed to the IO 
executor) began, skip the
+            // obtain/broadcast: the providers may already be stopped. Safe 
via this lock's
+            // happens-before with stop(). The dedupe flag is cleared above, 
so it is never stuck.
+            if (stopped) {
+                return;
             }
+        }
+        // Serialize the obtain-and-broadcast so a re-obtain racing the 
periodic renewal cannot run
+        // two cycles concurrently on the (multi-threaded) IO executor and 
broadcast out of order.
+        synchronized (obtainLock) {
+            try {
+                LOG.info("Starting tokens update task");
+                DelegationTokenContainer container = new 
DelegationTokenContainer();
+                Optional<Long> nextRenewal = 
obtainDelegationTokensAndGetNextRenewal(container);
+
+                if (container.hasTokens()) {
+                    
delegationTokenReceiverRepository.onNewTokensObtained(container);
+
+                    LOG.info("Notifying listener about new tokens");
+                    checkNotNull(listener, "Listener must not be null");
+                    
listener.onNewTokensObtained(InstantiationUtil.serializeObject(container));
+                    LOG.info("Listener notified successfully");
+                } else {
+                    LOG.warn("No tokens obtained so skipping notifications");
+                }
 
-            if (nextRenewal.isPresent()) {
-                lastKnownNextRenewal = nextRenewal.get();
-                currentRetryBackoff = renewalRetryInitialBackoff;
-                long renewalDelay =
-                        calculateRenewalDelay(Clock.systemDefaultZone(), 
nextRenewal.get());
-                synchronized (tokensUpdateFutureLock) {
-                    tokensUpdateFuture =
-                            scheduledExecutor.schedule(
-                                    () -> 
ioExecutor.execute(this::startTokensUpdate),
-                                    renewalDelay,
-                                    TimeUnit.MILLISECONDS);
+                if (nextRenewal.isPresent()) {
+                    lastKnownNextRenewal = nextRenewal.get();
+                    currentRetryBackoff = renewalRetryInitialBackoff;
+                    long renewalDelay = calculateRenewalDelay(clock, 
nextRenewal.get());
+                    maybeScheduleRenewal(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");
                 }
-                LOG.info(
-                        "Tokens update task started with {} delay",
-                        
TimeUtils.formatWithHighestUnit(Duration.ofMillis(renewalDelay)));
-            } else {
+            } catch (InterruptedException e) {
+                // Ignore, may happen if shutting down.
+                LOG.debug("Interrupted", e);
+            } catch (Exception e) {
+                long delay = calculateRetryDelay(clock);
+                maybeScheduleRenewal(delay);
                 LOG.warn(
-                        "Tokens update task not started because either no 
tokens obtained or none of the tokens specified its renewal date");
+                        "Failed to update tokens, will try again in {}",
+                        
TimeUtils.formatWithHighestUnit(Duration.ofMillis(delay)),
+                        e);
             }
-        } catch (InterruptedException e) {
-            // 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),
-                                delay,
-                                TimeUnit.MILLISECONDS);
+        }
+    }
+
+    /**
+     * Schedules a one-shot token-obtain-and-broadcast cycle after {@code 
delayMs}, replacing any
+     * pending renewal; a delay of {@code 0} brings the next cycle forward to 
now. Must only be
+     * called after {@link #start(Listener)} (the scheduled and IO executors 
are non-null then) and
+     * while holding {@link #tokensUpdateFutureLock}.
+     */
+    @GuardedBy("tokensUpdateFutureLock")
+    private void scheduleRenewalLocked(long delayMs) {
+        stopTokensUpdate();
+        nextScheduledAtMillis = clock.millis() + delayMs;
+        try {
+            tokensUpdateFuture =
+                    scheduledExecutor.schedule(
+                            () -> {
+                                try {
+                                    
ioExecutor.execute(this::startTokensUpdate);
+                                } catch (RejectedExecutionException e) {
+                                    // IO executor is shutting down: drop the 
cycle but release the
+                                    // dedupe flag so it cannot get stuck if 
the manager is reused.
+                                    synchronized (tokensUpdateFutureLock) {

Review Comment:
   The function broadcasts about itself that it's guarded by 
`tokensUpdateFutureLock` so why extra needed here?



##########
flink-runtime/src/main/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManager.java:
##########
@@ -416,13 +531,114 @@ long calculateRenewalDelay(Clock clock, long 
nextRenewal) {
         return renewalDelay;
     }
 
+    @VisibleForTesting
+    void setClock(Clock clock) {
+        this.clock = clock;
+    }
+
     /** Stops re-occurring token obtain task. */
     @Override
     public void stop() {
         LOG.info("Stopping credential renewal");
 
-        stopTokensUpdate();
+        synchronized (tokensUpdateFutureLock) {
+            // Mark stopped, cancel the pending cycle, and reset on-demand 
re-obtain bookkeeping
+            // atomically, so a concurrent reobtainDelegationTokens() cannot 
leave a live future
+            // orphaned after stop and a later start() does not inherit stale 
state.
+            stopped = true;
+            stopTokensUpdate();
+            reobtainScheduled = false;
+            lastReobtainAtMillis = NO_PREVIOUS_REOBTAIN;
+        }
+
+        for (DelegationTokenProvider provider : 
delegationTokenProviders.values()) {
+            try {
+                provider.stop();
+            } catch (Throwable t) {
+                LOG.error("Failed to stop delegation token provider {}", 
provider.serviceName(), t);
+            }
+        }
 
         LOG.info("Stopped credential renewal");
     }
+
+    @Override
+    public void reobtainDelegationTokens() {
+        synchronized (tokensUpdateFutureLock) {
+            if (scheduledExecutor == null || ioExecutor == null) {
+                LOG.debug(
+                        "A re-obtain of delegation tokens was requested but 
the manager was "
+                                + "constructed without executors (one-shot 
obtain path); the "
+                                + "request is ignored.");
+                return;
+            }
+            if (stopped) {
+                LOG.debug(
+                        "A re-obtain of delegation tokens was requested after 
the manager was "
+                                + "stopped; the request is ignored.");
+                return;
+            }
+            // Dedupe: if an on-demand re-obtain is already scheduled and has 
not started yet, the
+            // newly registered job(s) will be covered by it, so coalesce this 
request into it.
+            if (reobtainScheduled) {
+                LOG.debug("A re-obtain of delegation tokens is already 
scheduled; coalescing.");
+                return;
+            }
+            // Cooldown: bound how often on-demand re-obtains can run by 
deferring this cycle until
+            // at least reobtainCooldownMillis have passed since the previous 
on-demand re-obtain.
+            long now = clock.millis();
+            long delayMillis =
+                    lastReobtainAtMillis == NO_PREVIOUS_REOBTAIN
+                            ? 0L
+                            : Math.max(0L, lastReobtainAtMillis + 
reobtainCooldownMillis - now);
+            // Only bring the next cycle forward: if a cycle (e.g. the 
periodic renewal) is still
+            // pending and scheduled to fire sooner than the cooldown-deferred 
time, fire at that
+            // earlier time instead of pushing it later — otherwise a 
short-lived token could expire
+            // before it is renewed. The nextScheduledAtMillis > now guard 
skips an already-fired
+            // future that has not yet been re-armed, so this never bypasses 
the cooldown.
+            if (tokensUpdateFuture != null
+                    && nextScheduledAtMillis > now
+                    && nextScheduledAtMillis - now < delayMillis) {
+                delayMillis = nextScheduledAtMillis - now;
+            }
+            lastReobtainAtMillis = now;
+            reobtainScheduled = true;
+            LOG.debug(
+                    "Re-obtain of delegation tokens requested; scheduling an 
obtain cycle in {}",
+                    
TimeUtils.formatWithHighestUnit(Duration.ofMillis(delayMillis)));
+            scheduleRenewalLocked(delayMillis);
+        }
+    }
+
+    @Override
+    public void registerJob(JobID jobId, Configuration jobConfiguration) 
throws Exception {

Review Comment:
   It would be valuable to know which provider has blown up like in 
`unregisterJob`.



##########
flink-runtime/src/main/java/org/apache/flink/runtime/security/token/DelegationTokenManager.java:
##########
@@ -61,4 +63,37 @@ interface Listener {
 
     /** Stops re-occurring token obtain task. */
     void stop();
+
+    /**
+     * Requests an immediate, asynchronous token-obtain-and-distribute cycle, 
bringing the next
+     * cycle forward instead of waiting for the periodic renewal. May be 
called from any thread;
+     * it is a no-op on a manager constructed without executors (the one-shot 
obtain path).
+     * Concurrent requests are coalesced and a configurable cooldown may 
apply, so a call does
+     * not necessarily map to exactly one obtain.
+     *
+     * <p>Backs {@link
+     * 
org.apache.flink.core.security.token.DelegationTokenManagerCallback#reobtainDelegationTokens()}.
+     */
+    default void reobtainDelegationTokens() {}

Review Comment:
   I think we can have such new functions unimplemented and say that job aware 
approach is a mush have for all managers.



##########
flink-runtime/src/main/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManager.java:
##########
@@ -310,64 +361,127 @@ public void start(Listener listener) throws Exception {
         this.listener = checkNotNull(listener, "Listener must not be null");
         synchronized (tokensUpdateFutureLock) {
             checkState(tokensUpdateFuture == null, "Manager is already 
started");
+            stopped = false;

Review Comment:
   I think setting this flag freewillingly is simply wrong. AFIAU the whole 
point here is to go through the start/stop sequence to keep consistency inside 
the whole manager ecosystem. Here are my thouhgts:
   - Maybe we can call it `started`
   - Set `started=true` at the end of `start` and early return if already 
started
   - Set `started=fase` at the end of `stop` and early return if not started
   
   If you have something fundamentally different in mind then feel free to 
challange it.



##########
flink-runtime/src/main/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManager.java:
##########
@@ -310,64 +361,127 @@ public void start(Listener listener) throws Exception {
         this.listener = checkNotNull(listener, "Listener must not be null");
         synchronized (tokensUpdateFutureLock) {
             checkState(tokensUpdateFuture == null, "Manager is already 
started");
+            stopped = false;
         }
 
         startTokensUpdate();
     }
 
     @VisibleForTesting
     void startTokensUpdate() {
-        try {
-            LOG.info("Starting tokens update task");
-            DelegationTokenContainer container = new 
DelegationTokenContainer();
-            Optional<Long> nextRenewal = 
obtainDelegationTokensAndGetNextRenewal(container);
-
-            if (container.hasTokens()) {
-                
delegationTokenReceiverRepository.onNewTokensObtained(container);
-
-                LOG.info("Notifying listener about new tokens");
-                checkNotNull(listener, "Listener must not be null");
-                
listener.onNewTokensObtained(InstantiationUtil.serializeObject(container));
-                LOG.info("Listener notified successfully");
-            } else {
-                LOG.warn("No tokens obtained so skipping notifications");
+        synchronized (tokensUpdateFutureLock) {
+            // The obtain cycle is starting: clear the dedupe flag so later 
on-demand requests can
+            // schedule a fresh cycle.
+            reobtainScheduled = false;
+            // If stop() ran before this cycle (already handed to the IO 
executor) began, skip the
+            // obtain/broadcast: the providers may already be stopped. Safe 
via this lock's
+            // happens-before with stop(). The dedupe flag is cleared above, 
so it is never stuck.
+            if (stopped) {
+                return;
             }
+        }
+        // Serialize the obtain-and-broadcast so a re-obtain racing the 
periodic renewal cannot run
+        // two cycles concurrently on the (multi-threaded) IO executor and 
broadcast out of order.
+        synchronized (obtainLock) {
+            try {
+                LOG.info("Starting tokens update task");
+                DelegationTokenContainer container = new 
DelegationTokenContainer();
+                Optional<Long> nextRenewal = 
obtainDelegationTokensAndGetNextRenewal(container);
+
+                if (container.hasTokens()) {
+                    
delegationTokenReceiverRepository.onNewTokensObtained(container);
+
+                    LOG.info("Notifying listener about new tokens");
+                    checkNotNull(listener, "Listener must not be null");
+                    
listener.onNewTokensObtained(InstantiationUtil.serializeObject(container));
+                    LOG.info("Listener notified successfully");
+                } else {
+                    LOG.warn("No tokens obtained so skipping notifications");
+                }
 
-            if (nextRenewal.isPresent()) {
-                lastKnownNextRenewal = nextRenewal.get();
-                currentRetryBackoff = renewalRetryInitialBackoff;
-                long renewalDelay =
-                        calculateRenewalDelay(Clock.systemDefaultZone(), 
nextRenewal.get());
-                synchronized (tokensUpdateFutureLock) {
-                    tokensUpdateFuture =
-                            scheduledExecutor.schedule(
-                                    () -> 
ioExecutor.execute(this::startTokensUpdate),
-                                    renewalDelay,
-                                    TimeUnit.MILLISECONDS);
+                if (nextRenewal.isPresent()) {
+                    lastKnownNextRenewal = nextRenewal.get();
+                    currentRetryBackoff = renewalRetryInitialBackoff;
+                    long renewalDelay = calculateRenewalDelay(clock, 
nextRenewal.get());
+                    maybeScheduleRenewal(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");
                 }
-                LOG.info(
-                        "Tokens update task started with {} delay",
-                        
TimeUtils.formatWithHighestUnit(Duration.ofMillis(renewalDelay)));
-            } else {
+            } catch (InterruptedException e) {
+                // Ignore, may happen if shutting down.
+                LOG.debug("Interrupted", e);
+            } catch (Exception e) {
+                long delay = calculateRetryDelay(clock);
+                maybeScheduleRenewal(delay);
                 LOG.warn(
-                        "Tokens update task not started because either no 
tokens obtained or none of the tokens specified its renewal date");
+                        "Failed to update tokens, will try again in {}",
+                        
TimeUtils.formatWithHighestUnit(Duration.ofMillis(delay)),
+                        e);

Review Comment:
   Since `maybeScheduleRenewal` may or may not schedule a new round this 
statement is not true. I think retry logic should not collide with the main 
scheduling logic.



##########
flink-runtime/src/main/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManager.java:
##########
@@ -416,13 +531,114 @@ long calculateRenewalDelay(Clock clock, long 
nextRenewal) {
         return renewalDelay;
     }
 
+    @VisibleForTesting
+    void setClock(Clock clock) {
+        this.clock = clock;
+    }
+
     /** Stops re-occurring token obtain task. */
     @Override
     public void stop() {
         LOG.info("Stopping credential renewal");
 
-        stopTokensUpdate();
+        synchronized (tokensUpdateFutureLock) {
+            // Mark stopped, cancel the pending cycle, and reset on-demand 
re-obtain bookkeeping
+            // atomically, so a concurrent reobtainDelegationTokens() cannot 
leave a live future
+            // orphaned after stop and a later start() does not inherit stale 
state.
+            stopped = true;
+            stopTokensUpdate();
+            reobtainScheduled = false;
+            lastReobtainAtMillis = NO_PREVIOUS_REOBTAIN;
+        }
+
+        for (DelegationTokenProvider provider : 
delegationTokenProviders.values()) {
+            try {
+                provider.stop();
+            } catch (Throwable t) {
+                LOG.error("Failed to stop delegation token provider {}", 
provider.serviceName(), t);
+            }
+        }
 
         LOG.info("Stopped credential renewal");
     }
+
+    @Override
+    public void reobtainDelegationTokens() {
+        synchronized (tokensUpdateFutureLock) {
+            if (scheduledExecutor == null || ioExecutor == null) {
+                LOG.debug(
+                        "A re-obtain of delegation tokens was requested but 
the manager was "
+                                + "constructed without executors (one-shot 
obtain path); the "
+                                + "request is ignored.");
+                return;
+            }
+            if (stopped) {
+                LOG.debug(
+                        "A re-obtain of delegation tokens was requested after 
the manager was "
+                                + "stopped; the request is ignored.");
+                return;
+            }
+            // Dedupe: if an on-demand re-obtain is already scheduled and has 
not started yet, the
+            // newly registered job(s) will be covered by it, so coalesce this 
request into it.
+            if (reobtainScheduled) {
+                LOG.debug("A re-obtain of delegation tokens is already 
scheduled; coalescing.");
+                return;
+            }
+            // Cooldown: bound how often on-demand re-obtains can run by 
deferring this cycle until
+            // at least reobtainCooldownMillis have passed since the previous 
on-demand re-obtain.
+            long now = clock.millis();
+            long delayMillis =
+                    lastReobtainAtMillis == NO_PREVIOUS_REOBTAIN
+                            ? 0L
+                            : Math.max(0L, lastReobtainAtMillis + 
reobtainCooldownMillis - now);
+            // Only bring the next cycle forward: if a cycle (e.g. the 
periodic renewal) is still
+            // pending and scheduled to fire sooner than the cooldown-deferred 
time, fire at that
+            // earlier time instead of pushing it later — otherwise a 
short-lived token could expire
+            // before it is renewed. The nextScheduledAtMillis > now guard 
skips an already-fired
+            // future that has not yet been re-armed, so this never bypasses 
the cooldown.
+            if (tokensUpdateFuture != null
+                    && nextScheduledAtMillis > now
+                    && nextScheduledAtMillis - now < delayMillis) {
+                delayMillis = nextScheduledAtMillis - now;
+            }
+            lastReobtainAtMillis = now;
+            reobtainScheduled = true;
+            LOG.debug(
+                    "Re-obtain of delegation tokens requested; scheduling an 
obtain cycle in {}",
+                    
TimeUtils.formatWithHighestUnit(Duration.ofMillis(delayMillis)));
+            scheduleRenewalLocked(delayMillis);

Review Comment:
   When `scheduleRenewalLocked` blows up here then nobody in the universe will 
clean `reobtainScheduled` and never ever will fire the update again, right?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to