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.0.7.RELEASE
Show newest version
/*
 * Copyright (c) 2011-2016 Pivotal Software Inc, 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
 *
 *       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 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.AtomicLongFieldUpdater;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

import reactor.core.Disposable;
import reactor.core.Exceptions;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import reactor.core.scheduler.TimedScheduler;
import reactor.util.concurrent.QueueSupplier;

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

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

	/**
	 * Create a new {@link VirtualTimeScheduler} without enabling it. Call
	 * {@link #enable(VirtualTimeScheduler)} to enable it on all {@link Schedulers.Factory}
	 * factories.
	 *
	 * @return a new {@link VirtualTimeScheduler} intended for all {@link Schedulers} factories.
	 */
	public static VirtualTimeScheduler createForAll() {
		return new VirtualTimeScheduler(true);
	}

	/**
	 * Assign a single newly created {@link VirtualTimeScheduler} to all or timed-only
	 * {@link Schedulers.Factory} factories. While the method is thread safe, its usually
	 * advised to execute such wide-impact BEFORE all tested code runs (setup etc).
	 * The created scheduler is returned.
	 *
	 * @param allSchedulers true if all {@link Schedulers.Factory} factories
	 * @return the VirtualTimeScheduler that was created and set through the factory
	 */
	public static VirtualTimeScheduler enable(boolean allSchedulers) {
		return enable(() -> new VirtualTimeScheduler(allSchedulers), allSchedulers);
	}


	/**
	 * Assign an externally created {@link VirtualTimeScheduler} to the relevant
	 * {@link Schedulers.Factory} factories, depending on how it was created (see
	 * {@link #createForAll()} 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.
	 * 

* While the method is thread safe, it's usually advised to execute such wide-impact * BEFORE all tested code runs (setup etc). 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 enable(VirtualTimeScheduler scheduler) { return enable(() -> scheduler, scheduler.isEnabledOnAllSchedulers()); } /** * Common method to enable a {@link VirtualTimeScheduler} in {@link Schedulers} * factories. The supplier is lazily called if there's no current scheduler that * matches the {@code allSchedulers} parameter. Enabling the same scheduler twice is * also idempotent. * * @param schedulerSupplier the supplier executed to obtain a fresh {@link VirtualTimeScheduler} * @param allSchedulers whether or not the scheduler should be activated for all factories * @return the scheduler that is actually used after the operation. */ static VirtualTimeScheduler enable(Supplier schedulerSupplier, boolean allSchedulers) { for (; ; ) { VirtualTimeScheduler s = CURRENT.get(); if (s != null && s.allScheduler == allSchedulers) { return s; } VirtualTimeScheduler newS = schedulerSupplier.get(); if (newS == CURRENT.get()) { return newS; //nothing to do, it has already been set in the past } if (CURRENT.compareAndSet(s, newS)) { if (!allSchedulers) { Schedulers.setFactory(new TimedOnlyFactory(newS)); } else { Schedulers.setFactory(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" + ": " + s); } return s; } /** * Re-assign the default Reactor Core {@link Schedulers} factories. * While the method is thread safe, its usually advised to execute such wide-impact * AFTER all tested code has been run (teardown etc). */ public static void reset() { VirtualTimeScheduler s = CURRENT.get(); if (s != null && CURRENT.compareAndSet(s, null)) { Schedulers.resetFactory(); } } final boolean allScheduler; final Queue queue = new PriorityBlockingQueue<>(QueueSupplier.XS_BUFFER_SIZE); @SuppressWarnings("unused") volatile long counter; volatile long nanoTime; volatile boolean shutdown; protected VirtualTimeScheduler(boolean allScheduler) { this.allScheduler = allScheduler; } /** * 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(nanoTime + 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); } @Override public VirtualTimeWorker createWorker() { if (shutdown) { throw new IllegalStateException("VirtualTimeScheduler is shutdown"); } return new VirtualTimeWorker(); } /** * @return true if virtual-time is also enabled on non-timed global {@link * reactor.core.scheduler.Schedulers.Factory} */ public boolean isEnabledOnAllSchedulers() { return allScheduler; } @Override public long now(TimeUnit unit) { return unit.convert(nanoTime, TimeUnit.NANOSECONDS); } @Override public Disposable schedule(Runnable task) { if (shutdown) { return REJECTED; } return createWorker().schedule(task); } @Override public Disposable schedule(Runnable task, long delay, TimeUnit unit) { if (shutdown) { return REJECTED; } return createWorker().schedule(task, delay, unit); } @Override public boolean isDisposed() { return shutdown; } @Override public void dispose() { if (shutdown) { return; } queue.clear(); shutdown = true; VirtualTimeScheduler s = CURRENT.get(); if (s != null && s == this && CURRENT.compareAndSet(s, null)) { Schedulers.resetFactory(); } } @Override public void shutdown() { dispose(); } @Override public Disposable schedulePeriodically(Runnable task, long initialDelay, long period, TimeUnit unit) { if (shutdown) { return REJECTED; } final TimedWorker w = createWorker(); PeriodicDirectTask periodicTask = new PeriodicDirectTask(task, w); w.schedulePeriodically(periodicTask, initialDelay, period, unit); return periodicTask; } final void advanceTime(long targetTimeInNanoseconds) { while (!queue.isEmpty()) { TimedRunnable current = queue.peek(); if (current.time > targetTimeInNanoseconds) { break; } // if scheduled time is 0 (immediate) use current virtual time nanoTime = current.time == 0 ? nanoTime : current.time; queue.remove(); // Only execute if not unsubscribed if (!current.scheduler.shutdown) { current.run.run(); } } nanoTime = targetTimeInNanoseconds; } static final class TimedRunnable implements Comparable { final long time; final Runnable run; final VirtualTimeWorker scheduler; final long count; // for differentiating tasks at same time TimedRunnable(VirtualTimeWorker scheduler, long time, Runnable run, long count) { this.time = time; this.run = run; this.scheduler = scheduler; this.count = count; } @Override public int compareTo(TimedRunnable o) { if (time == o.time) { return compare(count, o.count); } return compare(time, o.time); } static int compare(long a, long b){ return a < b ? -1 : (a > b ? 1 : 0); } } static final class TimedOnlyFactory implements Schedulers.Factory { final VirtualTimeScheduler s; public TimedOnlyFactory(VirtualTimeScheduler s) { this.s = s; } @Override public TimedScheduler newTimer(ThreadFactory threadFactory) { return s; } } static final class AllFactory implements Schedulers.Factory { final VirtualTimeScheduler s; public AllFactory(VirtualTimeScheduler s) { this.s = s; } @Override public Scheduler newElastic(int ttlSeconds, ThreadFactory threadFactory) { return s; } @Override public Scheduler newParallel(int parallelism, ThreadFactory threadFactory) { return s; } @Override public Scheduler newSingle(ThreadFactory threadFactory) { return s; } @Override public TimedScheduler newTimer(ThreadFactory threadFactory) { return s; } } final class VirtualTimeWorker implements TimedWorker { volatile boolean shutdown; @Override public long now(TimeUnit unit) { return VirtualTimeScheduler.this.now(unit); } @Override public Disposable schedule(Runnable run) { if (shutdown) { return REJECTED; } final TimedRunnable timedTask = new TimedRunnable(this, 0, run, COUNTER.getAndIncrement(VirtualTimeScheduler.this)); queue.add(timedTask); return () -> queue.remove(timedTask); } @Override public Disposable schedule(Runnable run, long delayTime, TimeUnit unit) { if (shutdown) { return REJECTED; } final TimedRunnable timedTask = new TimedRunnable(this, nanoTime + unit.toNanos(delayTime), run, COUNTER.getAndIncrement(VirtualTimeScheduler.this)); queue.add(timedTask); return () -> queue.remove(timedTask); } @Override public Disposable schedulePeriodically(Runnable task, long initialDelay, long period, TimeUnit unit) { final long periodInNanoseconds = unit.toNanos(period); final long firstNowNanoseconds = now(TimeUnit.NANOSECONDS); 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 shutdown() { dispose(); } @Override public void dispose() { shutdown = true; } @Override public boolean isDisposed() { return shutdown; } } static final Disposable CANCELLED = () -> { }; static final Disposable EMPTY = () -> { }; 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) { long nextTick; long nowNanoseconds = now(TimeUnit.NANOSECONDS); // 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 boolean replace(AtomicReference ref, 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; final Scheduler.Worker worker; volatile boolean disposed; PeriodicDirectTask(Runnable run, Worker worker) { this.run = run; this.worker = worker; } @Override public void run() { if (!disposed) { try { run.run(); } catch (Throwable ex) { Exceptions.throwIfFatal(ex); worker.shutdown(); throw Exceptions.propagate(ex); } } } @Override public void dispose() { disposed = true; worker.shutdown(); } } 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 - 2025 Weber Informatics LLC | Privacy Policy