ARTEMIS-2926 Scheduled task executions are skipped randomly
Making Scheduled task to be more reliable when using scheduledComponent.delay() method and saving periodic tasks to be skipped although on correct timing
This commit is contained in:
parent
647151b0af
commit
e2c1848da4
|
@ -20,11 +20,12 @@ package org.apache.activemq.artemis.core.server;
|
||||||
import java.security.AccessController;
|
import java.security.AccessController;
|
||||||
import java.security.PrivilegedAction;
|
import java.security.PrivilegedAction;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
import org.apache.activemq.artemis.utils.ActiveMQThreadFactory;
|
import org.apache.activemq.artemis.utils.ActiveMQThreadFactory;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
@ -41,15 +42,13 @@ public abstract class ActiveMQScheduledComponent implements ActiveMQComponent, R
|
||||||
/** initialDelay < 0 would mean no initial delay, use the period instead */
|
/** initialDelay < 0 would mean no initial delay, use the period instead */
|
||||||
private long initialDelay;
|
private long initialDelay;
|
||||||
private long period;
|
private long period;
|
||||||
private long millisecondsPeriod;
|
|
||||||
private TimeUnit timeUnit;
|
private TimeUnit timeUnit;
|
||||||
private final Executor executor;
|
private final Executor executor;
|
||||||
private volatile ScheduledFuture future;
|
private volatile boolean isStarted;
|
||||||
|
private ScheduledFuture future;
|
||||||
private final boolean onDemand;
|
private final boolean onDemand;
|
||||||
|
// The start/stop actions shouldn't interact concurrently with delay so it doesn't need to be volatile
|
||||||
long lastTime = 0;
|
private AtomicBoolean bookedForRunning;
|
||||||
|
|
||||||
private final AtomicInteger delayed = new AtomicInteger(0);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It creates a scheduled component that can trigger {@link #run()} with a fixed {@code checkPeriod} on a configured {@code executor}.
|
* It creates a scheduled component that can trigger {@link #run()} with a fixed {@code checkPeriod} on a configured {@code executor}.
|
||||||
|
@ -73,6 +72,8 @@ public abstract class ActiveMQScheduledComponent implements ActiveMQComponent, R
|
||||||
this.period = checkPeriod;
|
this.period = checkPeriod;
|
||||||
this.timeUnit = timeUnit;
|
this.timeUnit = timeUnit;
|
||||||
this.onDemand = onDemand;
|
this.onDemand = onDemand;
|
||||||
|
this.bookedForRunning = new AtomicBoolean(false);
|
||||||
|
this.isStarted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -89,12 +90,7 @@ public abstract class ActiveMQScheduledComponent implements ActiveMQComponent, R
|
||||||
long checkPeriod,
|
long checkPeriod,
|
||||||
TimeUnit timeUnit,
|
TimeUnit timeUnit,
|
||||||
boolean onDemand) {
|
boolean onDemand) {
|
||||||
this.executor = null;
|
this(scheduledExecutorService, null, initialDelay, checkPeriod, timeUnit, onDemand);
|
||||||
this.scheduledExecutorService = scheduledExecutorService;
|
|
||||||
this.initialDelay = initialDelay;
|
|
||||||
this.period = checkPeriod;
|
|
||||||
this.timeUnit = timeUnit;
|
|
||||||
this.onDemand = onDemand;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -150,11 +146,11 @@ public abstract class ActiveMQScheduledComponent implements ActiveMQComponent, R
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized void start() {
|
public synchronized void start() {
|
||||||
if (future != null) {
|
if (isStarted) {
|
||||||
// already started
|
// already started
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
isStarted = true;
|
||||||
if (scheduledExecutorService == null) {
|
if (scheduledExecutorService == null) {
|
||||||
scheduledExecutorService = new ScheduledThreadPoolExecutor(1, getThreadFactory());
|
scheduledExecutorService = new ScheduledThreadPoolExecutor(1, getThreadFactory());
|
||||||
startedOwnScheduler = true;
|
startedOwnScheduler = true;
|
||||||
|
@ -165,10 +161,9 @@ public abstract class ActiveMQScheduledComponent implements ActiveMQComponent, R
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.millisecondsPeriod = timeUnit.convert(period, TimeUnit.MILLISECONDS);
|
|
||||||
|
|
||||||
if (period >= 0) {
|
if (period >= 0) {
|
||||||
future = scheduledExecutorService.scheduleWithFixedDelay(runForScheduler, initialDelay >= 0 ? initialDelay : period, period, timeUnit);
|
final AtomicBoolean booked = this.bookedForRunning;
|
||||||
|
future = scheduledExecutorService.scheduleWithFixedDelay(() -> runForScheduler(booked), initialDelay >= 0 ? initialDelay : period, period, timeUnit);
|
||||||
} else {
|
} else {
|
||||||
logger.tracef("did not start scheduled executor on %s because period was configured as %d", this, period);
|
logger.tracef("did not start scheduled executor on %s because period was configured as %d", this, period);
|
||||||
}
|
}
|
||||||
|
@ -188,15 +183,26 @@ public abstract class ActiveMQScheduledComponent implements ActiveMQComponent, R
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void delay() {
|
/**
|
||||||
int value = delayed.incrementAndGet();
|
* A delay request can succeed only if:
|
||||||
if (value > 10) {
|
* <ul>
|
||||||
delayed.decrementAndGet();
|
* <li>there is no other pending delay request
|
||||||
} else {
|
* <li>there is no pending execution request
|
||||||
// We only schedule up to 10 periods upfront.
|
* </ul>
|
||||||
// this is to avoid a window where a current one would be running and a next one is coming.
|
* <p>
|
||||||
// in theory just 2 would be enough. I'm using 10 as a precaution here.
|
* When a delay request succeed it schedule a new execution to happen in {@link #getPeriod()}.<br>
|
||||||
scheduledExecutorService.schedule(runForScheduler, Math.min(period, period * value), timeUnit);
|
*/
|
||||||
|
public boolean delay() {
|
||||||
|
final AtomicBoolean booked = this.bookedForRunning;
|
||||||
|
if (!booked.compareAndSet(false, true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
scheduledExecutorService.schedule(() -> bookedRunForScheduler(booked), period, timeUnit);
|
||||||
|
return true;
|
||||||
|
} catch (RejectedExecutionException e) {
|
||||||
|
booked.set(false);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -261,7 +267,14 @@ public abstract class ActiveMQScheduledComponent implements ActiveMQComponent, R
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stop() {
|
public synchronized void stop() {
|
||||||
|
if (!isStarted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isStarted = false;
|
||||||
|
// Replace the existing one: a new periodic task or any new delay after stop
|
||||||
|
// won't interact with the previously running ones
|
||||||
|
this.bookedForRunning = new AtomicBoolean(false);
|
||||||
if (future != null) {
|
if (future != null) {
|
||||||
future.cancel(false);
|
future.cancel(false);
|
||||||
future = null;
|
future = null;
|
||||||
|
@ -275,8 +288,8 @@ public abstract class ActiveMQScheduledComponent implements ActiveMQComponent, R
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized boolean isStarted() {
|
public boolean isStarted() {
|
||||||
return future != null;
|
return isStarted;
|
||||||
}
|
}
|
||||||
|
|
||||||
// this will restart the scheduled component upon changes
|
// this will restart the scheduled component upon changes
|
||||||
|
@ -287,35 +300,43 @@ public abstract class ActiveMQScheduledComponent implements ActiveMQComponent, R
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final Runnable runForExecutor = new Runnable() {
|
private void runForExecutor(AtomicBoolean booked) {
|
||||||
@Override
|
// It unblocks:
|
||||||
public void run() {
|
// - a new delay request
|
||||||
if (onDemand && delayed.get() > 0) {
|
// - next periodic run request (in case of executor != null)
|
||||||
delayed.decrementAndGet();
|
// Although tempting, don't move this one after ActiveMQScheduledComponent.this.run():
|
||||||
}
|
// - it can cause "delay" to change semantic ie a racing delay while finished executing the task, won't succeed
|
||||||
|
// - it won't prevent "slow tasks" to accumulate, because slowness cannot be measured inside running method;
|
||||||
if (!onDemand && lastTime > 0) {
|
// it just cause skipping runs for perfectly timed executions too
|
||||||
if (System.currentTimeMillis() - lastTime < millisecondsPeriod) {
|
boolean alwaysTrue = booked.compareAndSet(true, false);
|
||||||
logger.trace("Execution ignored due to too many simultaneous executions, probably a previous delayed execution");
|
assert alwaysTrue;
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastTime = System.currentTimeMillis();
|
|
||||||
|
|
||||||
ActiveMQScheduledComponent.this.run();
|
ActiveMQScheduledComponent.this.run();
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
final Runnable runForScheduler = new Runnable() {
|
private void bookedRunForScheduler(AtomicBoolean booked) {
|
||||||
@Override
|
assert booked.get();
|
||||||
public void run() {
|
|
||||||
if (executor != null) {
|
if (executor != null) {
|
||||||
executor.execute(runForExecutor);
|
try {
|
||||||
|
executor.execute(() -> runForExecutor(booked));
|
||||||
|
} catch (RejectedExecutionException e) {
|
||||||
|
if (booked != null) {
|
||||||
|
booked.set(false);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
runForExecutor.run();
|
runForExecutor(booked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
private void runForScheduler(AtomicBoolean booked) {
|
||||||
|
if (!booked.compareAndSet(false, true)) {
|
||||||
|
// let's skip this execution because there is:
|
||||||
|
// - a previously submitted period task yet to start -> executor is probably overbooked!
|
||||||
|
// - a pending delay request
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bookedRunForScheduler(booked);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,22 @@ public class ActiveMQScheduledComponentTest {
|
||||||
Assert.assertTrue("just because one took a lot of time, it doesn't mean we can accumulate many, we got " + count + " executions", count.get() < 5);
|
Assert.assertTrue("just because one took a lot of time, it doesn't mean we can accumulate many, we got " + count + " executions", count.get() < 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSubMillisDelay() throws InterruptedException {
|
||||||
|
final CountDownLatch triggered = new CountDownLatch(2);
|
||||||
|
final long nsInterval = TimeUnit.MICROSECONDS.toNanos(900);
|
||||||
|
final ActiveMQScheduledComponent local = new ActiveMQScheduledComponent(scheduledExecutorService, executorService, nsInterval, TimeUnit.NANOSECONDS, false) {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
triggered.countDown();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
local.start();
|
||||||
|
Assert.assertTrue(triggered.await(10, TimeUnit.SECONDS));
|
||||||
|
local.stop();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testVerifyInitialDelayChanged() {
|
public void testVerifyInitialDelayChanged() {
|
||||||
final long initialDelay = 10;
|
final long initialDelay = 10;
|
||||||
|
|
Loading…
Reference in New Issue