com.aspectran.utils.timer.CyclicTimeout Maven / Gradle / Ivy
/*
* Copyright (c) 2008-2024 The Aspectran Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.aspectran.utils.timer;
import com.aspectran.utils.annotation.jsr305.NonNull;
import com.aspectran.utils.logging.Logger;
import com.aspectran.utils.logging.LoggerFactory;
import com.aspectran.utils.thread.Scheduler;
import javax.security.auth.Destroyable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static java.lang.Long.MAX_VALUE;
/**
* This class is a clone of org.eclipse.jetty.io.CyclicTimeout
*
* An abstract implementation of a timeout.
* Subclasses should implement {@link #onTimeoutExpired()}.
* This implementation is optimised assuming that the timeout
* will mostly be cancelled and then reused with a similar value.
* This implementation has a {@link Timeout} holding the time
* at which the scheduled task should fire, and a linked list of
* {@link Wakeup}, each holding the actual scheduled task.
* Calling {@link #schedule(long, TimeUnit)} the first time will
* create a Timeout with an associated Wakeup and submit a task to
* the scheduler.
* Calling {@link #schedule(long, TimeUnit)} again with the same or
* a larger delay will cancel the previous Timeout, but keep the
* previous Wakeup without submitting a new task to the scheduler,
* therefore reducing the pressure on the scheduler and avoid it
* becomes a bottleneck.
* When the Wakeup task fires, it will see that the Timeout is now
* in the future and will attach a new Wakeup with the future time
* to the Timeout, and submit a scheduler task for the new Wakeup.
*/
public abstract class CyclicTimeout implements Destroyable {
private static final Logger logger = LoggerFactory.getLogger(CyclicTimeout.class);
private static final Timeout NOT_SET = new Timeout(MAX_VALUE, null);
private static final Scheduler.Task DESTROYED = () -> false;
/* The underlying scheduler to use */
private final Scheduler scheduler;
/* Reference to the current Timeout and chain of Wakeup */
private final AtomicReference timeout = new AtomicReference<>(NOT_SET);
/**
* @param scheduler A scheduler used to schedule wakeups
*/
public CyclicTimeout(Scheduler scheduler) {
this.scheduler = scheduler;
}
public Scheduler getScheduler() {
return scheduler;
}
/**
* Schedules a timeout, even if already set, cancelled or expired.
* If a timeout is already set, it will be cancelled and replaced
* by the new one.
* @param delay The period of time before the timeout expires.
* @param units The unit of time of the period.
* @return true if the timeout was already set.
*/
public boolean schedule(long delay, @NonNull TimeUnit units) {
long now = System.nanoTime();
long newTimeoutAt = now + units.toNanos(delay);
Wakeup newWakeup = null;
boolean result;
while (true) {
Timeout timeout = this.timeout.get();
result = (timeout.at != MAX_VALUE);
// Is the current wakeup good to use? ie before our timeout time?
Wakeup wakeup = timeout.wakeup;
if (wakeup == null || wakeup.at > newTimeoutAt) {
// No, we need an earlier wakeup.
wakeup = newWakeup = new Wakeup(newTimeoutAt, wakeup);
}
if (this.timeout.compareAndSet(timeout, new Timeout(newTimeoutAt, wakeup))) {
if (logger.isTraceEnabled()) {
logger.trace("Installed timeout in " + units.toMillis(delay) + " ms, " +
(newWakeup != null ? "new" : "existing") + " waking up in " +
TimeUnit.NANOSECONDS.toMillis(wakeup.at - now) + " ms");
}
break;
}
}
// If we created a new wakeup, we need to actually schedule it.
// Any wakeup that is created and discarded by the failed CAS will not be
// in the wakeup chain, will not have a scheduler task set and will be GC'd.
if (newWakeup != null) {
newWakeup.schedule(now);
}
return result;
}
/**
* Cancels this CyclicTimeout so that it won't expire.
* After being cancelled, this CyclicTimeout can be scheduled again.
* @return true if this CyclicTimeout was scheduled to expire
* @see #destroy()
*/
public boolean cancel() {
boolean result;
while (true) {
Timeout timeout = this.timeout.get();
result = (timeout.at != MAX_VALUE);
Wakeup wakeup = timeout.wakeup;
Timeout newTimeout = (wakeup == null ? NOT_SET : new Timeout(MAX_VALUE, wakeup));
if (this.timeout.compareAndSet(timeout, newTimeout)) {
break;
}
}
return result;
}
/**
* Invoked when the timeout expires.
*/
public abstract void onTimeoutExpired();
/**
* Destroys this CyclicTimeout.
* After being destroyed, this CyclicTimeout is not used anymore.
*/
@Override
public void destroy() {
Timeout timeout = this.timeout.getAndSet(NOT_SET);
Wakeup wakeup = (timeout != null ? timeout.wakeup : null);
while (wakeup != null) {
wakeup.destroy();
wakeup = wakeup.next;
}
}
/**
* A timeout time with a link to a Wakeup chain.
*/
private static class Timeout {
private final long at;
private final Wakeup wakeup;
private Timeout(long timeoutAt, Wakeup wakeup) {
this.at = timeoutAt;
this.wakeup = wakeup;
}
@Override
public String toString() {
return String.format("%s@%x:%dms,%s",
getClass().getSimpleName(),
hashCode(),
TimeUnit.NANOSECONDS.toMillis(at),
wakeup);
}
}
/**
* A Wakeup chain of real scheduler tasks.
*/
private class Wakeup implements Runnable {
private final AtomicReference task = new AtomicReference<>();
private final long at;
private final Wakeup next;
private Wakeup(long wakeupAt, Wakeup next) {
this.at = wakeupAt;
this.next = next;
}
private void schedule(long now) {
task.compareAndSet(null, scheduler.schedule(this, at - now, TimeUnit.NANOSECONDS));
}
private void destroy() {
Scheduler.Task task = this.task.getAndSet(DESTROYED);
if (task != null) {
task.cancel();
}
}
@Override
public void run() {
long now = System.nanoTime();
Wakeup newWakeup = null;
boolean hasExpired = false;
while (true) {
Timeout timeout = CyclicTimeout.this.timeout.get();
// We must look for ourselves in the current wakeup list.
// If we find ourselves, then we act and we use our tail for any new
// wakeup list, effectively removing any wakeup before us in the list (and making them no-ops).
// If we don't find ourselves, then a wakeup that should have expired after us has already run
// and removed us from the list, so we become a noop.
Wakeup wakeup = timeout.wakeup;
while (wakeup != null) {
if (wakeup == this) {
break;
}
// Not us, so look at next wakeup in the list.
wakeup = wakeup.next;
}
if (wakeup == null) {
// Not found, we become a noop.
return;
}
// We are in the wakeup list! So we have to act and we know our
// tail has not expired (else it would have removed us from the list).
// Remove ourselves (and any prior Wakeup) from the wakeup list.
wakeup = wakeup.next;
Timeout newTimeout;
if (timeout.at <= now) {
// We have timed out!
hasExpired = true;
newTimeout = (wakeup == null ? NOT_SET : new Timeout(MAX_VALUE, wakeup));
} else if (timeout.at != MAX_VALUE) {
// We have not timed out, but we are set to!
// Is the current wakeup good to use? ie before our timeout time?
if (wakeup == null || wakeup.at >= timeout.at) {
// No, we need an earlier wakeup.
wakeup = newWakeup = new Wakeup(timeout.at, wakeup);
}
newTimeout = new Timeout(timeout.at, wakeup);
} else {
// We don't timeout, preserve scheduled chain.
newTimeout = (wakeup == null ? NOT_SET : new Timeout(MAX_VALUE, wakeup));
}
// Loop until we succeed in changing state or we are a noop!
if (CyclicTimeout.this.timeout.compareAndSet(timeout, newTimeout)) {
break;
}
}
// If we created a new wakeup, we need to actually schedule it.
if (newWakeup != null) {
newWakeup.schedule(now);
}
// If we expired, then do the callback.
if (hasExpired) {
onTimeoutExpired();
}
}
@Override
public String toString() {
return String.format("%s@%x:%dms->%s",
getClass().getSimpleName(),
hashCode(),
(at == MAX_VALUE ? at : TimeUnit.NANOSECONDS.toMillis(at)),
next);
}
}
}