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


##########
flink-runtime/src/main/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManager.java:
##########
@@ -307,67 +411,209 @@ protected Optional<Long> 
obtainDelegationTokensAndGetNextRenewal(
     public void start(Listener listener) throws Exception {
         checkNotNull(scheduledExecutor, "Scheduled executor must not be null");
         checkNotNull(ioExecutor, "IO executor must not be null");
-        this.listener = checkNotNull(listener, "Listener must not be null");
+        checkNotNull(listener, "Listener must not be null");
         synchronized (tokensUpdateFutureLock) {
-            checkState(tokensUpdateFuture == null, "Manager is already 
started");
+            // Checked under the lock so a start() arriving after close() 
fails instead of
+            // resurrecting a session against stopped providers. A start() 
racing close() can
+            // still slip past the check. close()'s stop() then ends its 
session under this
+            // lock, so its inline first cycle either skips on running == 
false or runs at most
+            // one obtain that cannot deliver or reschedule and may overlap 
the provider stop()
+            // (see close()).
+            checkState(
+                    !closed.get(),
+                    "The delegation token manager is already closed, its 
providers are stopped");
+            if (running) {
+                LOG.warn("DelegationTokenManager is already started, ignoring 
redundant start()");
+                return;
+            }
+            this.listener = listener;
+            sessionEpoch++;
+            // Set before the inline first cycle below: startTokensUpdate() and
+            // maybeScheduleRenewal() gate on it.
+            running = true;
+        }
+
+        // A new session must not inherit the previous session's retry backoff 
or renewal
+        // deadline. obtainLock orders this reset after any still-running 
previous cycle. Not
+        // nested in the block above to keep the obtainLock -> 
tokensUpdateFutureLock order.
+        synchronized (obtainLock) {
+            currentRetryBackoff = renewalRetryInitialBackoff;
+            lastKnownNextRenewal = Long.MAX_VALUE;
         }
 
         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");
+        final long cycleEpoch;
+        synchronized (tokensUpdateFutureLock) {
+            // Clear the dedupe flag so later on-demand requests can schedule 
a fresh cycle.
+            reobtainScheduled = false;
+            // Stopped or never started: skip the cycle. The providers may 
already be stopped
+            // and the listener may not be set yet.
+            if (!running) {
+                return;
             }
+            cycleEpoch = sessionEpoch;
+        }
+        // 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()) {
+                    // stop() does not wait for an in-flight cycle: re-check 
running so a resumed
+                    // cycle does not notify the stopped session's listener, 
and compare epochs
+                    // so a cycle begun under an earlier session cannot 
deliver into the next
+                    // one (see sessionEpoch). A stop() right after this read 
still lets one
+                    // delivery through, which is benign.
+                    final Listener currentListener;
+                    synchronized (tokensUpdateFutureLock) {
+                        currentListener = running && cycleEpoch == 
sessionEpoch ? listener : null;
+                    }
+                    if (currentListener != null) {
+                        
delegationTokenReceiverRepository.onNewTokensObtained(container);
+
+                        LOG.info("Notifying listener about new tokens");
+                        currentListener.onNewTokensObtained(
+                                InstantiationUtil.serializeObject(container));
+                        LOG.info("Listener notified successfully");
+                    } else {
+                        LOG.info(
+                                "Manager stopped while the tokens were being 
obtained, skipping "
+                                        + "notifications");
+                    }
+                } 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());
+                    long effectiveDelay = maybeScheduleRenewal(renewalDelay);
+                    if (effectiveDelay >= 0) {
+                        LOG.info(
+                                "Tokens update task started with {} delay",
+                                
TimeUtils.formatWithHighestUnit(Duration.ofMillis(effectiveDelay)));
+                    } else {
+                        LOG.info("Tokens update task not rescheduled, the 
manager is not running");
+                    }
+                } 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 {
-                LOG.warn(
-                        "Tokens update task not started because either no 
tokens obtained or none of the tokens specified its renewal date");
+            } catch (InterruptedException e) {
+                // Ignore, may happen if shutting down.
+                LOG.debug("Interrupted", e);
+            } catch (Exception e) {
+                long delay = calculateRetryDelay(clock);
+                long effectiveDelay;
+                try {
+                    effectiveDelay = maybeScheduleRenewal(delay);
+                } catch (Throwable schedulingFailure) {
+                    // The original failure was not logged yet, keep it 
attached.
+                    schedulingFailure.addSuppressed(e);
+                    throw schedulingFailure;
+                }
+                if (effectiveDelay >= 0) {
+                    LOG.warn(
+                            "Failed to update tokens, will try again in {}",
+                            
TimeUtils.formatWithHighestUnit(Duration.ofMillis(effectiveDelay)),
+                            e);
+                } else {
+                    LOG.warn(
+                            "Failed to update tokens, no retry scheduled 
because the manager is "
+                                    + "not running",
+                            e);
+                }
+            }
+        }
+    }
+
+    /**
+     * 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.relativeTimeMillis() + 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) {
+                                        reobtainScheduled = false;
+                                    }
+                                    LOG.debug("Tokens update task rejected by 
IO executor", e);
+                                }
+                            },
+                            delayMs,
+                            TimeUnit.MILLISECONDS);

Review Comment:
   You are right, the rejection alone does not imply shutdown. Addressed in 
0f60c5c619d.  Thanks for a good finding.
   Now, If the scheduler rejects a submission, an independent timer retries it, 
so arranging the retry doesn’t depend on the rejecting scheduler. Rejection 
from a live IO executor also schedules another attempt. These retries submit 
work and token acquisition still runs on the IO executor. 
   Submission retries use the configured initial backoff, with a one-second 
minimum. To avoid flooding logs, the **first** rejection is logged at `WARN` 
with its stack trace. Further rejections are logged at `DEBUG`, with a `WARN` 
count summary at most once per minute during continued rejection. This resets 
when a token cycle starts or the manager stops. Pending requests stay 
coalesced, and stop() cancels pending retries. 
   
   Added deterministic tests for recovery, repeated rejection, shutdown, stale 
rejection handlers, retry delays, and log rate limiting.



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