feat: schedule due processing from next dueAt instead of polling
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+50
-5
@@ -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
|
||||
|
||||
+25
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-1
@@ -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 {
|
||||
|
||||
+13
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user