All Downloads are FREE. Search and download functionalities are using the official Maven repository.

reactor.test.scheduler.VirtualTimeScheduler Maven / Gradle / Ivy

There is a newer version: 3.7.0
Show newest version
/*
 * Copyright (c) 2017-2023 VMware Inc. or its affiliates, All Rights Reserved.
 *
 * 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
 *
 *   https://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 reactor.test.scheduler;

import java.time.Duration;
import java.time.Instant;
import java.util.Queue;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

import reactor.core.Disposable;
import reactor.core.Disposables;
import reactor.core.Exceptions;
import reactor.core.publisher.Operators;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import reactor.util.annotation.Nullable;
import reactor.util.concurrent.Queues;

/**
 * A {@link Scheduler} that uses a virtual clock, allowing to manipulate time
 * (eg. in tests). Can replace the default reactor schedulers by using
 * the {@link #getOrSet} / {@link #set(VirtualTimeScheduler)} methods.
 *
 * @author Stephane Maldini
 */
public class VirtualTimeScheduler implements Scheduler {

	/**
	 * Create a new {@link VirtualTimeScheduler} without enabling it. Call
	 * {@link #getOrSet(VirtualTimeScheduler)} to enable it on
	 * {@link reactor.core.scheduler.Schedulers.Factory} factories.
	 *
	 * @return a new {@link VirtualTimeScheduler} intended for timed-only
	 * {@link Schedulers} factories.
	 */
	public static VirtualTimeScheduler create() {
		return create(false);
	}

	/**
	 * Create a new {@link VirtualTimeScheduler} without enabling it. Call
	 * {@link #getOrSet(VirtualTimeScheduler)} to enable it on
	 * {@link reactor.core.scheduler.Schedulers.Factory} factories.
	 *
	 * @param defer true to defer all clock move operations until there are tasks in queue
	 *
	 * @return a new {@link VirtualTimeScheduler} intended for timed-only
	 * {@link Schedulers} factories.
	 */
	public static VirtualTimeScheduler create(boolean defer) {
		VirtualTimeScheduler instance = new VirtualTimeScheduler(defer);
		instance.init();
		return instance;
	}

	/**
	 * Assign a newly created {@link VirtualTimeScheduler} to all {@link reactor.core.scheduler.Schedulers.Factory}
	 * factories ONLY if no {@link VirtualTimeScheduler} is currently set. In case of scheduler creation,
	 * there is no deferring of time operations (see {@link #create(boolean)}.
	 * Note that prior to replacing the factories, a {@link Schedulers#setFactoryWithSnapshot(Schedulers.Factory) snapshot}
	 * will be performed. Resetting the factory will restore said snapshot.
	 * 

* While this methods makes best effort to be thread safe, it is usually advised to * perform such wide-impact setup serially and BEFORE all test code runs * (setup/beforeAll/beforeClass...). The created Scheduler is returned. * * @return the VirtualTimeScheduler that was created and set through the factory */ public static VirtualTimeScheduler getOrSet() { return enable(VirtualTimeScheduler::create, false); } /** * Assign a newly created {@link VirtualTimeScheduler} to all {@link reactor.core.scheduler.Schedulers.Factory} * factories ONLY if no {@link VirtualTimeScheduler} is currently set. In case of scheduler creation, * there is opt-in deferring of time related operations (see {@link #create(boolean)}. * Note that prior to replacing the factories, a {@link Schedulers#setFactoryWithSnapshot(Schedulers.Factory) snapshot} * will be performed. Resetting the factory will restore said snapshot. *

* While this methods makes best effort to be thread safe, it is usually advised to * perform such wide-impact setup serially and BEFORE all test code runs * (setup/beforeAll/beforeClass...). The created Scheduler is returned. * * @param defer true to defer all clock move operations until there are tasks in queue, if a scheduler is created * @return the VirtualTimeScheduler that was created and set through the factory * @see #create(boolean) */ public static VirtualTimeScheduler getOrSet(final boolean defer) { return enable(() -> VirtualTimeScheduler.create(defer), false); } /** * Assign an externally created {@link VirtualTimeScheduler} to the relevant * {@link reactor.core.scheduler.Schedulers.Factory} factories, depending on how it was created (see * {@link #create()} and {@link #create()}). Note that the returned scheduler * should always be captured and used going forward, as the provided scheduler can be * superseded by a matching scheduler that has already been enabled. * Note also that prior to replacing the factories, a {@link Schedulers#setFactoryWithSnapshot(Schedulers.Factory) snapshot} * will be performed. Resetting the factory will restore said snapshot. *

* While this methods makes best effort to be thread safe, it is usually advised to * perform such wide-impact setup serially and BEFORE all test code runs * (setup/beforeAll/beforeClass...). The actual enabled Scheduler is returned. * * @param scheduler the {@link VirtualTimeScheduler} to use in factories. * @return the enabled VirtualTimeScheduler (can be different from the provided one) */ public static VirtualTimeScheduler getOrSet(VirtualTimeScheduler scheduler) { return enable(() -> scheduler, false); } /** * Assign an externally created {@link VirtualTimeScheduler} to the relevant * {@link reactor.core.scheduler.Schedulers.Factory} factories, depending on how it was created (see * {@link #create()} and {@link #create()}). Contrary to {@link #getOrSet(VirtualTimeScheduler)}, * the provided scheduler is always used, even if a matching scheduler is currently enabled. * Note that prior to replacing the factories, a {@link Schedulers#setFactoryWithSnapshot(Schedulers.Factory) snapshot} * will be performed. Resetting the factory will restore said snapshot. *

* While this methods makes best effort to be thread safe, it is usually advised to * perform such wide-impact setup serially and BEFORE all test code runs * (setup/beforeAll/beforeClass...). * * @param scheduler the {@link VirtualTimeScheduler} to use in factories. * @return the enabled VirtualTimeScheduler (same as provided), for chaining */ public static VirtualTimeScheduler set(VirtualTimeScheduler scheduler) { return enable(() -> scheduler, true); } /** * Common method to enable a {@link VirtualTimeScheduler} in {@link Schedulers} * factories. The supplier is lazily called. Enabling the same scheduler twice is * also idempotent. * * @param schedulerSupplier the supplier executed to obtain a fresh {@link VirtualTimeScheduler} * @return the scheduler that is actually used after the operation. */ static VirtualTimeScheduler enable(Supplier schedulerSupplier) { return enable(schedulerSupplier, false); } /** * Common method to enable a {@link VirtualTimeScheduler} in {@link Schedulers} * factories. The supplier is lazily called. Enabling the same scheduler twice is * also idempotent. * * @param schedulerSupplier the supplier executed to obtain a fresh {@link VirtualTimeScheduler} * @param exact whether or not to force the use of the supplier, even if there's a matching scheduler * @return the scheduler that is actually used after the operation. */ static VirtualTimeScheduler enable(Supplier schedulerSupplier, boolean exact) { for (; ; ) { VirtualTimeScheduler s = CURRENT.get(); if (s != null && !exact) { return s; } VirtualTimeScheduler newS = schedulerSupplier.get(); if (newS == CURRENT.get()) { return newS; //nothing to do, it has already been set and started in the past } if (CURRENT.compareAndSet(s, newS)) { if (s != null) { newS.schedulersSnapshot = s.schedulersSnapshot; Schedulers.setFactory(new AllFactory(newS)); } else { newS.schedulersSnapshot = Schedulers.setFactoryWithSnapshot(new AllFactory(newS)); } if (CURRENT.get() == newS) { return newS; } } } } /** * The current {@link VirtualTimeScheduler} assigned in {@link Schedulers} * @return current {@link VirtualTimeScheduler} assigned in {@link Schedulers} * @throws IllegalStateException if no {@link VirtualTimeScheduler} has been found */ public static VirtualTimeScheduler get(){ VirtualTimeScheduler s = CURRENT.get(); if (s == null) { throw new IllegalStateException("Check if VirtualTimeScheduler#enable has been invoked first"); } return s; } /** * Return true if there is a {@link VirtualTimeScheduler} currently used by the * {@link Schedulers} factory (ie it has been {@link #set(VirtualTimeScheduler) enabled}), * false otherwise (ie it has been {@link #reset() reset}). */ public static boolean isFactoryEnabled() { return CURRENT.get() != null; } /** * Re-activate the global {@link Schedulers} and potentially customized * {@link reactor.core.scheduler.Schedulers.Factory} that were * active prior to last activation of {@link VirtualTimeScheduler} factories. (ie the * last {@link #set(VirtualTimeScheduler) set} or {@link #getOrSet() getOrSet}). *

* While this methods makes best effort to be thread safe, it is usually advised to * perform such wide-impact setup serially and AFTER all tested code has been run * (teardown/afterAll/afterClass...). */ public static void reset() { VirtualTimeScheduler s = CURRENT.get(); if (s != null && CURRENT.compareAndSet(s, null)) { //note that resetFrom handles null, but it shouldn't happen unless very specific race // with #set and parallel disposal of the set VTS, which doesn't make much sense Schedulers.resetFrom(s.schedulersSnapshot); } } final Queue queue = new PriorityBlockingQueue<>(Queues.XS_BUFFER_SIZE); @SuppressWarnings("unused") volatile long counter; volatile long nanoTime; volatile long deferredNanoTime; static final AtomicLongFieldUpdater DEFERRED_NANO_TIME = AtomicLongFieldUpdater.newUpdater(VirtualTimeScheduler.class, "deferredNanoTime"); volatile int advanceTimeWip; static final AtomicIntegerFieldUpdater ADVANCE_TIME_WIP = AtomicIntegerFieldUpdater.newUpdater(VirtualTimeScheduler.class, "advanceTimeWip"); volatile boolean shutdown; final boolean defer; final VirtualTimeWorker directWorker; private Schedulers.Snapshot schedulersSnapshot; protected VirtualTimeScheduler(boolean defer) { this.defer = defer; directWorker = createWorker(); } /** * Triggers any tasks that have not yet been executed and that are scheduled to be * executed at or before this {@link VirtualTimeScheduler}'s present time. */ public void advanceTime() { advanceTimeBy(Duration.ZERO); } /** * Moves the {@link VirtualTimeScheduler}'s clock forward by a specified amount of time. * * @param delayTime the amount of time to move the {@link VirtualTimeScheduler}'s clock forward */ public void advanceTimeBy(Duration delayTime) { advanceTime(delayTime.toNanos()); } /** * Moves the {@link VirtualTimeScheduler}'s clock to a particular moment in time. * * @param instant the point in time to move the {@link VirtualTimeScheduler}'s * clock to */ public void advanceTimeTo(Instant instant) { long targetTime = TimeUnit.NANOSECONDS.convert(instant.toEpochMilli(), TimeUnit.MILLISECONDS); advanceTime(targetTime - nanoTime); } /** * Get the number of scheduled tasks. *

* This count includes tasks that have already performed as well as ones scheduled in future. * For periodical task, initial task is first scheduled and counted as one. Whenever * subsequent repeat happens this count gets incremented for the one that is scheduled * for the next run. * * @return number of tasks that have scheduled on this scheduler. */ public long getScheduledTaskCount() { return this.counter; } @Override public VirtualTimeWorker createWorker() { if (shutdown) { throw new IllegalStateException("VirtualTimeScheduler is shutdown"); } return new VirtualTimeWorker(); } @Override public long now(TimeUnit unit) { return unit.convert(nanoTime + deferredNanoTime, TimeUnit.NANOSECONDS); } @Override public Disposable schedule(Runnable task) { if (shutdown) { throw Exceptions.failWithRejected(); } return directWorker.schedule(task); } @Override public Disposable schedule(Runnable task, long delay, TimeUnit unit) { if (shutdown) { throw Exceptions.failWithRejected(); } return directWorker.schedule(task, delay, unit); } @Override public boolean isDisposed() { return shutdown; } @Override public void dispose() { if (shutdown) { return; } queue.clear(); shutdown = true; directWorker.dispose(); //TODO remove the below behavior? VirtualTimeScheduler s = CURRENT.get(); if (s == this && CURRENT.compareAndSet(s, null)) { Schedulers.resetFrom(this.schedulersSnapshot); } } @Override public Disposable schedulePeriodically(Runnable task, long initialDelay, long period, TimeUnit unit) { if (shutdown) { throw Exceptions.failWithRejected(); } PeriodicDirectTask periodicTask = new PeriodicDirectTask(task); directWorker.schedulePeriodically(periodicTask, initialDelay, period, unit); return periodicTask; } final void advanceTime(long timeShiftInNanoseconds) { Operators.addCap(DEFERRED_NANO_TIME, this, timeShiftInNanoseconds); drain(); } final void drain() { int remainingWork = ADVANCE_TIME_WIP.incrementAndGet(this); if (remainingWork != 1) { return; } for(;;) { if (!defer || !queue.isEmpty()) { //resetting for the first time a delayed schedule is called after a deferredNanoTime is set long targetNanoTime = nanoTime + DEFERRED_NANO_TIME.getAndSet(this, 0); while (!queue.isEmpty()) { TimedRunnable current = queue.peek(); if (current == null || current.time > targetNanoTime) { break; } //for the benefit of tasks that call `now()` // if scheduled time is 0 (immediate) use current virtual time nanoTime = current.time == 0 ? nanoTime : current.time; queue.poll(); // Only execute if not unsubscribed if (!current.worker.shutdown) { try { current.run.run(); } finally { current.set(true); } } } nanoTime = targetNanoTime; } remainingWork = ADVANCE_TIME_WIP.addAndGet(this, -remainingWork); if (remainingWork == 0) { break; } } } static final class TimedRunnable extends AtomicBoolean implements Comparable, Disposable { final VirtualTimeScheduler scheduler; final VirtualTimeWorker worker; final long time; final Runnable run; final long count; // for differentiating tasks at same time TimedRunnable(VirtualTimeScheduler scheduler, VirtualTimeWorker worker, long time, Runnable run, long count) { this.scheduler = scheduler; this.worker = worker; this.time = time; this.run = run; this.count = count; } @Override public int compareTo(TimedRunnable o) { if (time == o.time) { return Long.compare(count, o.count); } return Long.compare(time, o.time); } @Override public boolean isDisposed() { return super.get(); } @Override public void dispose() { scheduler.queue.remove(this); scheduler.drain(); set(true); } } static final class AllFactory implements Schedulers.Factory { final VirtualTimeScheduler s; AllFactory(VirtualTimeScheduler s) { this.s = s; } @Override public Scheduler newBoundedElastic(int threadCap, int taskCap, ThreadFactory threadFactory, int ttlSeconds) { return s; } @Override public Scheduler newThreadPerTaskBoundedElastic(int threadCap, int queuedTaskCap, ThreadFactory threadFactory) { return s; } @Override public Scheduler newParallel(int parallelism, ThreadFactory threadFactory) { return s; } @Override public Scheduler newSingle(ThreadFactory threadFactory) { return s; } } final class VirtualTimeWorker implements Worker { volatile boolean shutdown; VirtualTimeWorker() { } @Override public Disposable schedule(Runnable run) { return doScheduleAtTime(run,0); } @Override public Disposable schedule(Runnable run, long delayTime, TimeUnit unit) { return doScheduleAtTime(run,nanoTime + unit.toNanos(delayTime)); } private Disposable doScheduleAtTime(Runnable run, long time) { if (shutdown) { throw Exceptions.failWithRejected(); } TimedRunnable timedTask = new TimedRunnable(VirtualTimeScheduler.this, this, time, run, COUNTER.getAndIncrement(VirtualTimeScheduler.this)); queue.add(timedTask); drain(); return timedTask; } @Override public Disposable schedulePeriodically(Runnable task, long initialDelay, long period, TimeUnit unit) { final long periodInNanoseconds = unit.toNanos(period); final long firstNowNanoseconds = nanoTime; final long firstStartInNanoseconds = firstNowNanoseconds + unit.toNanos(initialDelay); PeriodicTask periodicTask = new PeriodicTask(firstStartInNanoseconds, task, firstNowNanoseconds, periodInNanoseconds); replace(periodicTask, schedule(periodicTask, initialDelay, unit)); return periodicTask; } @Override public void dispose() { shutdown = true; } @Override public boolean isDisposed() { return shutdown; } final class PeriodicTask extends AtomicReference implements Runnable, Disposable { final Runnable decoratedRun; final long periodInNanoseconds; long count; long lastNowNanoseconds; long startInNanoseconds; PeriodicTask(long firstStartInNanoseconds, Runnable decoratedRun, long firstNowNanoseconds, long periodInNanoseconds) { this.decoratedRun = decoratedRun; this.periodInNanoseconds = periodInNanoseconds; lastNowNanoseconds = firstNowNanoseconds; startInNanoseconds = firstStartInNanoseconds; lazySet(EMPTY); } @Override public void run() { decoratedRun.run(); if (get() != CANCELLED && !shutdown) { long nextTick; long nowNanoseconds = nanoTime; // If the clock moved in a direction quite a bit, rebase the repetition period if (nowNanoseconds + CLOCK_DRIFT_TOLERANCE_NANOSECONDS < lastNowNanoseconds || nowNanoseconds >= lastNowNanoseconds + periodInNanoseconds + CLOCK_DRIFT_TOLERANCE_NANOSECONDS) { nextTick = nowNanoseconds + periodInNanoseconds; /* * Shift the start point back by the drift as if the whole thing * started count periods ago. */ startInNanoseconds = nextTick - (periodInNanoseconds * (++count)); } else { nextTick = startInNanoseconds + (++count * periodInNanoseconds); } lastNowNanoseconds = nowNanoseconds; long delay = nextTick - nowNanoseconds; replace(this, schedule(this, delay, TimeUnit.NANOSECONDS)); } } @Override public void dispose() { getAndSet(CANCELLED).dispose(); } } } static final Disposable CANCELLED = Disposables.disposed(); static final Disposable EMPTY = Disposables.never(); static boolean replace(AtomicReference ref, @Nullable Disposable c) { for (; ; ) { Disposable current = ref.get(); if (current == CANCELLED) { if (c != null) { c.dispose(); } return false; } if (ref.compareAndSet(current, c)) { return true; } } } static class PeriodicDirectTask implements Runnable, Disposable { final Runnable run; volatile boolean disposed; PeriodicDirectTask(Runnable run) { this.run = run; } @Override public void run() { if (!disposed) { try { run.run(); } catch (Throwable ex) { Exceptions.throwIfFatal(ex); throw Exceptions.propagate(ex); } } } @Override public void dispose() { disposed = true; } } static final AtomicReference CURRENT = new AtomicReference<>(); static final AtomicLongFieldUpdater COUNTER = AtomicLongFieldUpdater.newUpdater(VirtualTimeScheduler.class, "counter"); static final long CLOCK_DRIFT_TOLERANCE_NANOSECONDS; static { CLOCK_DRIFT_TOLERANCE_NANOSECONDS = TimeUnit.MINUTES.toNanos(Long.getLong( "reactor.scheduler.drift-tolerance", 15)); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy