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


##########
flink-runtime/src/main/java/org/apache/flink/runtime/resourcemanager/ResourceManager.java:
##########
@@ -427,6 +430,22 @@ public CompletableFuture<RegistrationResponse> 
registerJobMaster(
                             jobMasterIdFuture,
                             (JobMasterGateway jobMasterGateway, JobMasterId 
leadingJobMasterId) -> {
                                 if (Objects.equals(leadingJobMasterId, 
jobMasterId)) {
+                                    // Register with the delegation token 
manager first, so a
+                                    // provider failure rejects the 
registration and the job does
+                                    // not start without the tokens it 
requires. LinkageError is
+                                    // caught so a plugin classpath failure is 
reported the same
+                                    // way.
+                                    try {
+                                        
delegationTokenManager.registerJob(jobId, jobConfiguration);
+                                    } catch (Exception | LinkageError e) {

Review Comment:
   Why do we want to prepare for `LinkageError`? Giving a meaningful rejection 
is fine here but as a general saying any provider that throws such should not 
be considered healthy. To be exact I'm against to treat providers inside the 
manager which throw such exception to be tracked. Temporary exceptions can 
happen but this is deployment/compile issue which should just block workloads.



##########
flink-runtime/src/main/java/org/apache/flink/runtime/resourcemanager/ResourceManagerGateway.java:
##########
@@ -63,10 +64,42 @@ public interface ResourceManagerGateway
     /**
      * Register a {@link JobMaster} at the resource manager.
      *
+     * <p>Backward-compatible overload that registers without a job 
configuration. Equivalent to
+     * calling {@link #registerJobMaster(JobMasterId, ResourceID, String, 
JobID, Configuration,
+     * Duration)} with an empty configuration.
+     *
+     * @param jobMasterId The fencing token for the JobMaster leader
+     * @param jobMasterResourceId The resource ID of the JobMaster that 
registers
+     * @param jobMasterAddress The address of the JobMaster that registers
+     * @param jobId The Job ID of the JobMaster that registers
+     * @param timeout Timeout for the future to complete
+     * @return Future registration response
+     */
+    default CompletableFuture<RegistrationResponse> registerJobMaster(

Review Comment:
   IIUC it's a dead weight that only exists to avoid touching two test files. 
If that's true maybe we can modify those tests



##########
flink-runtime/src/test/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManagerTest.java:
##########
@@ -211,8 +231,9 @@ void startTokensUpdate() {
                     }
                 };
 
-        delegationTokenManager.startTokensUpdate();
+        // The first two cycles fail and schedule a retry each. The third 
succeeds.
         ExceptionThrowingDelegationTokenProvider.throwInUsage.set(true);
+        delegationTokenManager.start(tokens -> {});

Review Comment:
   We've migrated from `startTokensUpdate` to `start` which is fine but then it 
worth to be symmetric and use `stop` instead of `stopTokensUpdate`.



##########
flink-runtime/src/main/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManager.java:
##########
@@ -92,6 +101,22 @@ public class DefaultDelegationTokenManager implements 
DelegationTokenManager {
 
     @VisibleForTesting long lastKnownNextRenewal = Long.MAX_VALUE;
 
+    private final long reobtainCooldownMillis;
+
+    /**
+     * Clock used for renewal and cooldown timing. Renewal math reads absolute 
time (a token's
+     * validUntil is an absolute epoch), while scheduling and the cooldown 
read relative time, which
+     * wall-clock adjustments cannot distort. Never mix the two in one 
expression.
+     */
+    private final Clock clock;
+
+    /**
+     * Serializes the obtain-and-broadcast cycle so that, even though {@code 
cancel(true)} does not
+     * wait for an in-flight cycle and the IO executor is multi-threaded, two 
cycles can never run
+     * concurrently and broadcast tokens out of order.
+     */
+    private final Object obtainLock = new Object();

Review Comment:
   Two locks here, each protecting a distinct concern: `obtainLock` serializes 
one full obtain->broadcast->retry-bookkeeping cycle, `tokensUpdateFutureLock` 
guards scheduling/lifecycle state (when the next cycle runs, `running`, 
`sessionEpoch`, `listener`). `tokensUpdateFutureLock` doesn't reflect that, it 
sounds like it only guards one field. Suggest renaming `obtainLock` to 
`renewalCycleLock` and `tokensUpdateFutureLock` to `schedulingLock`.
   



##########
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:
   `RejectedExecutionException` doesn't necessarily mean shutdown, it's also 
thrown when a bounded queue is saturated while the executor is still alive. 
`scheduledExecutor` is a generic `ScheduledExecutor`, so we can't assume 
shutdown here. If it's actually saturation, this catch drops the renewal cycle 
for the rest of the session with no retry. Shouldn't this schedule a retry 
instead of assuming shutdown?



##########
flink-runtime/src/test/java/org/apache/flink/runtime/resourcemanager/ResourceManagerJobMasterTest.java:
##########
@@ -152,6 +161,39 @@ void testRegisterJobMaster() {
                 .isInstanceOf(JobMasterRegistrationSuccess.class);
     }
 
+    /**
+     * FLIP-588: if the delegation token manager rejects the job (its {@code 
registerJob} throws),
+     * the ResourceManager must reject the JobMaster registration so the job 
does not start without
+     * the tokens it requires. This also exercises the widened (6-arg) {@code 
registerJobMaster} RPC
+     * that carries the job {@link Configuration}.
+     */
+    @Test
+    void testRegisterJobMasterRejectedWhenDelegationTokenRegistrationFails() 
throws Exception {
+        // Rebuild the RM service with a delegation token manager that rejects 
registerJob.
+        resourceManagerService.rethrowFatalErrorIfAny();
+        resourceManagerService.cleanUp();
+        final FlinkRuntimeException failure =
+                new FlinkRuntimeException("registerJob rejected by provider");
+        createAndStartResourceManagerService(new 
RejectingDelegationTokenManager(failure));
+
+        final CompletableFuture<RegistrationResponse> registrationFuture =
+                resourceManagerGateway.registerJobMaster(
+                        jobMasterGateway.getFencingToken(),
+                        jobMasterResourceId,
+                        jobMasterGateway.getAddress(),
+                        jobId,
+                        new Configuration(),
+                        TIMEOUT);
+
+        final RegistrationResponse response =
+                registrationFuture.get(TIMEOUT.toMillis(), 
TimeUnit.MILLISECONDS);
+        assertThat(response).isInstanceOf(RegistrationResponse.Failure.class);
+        final Throwable reason = ((RegistrationResponse.Failure) 
response).getReason();
+        assertThat(reason.getMessage()).contains(jobId.toString());
+        assertThat(reason.getMessage()).contains("delegation token manager");
+        assertThat(reason.getCause().getMessage()).contains("registerJob 
rejected by provider");

Review Comment:
   Nice that this locks in the RPC-level failure response, but it doesn't cover 
what happens to `jobLeaderIdService` afterward. 
`jobLeaderIdService.addJob(jobId)` runs unconditionally before the 
delegation-token check and nothing removes it on this failure path, so the job 
stays tracked until either a retry succeeds (`containsJob` short-circuits 
`addJob` and the retry re-attempts `registerJob`) or the leader-id timeout 
fires. Could we add a test that retries registration after this failure and 
asserts it eventually succeeds end-to-end?



##########
flink-runtime/src/main/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManager.java:
##########
@@ -377,13 +623,14 @@ void stopTokensUpdate() {
             if (tokensUpdateFuture != null) {
                 tokensUpdateFuture.cancel(true);
                 tokensUpdateFuture = null;
+                nextScheduledAtMillis = Long.MAX_VALUE;
             }
         }
     }
 
     @VisibleForTesting

Review Comment:
   `currentRetryBackoff` and `lastKnownNextRenewal` are only ever read/written 
while `obtainLock` is held (`start()`, `startTokensUpdate()`, here), but 
neither is annotated. Should both be `@GuardedBy("obtainLock")` for consistency 
with the rest of the fields in this class.



##########
flink-runtime/src/main/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManager.java:
##########
@@ -416,13 +663,191 @@ long calculateRenewalDelay(Clock clock, long 
nextRenewal) {
         return renewalDelay;
     }
 
-    /** Stops re-occurring token obtain task. */
+    /**
+     * Stops the re-occurring token obtain task, releases the listener, and 
unregisters the jobs of
+     * the ending session. Providers stay usable for a later {@link 
#start(Listener)}. Their
+     * teardown happens in {@link #close()}.
+     */
     @Override
     public void stop() {
         LOG.info("Stopping credential renewal");
 
-        stopTokensUpdate();
+        synchronized (tokensUpdateFutureLock) {
+            // Mark not running, cancel the pending cycle, and reset the 
re-obtain bookkeeping
+            // atomically, so a re-obtain racing shutdown cannot schedule a 
cycle for a manager
+            // that is shutting down.
+            running = false;
+            stopTokensUpdate();
+            reobtainScheduled = false;
+            lastReobtainAtMillis = NO_PREVIOUS_REOBTAIN;
+            // Release the listener: keeping it would pin the disposed 
ResourceManager of a
+            // revoked leadership session, forever on a standby that never 
regains leadership.
+            listener = null;
+        }
+
+        // Unregister all jobs: running jobs re-register with the next 
session, ended jobs never
+        // would and their entries would leak in the providers.
+        for (JobID jobId : registeredJobs) {
+            try {
+                unregisterJobInternal(jobId);
+            } catch (Exception | LinkageError e) {
+                // Guards the cleanup against pathological errors from a 
broken plugin's
+                // serviceName().
+                LOG.error("Failed to unregister job {} while stopping the 
manager", jobId, e);
+            }
+        }
 
         LOG.info("Stopped credential renewal");
     }
+
+    /**
+     * Terminal teardown: ends any active session via {@link #stop()} and then 
stops all providers,
+     * exactly once. Called by the component that created the manager at 
process shutdown, not on
+     * ResourceManager leadership changes.
+     */
+    @Override
+    public void close() {
+        // Flip the flag before stopping anything. start() checks it under
+        // tokensUpdateFutureLock, so a racing start() either fails the check 
or has its
+        // session ended by the stop() below (see start()). At most one obtain 
may still
+        // overlap the provider stop() below, which the provider threading 
contract covers.
+        if (!closed.compareAndSet(false, true)) {
+            return;
+        }
+        stop();
+        for (DelegationTokenProvider provider : 
delegationTokenProviders.values()) {
+            try {
+                provider.stop();
+            } catch (Throwable t) {
+                LOG.error("Failed to stop delegation token provider {}", 
provider.serviceName(), t);
+            }
+        }
+    }
+
+    @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), "
+                                + "ignoring the request.");
+                return;
+            }
+            if (!running) {
+                LOG.debug(
+                        "A re-obtain of delegation tokens was requested while 
the manager is not "
+                                + "running (not started yet, or already 
stopped), ignoring the "
+                                + "request.");
+                return;
+            }
+            // An already scheduled re-obtain that has not started yet covers 
this request too.
+            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.relativeTimeMillis();
+            long delayMillis =
+                    lastReobtainAtMillis == NO_PREVIOUS_REOBTAIN
+                            ? 0L
+                            : Math.max(0L, lastReobtainAtMillis + 
reobtainCooldownMillis - now);
+            // Only bring the next cycle forward, never push a pending cycle 
later, or a
+            // short-lived token could expire before it is renewed. The 
nextScheduledAtMillis >
+            // now guard skips an already-fired future, so this never bypasses 
the cooldown.
+            if (tokensUpdateFuture != null
+                    && nextScheduledAtMillis > now
+                    && nextScheduledAtMillis - now < delayMillis) {
+                delayMillis = nextScheduledAtMillis - now;
+            }
+            // Anchor the cooldown to when the cycle will run, not to this 
request, so a request
+            // arriving right after a deferred cycle fired cannot run a second 
cycle back to back.
+            lastReobtainAtMillis = now + delayMillis;
+            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 {
+        // Hand providers a copy so plugin code cannot mutate the caller's 
live job configuration.
+        // clone() locks the backing map. Like the copy constructor, the copy 
is shallow.
+        final Configuration providerJobConfiguration = 
jobConfiguration.clone();
+        final boolean previouslyRegistered = registeredJobs.contains(jobId);
+        DelegationTokenProvider failedProvider = null;
+        try {
+            for (DelegationTokenProvider provider : 
delegationTokenProviders.values()) {
+                failedProvider = provider;
+                provider.registerJob(jobId, providerJobConfiguration);
+            }
+            registeredJobs.add(jobId);
+        } catch (Exception | LinkageError e) {
+            // LinkageError is included because provider plugin code can fail 
class resolution.
+            if (previouslyRegistered) {
+                // A failed re-registration must not roll back: the job 
registered successfully
+                // before and its tasks may still be running.
+                LOG.error(
+                        "Failed to re-register job {} for provider {}, keeping 
the previous "
+                                + "registration",
+                        jobId,
+                        failedProvider == null ? "<none>" : 
failedProvider.serviceName(),
+                        e);
+            } else {
+                // First registration: roll back from all providers 
(unregisterJob is idempotent).
+                // The rollback must never mask the original failure.
+                try {
+                    if (!unregisterJobInternal(jobId)) {
+                        // Keep the job tracked so stop() or a registration 
retry can release the
+                        // provider state left behind.
+                        registeredJobs.add(jobId);
+                    }
+                } catch (Exception | LinkageError rollbackException) {
+                    LOG.error(
+                            "Failed to roll back registration of job {}", 
jobId, rollbackException);
+                }
+                LOG.error(
+                        "Failed to register job {} for provider {}",
+                        jobId,
+                        failedProvider == null ? "<none>" : 
failedProvider.serviceName(),
+                        e);
+            }
+            throw e;

Review Comment:
   Not sure I get why we have this catch logic. I've the feeling that we want 
to hack around and be adaptive to wrongly implemented providers which throw 
exception in register/unregister. IIUC the actual code is:
   - Tracking half successful registrations
   - When unregister was not fully successful then it remains in the manager's 
list infinitely without any retry or GC
   
   My personal philosophy is that:
   - We should not track any job which has thrown exception in register. Do 
unregister, ignore exceptions with error and that's it.
   - If a provider throws exception in unregister then move on and drop from 
manager
   
   Maybe there is a reason to add code complexity like `previouslyRegistered` 
or similar but not yet get it. My general saying is that there must be a good 
reason to track partially successful registration because it can degrade 
stability and hard to debug.
   



##########
flink-runtime/src/main/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManager.java:
##########
@@ -139,12 +240,15 @@ public DefaultDelegationTokenManager(
     private Map<String, DelegationTokenProvider> loadProviders() {
         LOG.info("Loading delegation token providers");
 
+        // Handed to every provider so it can request an immediate re-obtain 
later, from any
+        // thread, decoupled from the registerJob call stack.
+        final DelegationTokenManagerCallback callback = 
this::reobtainDelegationTokens;
         Map<String, DelegationTokenProvider> providers = new HashMap<>();
         Consumer<DelegationTokenProvider> loadProvider =
                 (provider) -> {
                     try {
                         if (isProviderEnabled(configuration, 
provider.serviceName())) {
-                            provider.init(configuration);
+                            provider.init(configuration, callback);

Review Comment:
   We can just hardcode it, right?



##########
flink-runtime/src/main/java/org/apache/flink/runtime/resourcemanager/ResourceManager.java:
##########
@@ -427,6 +430,22 @@ public CompletableFuture<RegistrationResponse> 
registerJobMaster(
                             jobMasterIdFuture,
                             (JobMasterGateway jobMasterGateway, JobMasterId 
leadingJobMasterId) -> {
                                 if (Objects.equals(leadingJobMasterId, 
jobMasterId)) {
+                                    // Register with the delegation token 
manager first, so a
+                                    // provider failure rejects the 
registration and the job does
+                                    // not start without the tokens it 
requires. LinkageError is
+                                    // caught so a plugin classpath failure is 
reported the same
+                                    // way.
+                                    try {
+                                        
delegationTokenManager.registerJob(jobId, jobConfiguration);

Review Comment:
   To clarify my earlier comment on the test: I'm not claiming a prod bug here, 
this looks self-healing by design (retry skips `addJob` via `containsJob`, 
cleanup happens via `removeJob`/leader-id timeout otherwise). What's missing is 
bookkeeping test coverage: a test that fails delegation-token registration 
once, then retries, and asserts the retry completes end-to-end without leaving 
`jobLeaderIdService` in a duplicated or stale state. This path touches token 
delivery so I would like it locked in by a test rather than relying on 
inspection.



##########
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've not seen any such hardcode user who has done that but we can keep this 
to be on the safe side 🙂



##########
flink-runtime/src/main/java/org/apache/flink/runtime/security/token/DefaultDelegationTokenManager.java:
##########
@@ -416,13 +663,191 @@ long calculateRenewalDelay(Clock clock, long 
nextRenewal) {
         return renewalDelay;
     }
 
-    /** Stops re-occurring token obtain task. */
+    /**
+     * Stops the re-occurring token obtain task, releases the listener, and 
unregisters the jobs of
+     * the ending session. Providers stay usable for a later {@link 
#start(Listener)}. Their
+     * teardown happens in {@link #close()}.
+     */
     @Override
     public void stop() {
         LOG.info("Stopping credential renewal");
 
-        stopTokensUpdate();
+        synchronized (tokensUpdateFutureLock) {
+            // Mark not running, cancel the pending cycle, and reset the 
re-obtain bookkeeping
+            // atomically, so a re-obtain racing shutdown cannot schedule a 
cycle for a manager
+            // that is shutting down.
+            running = false;
+            stopTokensUpdate();
+            reobtainScheduled = false;
+            lastReobtainAtMillis = NO_PREVIOUS_REOBTAIN;
+            // Release the listener: keeping it would pin the disposed 
ResourceManager of a
+            // revoked leadership session, forever on a standby that never 
regains leadership.
+            listener = null;
+        }
+
+        // Unregister all jobs: running jobs re-register with the next 
session, ended jobs never
+        // would and their entries would leak in the providers.
+        for (JobID jobId : registeredJobs) {
+            try {
+                unregisterJobInternal(jobId);
+            } catch (Exception | LinkageError e) {
+                // Guards the cleanup against pathological errors from a 
broken plugin's
+                // serviceName().
+                LOG.error("Failed to unregister job {} while stopping the 
manager", jobId, e);
+            }
+        }
 
         LOG.info("Stopped credential renewal");
     }
+
+    /**
+     * Terminal teardown: ends any active session via {@link #stop()} and then 
stops all providers,
+     * exactly once. Called by the component that created the manager at 
process shutdown, not on
+     * ResourceManager leadership changes.
+     */
+    @Override
+    public void close() {
+        // Flip the flag before stopping anything. start() checks it under
+        // tokensUpdateFutureLock, so a racing start() either fails the check 
or has its
+        // session ended by the stop() below (see start()). At most one obtain 
may still
+        // overlap the provider stop() below, which the provider threading 
contract covers.
+        if (!closed.compareAndSet(false, true)) {
+            return;
+        }
+        stop();
+        for (DelegationTokenProvider provider : 
delegationTokenProviders.values()) {
+            try {
+                provider.stop();
+            } catch (Throwable t) {
+                LOG.error("Failed to stop delegation token provider {}", 
provider.serviceName(), t);
+            }
+        }
+    }
+
+    @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), "
+                                + "ignoring the request.");
+                return;
+            }
+            if (!running) {
+                LOG.debug(
+                        "A re-obtain of delegation tokens was requested while 
the manager is not "
+                                + "running (not started yet, or already 
stopped), ignoring the "
+                                + "request.");
+                return;
+            }
+            // An already scheduled re-obtain that has not started yet covers 
this request too.
+            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.relativeTimeMillis();
+            long delayMillis =
+                    lastReobtainAtMillis == NO_PREVIOUS_REOBTAIN
+                            ? 0L
+                            : Math.max(0L, lastReobtainAtMillis + 
reobtainCooldownMillis - now);
+            // Only bring the next cycle forward, never push a pending cycle 
later, or a
+            // short-lived token could expire before it is renewed. The 
nextScheduledAtMillis >
+            // now guard skips an already-fired future, so this never bypasses 
the cooldown.
+            if (tokensUpdateFuture != null
+                    && nextScheduledAtMillis > now
+                    && nextScheduledAtMillis - now < delayMillis) {
+                delayMillis = nextScheduledAtMillis - now;
+            }
+            // Anchor the cooldown to when the cycle will run, not to this 
request, so a request
+            // arriving right after a deferred cycle fired cannot run a second 
cycle back to back.
+            lastReobtainAtMillis = now + delayMillis;
+            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 {
+        // Hand providers a copy so plugin code cannot mutate the caller's 
live job configuration.
+        // clone() locks the backing map. Like the copy constructor, the copy 
is shallow.
+        final Configuration providerJobConfiguration = 
jobConfiguration.clone();
+        final boolean previouslyRegistered = registeredJobs.contains(jobId);
+        DelegationTokenProvider failedProvider = null;
+        try {
+            for (DelegationTokenProvider provider : 
delegationTokenProviders.values()) {
+                failedProvider = provider;
+                provider.registerJob(jobId, providerJobConfiguration);
+            }
+            registeredJobs.add(jobId);
+        } catch (Exception | LinkageError e) {
+            // LinkageError is included because provider plugin code can fail 
class resolution.
+            if (previouslyRegistered) {
+                // A failed re-registration must not roll back: the job 
registered successfully
+                // before and its tasks may still be running.
+                LOG.error(
+                        "Failed to re-register job {} for provider {}, keeping 
the previous "
+                                + "registration",
+                        jobId,
+                        failedProvider == null ? "<none>" : 
failedProvider.serviceName(),
+                        e);
+            } else {
+                // First registration: roll back from all providers 
(unregisterJob is idempotent).
+                // The rollback must never mask the original failure.
+                try {
+                    if (!unregisterJobInternal(jobId)) {
+                        // Keep the job tracked so stop() or a registration 
retry can release the
+                        // provider state left behind.
+                        registeredJobs.add(jobId);
+                    }
+                } catch (Exception | LinkageError rollbackException) {
+                    LOG.error(
+                            "Failed to roll back registration of job {}", 
jobId, rollbackException);
+                }
+                LOG.error(
+                        "Failed to register job {} for provider {}",
+                        jobId,
+                        failedProvider == null ? "<none>" : 
failedProvider.serviceName(),
+                        e);
+            }
+            throw e;
+        }
+    }
+
+    @Override
+    public void unregisterJob(JobID jobId) throws Exception {

Review Comment:
   Having a function which is not doing what it actually tells to do is just 
bad. `unregisterJobInternal` removes jobId under some circumstances. We must 
either unregister or throw exception but not silent ignore the function intent.



##########
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:
   I think previously I've not made distinction between `stop` and `close`. Let 
me explain my current understanding and correct me if I'm wrong. `stop` 
(together with `start`) can happen when HA kicks in during failover and it 
makes sure that new leader re-obtained tokens. `stop` has nothing to do with 
freeing resources allocated by the provider and this function called regularly. 
`close` happens during process shutdown and practically a single no way back 
action which should free resources. At least this is what I see from the 
manager from naming perspective. If this is true then maybe we can call 
`DelegationTokenProvider.stop` as `DelegationTokenProvider.close` just to have 
a single convention. If my understanding is correct then this is a one way 
process shutdown and good as-is and we shouldn't care about any race.



##########
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:
   Missed that part so this is fine.



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