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

reactor.test.util.RaceTestUtils Maven / Gradle / Ivy

There is a newer version: 3.7.0
Show newest version
/*
 * Copyright (c) 2016-2024 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.util;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Predicate;

import reactor.core.Exceptions;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;

/**
 * @author Stephane Maldini
 * @author David Karnok
 */
public class RaceTestUtils {

	/**
	 * Synchronizes the execution of two concurrent state modifications as much as
	 * possible to test race conditions. The method blocks until the given {@link Predicate}
	 * matches. It performs a {@link BiPredicate} test at the end to validate the end
	 * result.
	 *
	 * @param initial the initial state
	 * @param race the state-modification {@link Function}
	 * @param stopRace the stop condition for the race loop, as a {@link Predicate}
	 * @param terminate the validation check, as a {@link BiPredicate}
	 * @return the result of the {@code terminate} check
	 */
	public static  boolean race(T initial, Function race,
			Predicate stopRace,
			BiPredicate terminate) {

		Scheduler w1 = Schedulers.newSingle("w1");
		Scheduler w2 = Schedulers.newSingle("w2");

		try {

			AtomicReference ref1 = new AtomicReference<>();
			CountDownLatch cdl1 = new CountDownLatch(1);
			AtomicReference ref2 = new AtomicReference<>();
			CountDownLatch cdl2 = new CountDownLatch(1);

			w1.schedule(() -> {
				T state = initial;
				while (!stopRace.test(state)) {
					state = race.apply(state);
					LockSupport.parkNanos(1L);
				}
				ref1.set(state);
				cdl1.countDown();
			});
			w2.schedule(() -> {
				T state = initial;
				while (!stopRace.test(state)) {
					state = race.apply(state);
					LockSupport.parkNanos(1L);
				}
				ref2.set(state);
				cdl2.countDown();
			});

			try {
				cdl1.await();
				cdl2.await();
			}
			catch (InterruptedException e) {
				Thread.currentThread()
				      .interrupt();
			}

			return terminate.test(ref1.get(), ref2.get());
		}
		finally {
			w1.dispose();
			w2.dispose();
		}
	}

	/**
	 * Synchronizes the execution of several {@link Runnable}s as much as possible
	 * to test race conditions. The method blocks until all have run to completion.
	 * @param rs the runnables to execute
	 */
	public static void race(final Runnable... rs) {
		race(Schedulers.boundedElastic(), rs);
	}

	/**
	 * Synchronizes the execution of two {@link Runnable}s as much as possible
	 * to test race conditions. The method blocks until both have run to completion.
	 * Kept for binary compatibility, see the varargs variant.
	 * @param r1 the first runnable to execute
	 * @param r2 the second runnable to execute
	 * @see #race(Runnable...)
	 */
	public static void race(final Runnable r1, final Runnable r2) {
		race(new Runnable[]{r1, r2});
	}

	/**
	 * Synchronizes the execution of two {@link Runnable} as much as possible
	 * to test race conditions. The method blocks until both have run to completion.
	 * @param s the {@link Scheduler} on which to execute the runnables
	 * @param r1 the first runnable
	 * @param r2 the second runnable
	 * @deprecated Use {@link #race(Scheduler, Runnable...)}. To be removed in 3.6.0, at the earliest.
	 */
	@Deprecated
	public static void race(final Runnable r1, final Runnable r2, Scheduler s) {
		race(s, r1, r2);
	}

	/**
	 * Synchronizes the execution of several {@link Runnable}s as much as possible
	 * to test race conditions. The method blocks until all have run to completion,
	 * with a 5s timeout.
	 * @param s the {@link Scheduler} on which to execute the runnables
	 * @param rs the runnables to execute
	 */
	public static void race(Scheduler s, final Runnable... rs) {
		race(5, s, rs);
	}

	/**
	 * Synchronizes the execution of several {@link Runnable}s as much as possible
	 * to test race conditions. The method blocks until all have run to completion,
	 * with a configurable timeout (allowing for debugging sessions to use a larger timeout).
	 * @param timeoutSeconds the number of seconds after which the race is considered timed out. intended for debugging purposes.
	 * @param s the {@link Scheduler} on which to execute the runnables
	 * @param rs the runnables to execute
	 */
	public static void race(int timeoutSeconds, Scheduler s, final Runnable... rs) {
		final AtomicInteger count = new AtomicInteger(rs.length);
		final CountDownLatch cdl = new CountDownLatch(rs.length);
		final Throwable[] errors = new Throwable[rs.length];
		for (int i = 0; i < rs.length; i++) {
			final int index = i;
			s.schedule(() -> {
				if (count.decrementAndGet() != 0) {
					while (count.get() != 0) {
						// If there are less CPUs than Runnable instances, this spins
						// forever. In case of platform Threads, things should be fine
						// as the OS can interrupt their work and make progress. With
						// VirtualThreads, they are run on the common ForkJoinPool,
						// which allocates as many Threads as there are CPUs available.
						// In such a case, the Runnables pin the platform Threads
						// disallowing a continuation to be replaced by a pending one.
						// This call creates the opportunity to swap the Runnables and
						// make progress. For platform Threads it is just a hint to the
						// runtime and might be ignored.
						Thread.yield();
					}
				}

				try {
					try {
						rs[index].run();
					} catch (Throwable ex) {
						errors[index] = ex;
					}
				} finally {
					cdl.countDown();
				}
			});
		}

		try {
			if (!cdl.await(timeoutSeconds, TimeUnit.SECONDS)) {
				throw new AssertionError("RaceTestUtils.race wait timed out after " + timeoutSeconds + "s");
			}
		} catch (InterruptedException ex) {
			throw new RuntimeException(ex);
		}

		List es = new ArrayList<>(rs.length);
		for (Throwable t : errors) {
			if (t != null) {
				es.add(t);
			}
		}
		if (es.size() == 1) {
			throw Exceptions.propagate(es.get(0));
		} else if (es.size() > 1) {
			throw Exceptions.multiple(es.toArray(new Throwable[0]));
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy