feat: schedule due processing from next dueAt instead of polling

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
0264408
2026-09-20 16:54:13 +08:00
co-authored by Cursor
parent 8d08a2840b
commit 02557a9b86
15 changed files with 183 additions and 12 deletions
@@ -5,24 +5,35 @@ import org.springframework.context.SmartLifecycle;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.time.Clock;
import java.time.Instant;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/** Optional poller that calls {@link OrdoEngine#processDue(int)} when {@code ordo.due.poll-ms} is positive. */
/** Optional scheduler that drains {@link OrdoEngine#processDue(int)} when {@code ordo.due.poll-ms} is positive. */
public final class OrdoDuePoller implements SmartLifecycle {
private static final Logger LOG = System.getLogger("ordo");
private static final int BATCH_SIZE = 100;
private final OrdoEngine ordoEngine;
private final Clock clock;
private final long pollMs;
private final int batchSize;
private ScheduledExecutorService executor;
private ScheduledFuture<?> scheduled;
private volatile boolean running;
private volatile boolean ticking;
public OrdoDuePoller(OrdoEngine ordoEngine, long pollMs) {
public OrdoDuePoller(OrdoEngine ordoEngine, Clock clock, long pollMs, int batchSize) {
this.ordoEngine = Objects.requireNonNull(ordoEngine, "ordoEngine must not be null");
this.clock = Objects.requireNonNull(clock, "clock must not be null");
this.pollMs = pollMs;
if (batchSize <= 0) {
throw new IllegalArgumentException("batch size must be positive");
}
this.batchSize = batchSize;
}
@Override
@@ -40,18 +51,51 @@ public final class OrdoDuePoller implements SmartLifecycle {
thread.setDaemon(true);
return thread;
});
executor.scheduleWithFixedDelay(this::tick, pollMs, pollMs, TimeUnit.MILLISECONDS);
running = true;
schedule(0);
}
void wake() {
if (!running || ticking) {
return;
}
schedule(0);
}
private void tick() {
ticking = true;
try {
ordoEngine.processDue(BATCH_SIZE);
int processed;
do {
processed = ordoEngine.processDue(batchSize);
} while (processed == batchSize);
} catch (RuntimeException e) {
LOG.log(Level.WARNING, "processDue failed", e);
} finally {
if (running) {
schedule(nextDelayMs());
}
ticking = false;
}
}
private long nextDelayMs() {
Instant now = clock.instant();
return ordoEngine.nextDueAt()
.map(dueAt -> Math.min(Math.max(0L, dueAt.toEpochMilli() - now.toEpochMilli()), pollMs))
.orElse(pollMs);
}
private synchronized void schedule(long delayMs) {
if (!running || executor == null) {
return;
}
if (scheduled != null) {
scheduled.cancel(false);
}
scheduled = executor.schedule(this::tick, delayMs, TimeUnit.MILLISECONDS);
}
@Override
public void stop() {
running = false;
@@ -59,6 +103,7 @@ public final class OrdoDuePoller implements SmartLifecycle {
executor.shutdownNow();
executor = null;
}
scheduled = null;
}
@Override
@@ -0,0 +1,25 @@
package com.jetlumen.ordo.spring;
import com.jetlumen.ordo.api.runtime.ProcessEvent;
import com.jetlumen.ordo.api.runtime.ProcessEventType;
import com.jetlumen.ordo.api.spi.OrdoEventListener;
/** Forwards {@code TASK_CREATED} to {@link OrdoDuePoller} after the engine listener snapshot is taken. */
public final class OrdoDueWakeBridge implements OrdoEventListener {
private volatile OrdoDuePoller poller;
public void attach(OrdoDuePoller poller) {
this.poller = poller;
}
@Override
public void onEvent(ProcessEvent event) {
if (event.type() != ProcessEventType.TASK_CREATED) {
return;
}
OrdoDuePoller current = poller;
if (current != null) {
current.wake();
}
}
}
@@ -51,9 +51,12 @@ public class OrdoProperties {
}
public static class Due {
/** Poll interval in milliseconds. {@code 0} disables scheduling. */
/** Enables scheduling when positive; also the maximum idle sleep in milliseconds. */
private long pollMs;
/** Tasks claimed per {@code processDue} call while draining. */
private int batchSize = 100;
public long getPollMs() {
return pollMs;
}
@@ -61,6 +64,14 @@ public class OrdoProperties {
public void setPollMs(long pollMs) {
this.pollMs = pollMs;
}
public int getBatchSize() {
return batchSize;
}
public void setBatchSize(int batchSize) {
this.batchSize = batchSize;
}
}
public static class Jdbc {
@@ -19,6 +19,7 @@ import com.jetlumen.ordo.core.spi.DispatchingActionHandler;
import com.jetlumen.ordo.core.spi.DispatchingRoutingCondition;
import com.jetlumen.ordo.spring.OrdoDefinitionLoader;
import com.jetlumen.ordo.spring.OrdoDuePoller;
import com.jetlumen.ordo.spring.OrdoDueWakeBridge;
import com.jetlumen.ordo.spring.OrdoProperties;
import com.jetlumen.ordo.storage.jdbc.JdbcActionExecutionRepository;
import com.jetlumen.ordo.storage.jdbc.JdbcApprovalTaskRepository;
@@ -152,6 +153,12 @@ public class OrdoJdbcAutoConfiguration {
return new JdbcInstanceTokenRepository(connectionProvider, ordoSqlDialect);
}
@Bean
@ConditionalOnMissingBean
public OrdoDueWakeBridge ordoDueWakeBridge() {
return new OrdoDueWakeBridge();
}
@Bean
@ConditionalOnMissingBean
public OrdoEngine ordoEngine(Clock ordoClock,
@@ -181,7 +188,11 @@ public class OrdoJdbcAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public OrdoDuePoller ordoDuePoller(OrdoEngine ordoEngine, OrdoProperties ordoProperties) {
return new OrdoDuePoller(ordoEngine, ordoProperties.getDue().getPollMs());
public OrdoDuePoller ordoDuePoller(OrdoEngine ordoEngine, Clock ordoClock, OrdoProperties ordoProperties,
OrdoDueWakeBridge ordoDueWakeBridge) {
OrdoDuePoller poller = new OrdoDuePoller(ordoEngine, ordoClock, ordoProperties.getDue().getPollMs(),
ordoProperties.getDue().getBatchSize());
ordoDueWakeBridge.attach(poller);
return poller;
}
}