mirror of
https://github.com/keycloak/keycloak.git
synced 2026-05-28 04:13:22 -04:00
Add SPI option to setup the start time of the workflows step runner task
Closes #47540 Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
This commit is contained in:
parent
87d8fbe521
commit
d24d2697aa
4 changed files with 79 additions and 4 deletions
|
|
@ -27,7 +27,14 @@ By default, the background task tracking due steps runs every 12 hours, but this
|
|||
* `spi-events-listener--workflow-event-listener--step-runner-task-interval`: Defines the interval at which the background task runs to check for workflow steps that are due for execution.
|
||||
It follows the same format used in the workflow step `after` field, where you can specify the interval as a number followed by a time unit (`ms`, `s` - default, `m`, `h`, `d`) or using the ISO-8601 duration format.
|
||||
|
||||
You can adjust this interval based on your realm's needs and the expected frequency of workflow executions.
|
||||
By default, the first execution of the background task occurs after one full interval has elapsed since the server started.
|
||||
You can control this by setting a start time that acts as an anchor for aligning executions to a predictable schedule:
|
||||
|
||||
* `spi-events-listener--workflow-event-listener--step-runner-task-start-time`: Defines the time of day used to align the background task executions, in `HH:mm` format (e.g., `02:00`, `14:30`), using the server's local timezone.
|
||||
|
||||
When a start time is set, the task interval is aligned to a grid of execution times anchored at the specified time. For example, with a start time of `02:00` and an interval of `12h`, the task always runs at 02:00 and 14:00 regardless of when the server was started. If the server starts at 10:30, the first execution would occur at 14:00.
|
||||
|
||||
You can adjust these options based on your realm's needs and the expected frequency of workflow executions.
|
||||
|
||||
== Configuring the task execution timeout
|
||||
|
||||
|
|
|
|||
|
|
@ -28,10 +28,26 @@ public interface TimerProvider extends Provider {
|
|||
|
||||
public void schedule(Runnable runnable, long intervalMillis, String taskName);
|
||||
|
||||
/**
|
||||
* Schedule a task with an initial delay that differs from the interval.
|
||||
*
|
||||
* @param runnable the task to run
|
||||
* @param initialDelayMillis delay before the first execution
|
||||
* @param intervalMillis interval between subsequent executions
|
||||
* @param taskName unique name for the task
|
||||
*/
|
||||
default void schedule(Runnable runnable, long initialDelayMillis, long intervalMillis, String taskName) {
|
||||
schedule(runnable, intervalMillis, taskName);
|
||||
}
|
||||
|
||||
default void schedule(TaskRunner runner, long intervalMillis) {
|
||||
schedule(runner, intervalMillis, runner.getTaskName());
|
||||
}
|
||||
|
||||
default void schedule(TaskRunner runner, long initialDelayMillis, long intervalMillis) {
|
||||
schedule(runner, initialDelayMillis, intervalMillis, runner.getTaskName());
|
||||
}
|
||||
|
||||
public void scheduleTask(ScheduledTask scheduledTask, long intervalMillis, String taskName);
|
||||
|
||||
public default void scheduleTask(ScheduledTask scheduledTask, long intervalMillis) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@
|
|||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
import org.keycloak.Config.Scope;
|
||||
import org.keycloak.common.Profile;
|
||||
|
|
@ -31,11 +34,16 @@ import org.keycloak.provider.ProviderEvent;
|
|||
import org.keycloak.services.scheduled.ClusterAwareScheduledTaskRunner;
|
||||
import org.keycloak.timer.TimerProvider;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
public class WorkflowsEventListenerFactory implements EventListenerProviderFactory, EnvironmentDependentProviderFactory {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(WorkflowsEventListenerFactory.class);
|
||||
|
||||
public static final String ID = "workflow-event-listener";
|
||||
private static final long DEFAULT_STEP_RUNNER_TASK_INTERVAL = Duration.ofHours(12).toMillis();
|
||||
private long stepRunnerTaskInterval;
|
||||
private LocalTime stepRunnerTaskStartTime;
|
||||
|
||||
@Override
|
||||
public EventListenerProvider create(KeycloakSession session) {
|
||||
|
|
@ -51,6 +59,16 @@ public class WorkflowsEventListenerFactory implements EventListenerProviderFacto
|
|||
public void init(Scope config) {
|
||||
String taskIntervalStr = config.get("stepRunnerTaskInterval");
|
||||
this.stepRunnerTaskInterval = taskIntervalStr == null ? DEFAULT_STEP_RUNNER_TASK_INTERVAL : DurationConverter.parseDuration(taskIntervalStr).toMillis();
|
||||
|
||||
String startTimeStr = config.get("stepRunnerTaskStartTime");
|
||||
if (startTimeStr != null) {
|
||||
try {
|
||||
this.stepRunnerTaskStartTime = LocalTime.parse(startTimeStr);
|
||||
} catch (DateTimeParseException e) {
|
||||
throw new IllegalArgumentException("Invalid stepRunnerTaskStartTime value '" + startTimeStr
|
||||
+ "'. Expected format: HH:mm (e.g., 02:00, 14:30)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -85,9 +103,38 @@ public class WorkflowsEventListenerFactory implements EventListenerProviderFacto
|
|||
}
|
||||
|
||||
private void scheduleStepRunnerTask(KeycloakSessionFactory factory) {
|
||||
long initialDelay = computeInitialDelay();
|
||||
|
||||
try (KeycloakSession session = factory.create()) {
|
||||
TimerProvider timer = session.getProvider(TimerProvider.class);
|
||||
timer.schedule(new ClusterAwareScheduledTaskRunner(factory, new WorkflowRunnerScheduledTask(factory), stepRunnerTaskInterval), stepRunnerTaskInterval);
|
||||
ClusterAwareScheduledTaskRunner runner = new ClusterAwareScheduledTaskRunner(factory,
|
||||
new WorkflowRunnerScheduledTask(factory), stepRunnerTaskInterval);
|
||||
timer.schedule(runner, initialDelay, stepRunnerTaskInterval);
|
||||
}
|
||||
|
||||
ZonedDateTime nextExecution = ZonedDateTime.now().plus(Duration.ofMillis(initialDelay));
|
||||
logger.infof("Workflow runner task scheduled: next execution at %s, then every %s",
|
||||
nextExecution.toLocalTime().withNano(0),
|
||||
Duration.ofMillis(stepRunnerTaskInterval));
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the initial delay before the first task execution.
|
||||
* <p>
|
||||
* If a start time is configured, it is used as an anchor to align executions to a predictable
|
||||
* schedule. For example, with a start time of 18:00 and an interval of 2 hours, the execution
|
||||
* grid is 00:00, 02:00, 04:00, ..., 16:00, 18:00, 20:00, 22:00. The initial delay is calculated
|
||||
* so that the first execution occurs at the next grid point after the current time.
|
||||
* <p>
|
||||
* If no start time is configured, the initial delay equals the interval (current default behavior).
|
||||
*/
|
||||
long computeInitialDelay() {
|
||||
if (stepRunnerTaskStartTime == null) {
|
||||
return stepRunnerTaskInterval;
|
||||
}
|
||||
ZonedDateTime now = ZonedDateTime.now();
|
||||
ZonedDateTime anchor = now.toLocalDate().atTime(stepRunnerTaskStartTime).atZone(now.getZone());
|
||||
long millisPastLastGridPoint = Math.floorMod(Duration.between(anchor, now).toMillis(), stepRunnerTaskInterval);
|
||||
return stepRunnerTaskInterval - millisPastLastGridPoint;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,11 @@ public class BasicTimerProvider implements TimerProvider {
|
|||
|
||||
@Override
|
||||
public void schedule(final Runnable runnable, final long intervalMillis, String taskName) {
|
||||
schedule(runnable, intervalMillis, intervalMillis, taskName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void schedule(final Runnable runnable, final long initialDelayMillis, final long intervalMillis, String taskName) {
|
||||
TimerTask task = new BasicTimerTask(runnable);
|
||||
|
||||
TimerTaskContextImpl taskContext = new TimerTaskContextImpl(runnable, task, Time.currentTimeMillis(), intervalMillis);
|
||||
|
|
@ -61,8 +66,8 @@ public class BasicTimerProvider implements TimerProvider {
|
|||
existingTask.timerTask.cancel();
|
||||
}
|
||||
|
||||
logger.debugf("Starting task '%s' with interval '%d'", taskName, intervalMillis);
|
||||
timer.schedule(task, intervalMillis, intervalMillis);
|
||||
logger.debugf("Starting task '%s' with initial delay '%d' and interval '%d'", taskName, initialDelayMillis, intervalMillis);
|
||||
timer.schedule(task, initialDelayMillis, intervalMillis);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
Loading…
Reference in a new issue