package org.radargun.stages.test; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicIntegerArray; import java.util.concurrent.atomic.AtomicLongArray; import org.radargun.utils.TimeService; /** * Based on provided frequency, returns matching invocation from {@link #next()} or blocks the thread calling it. * * @author Radim Vansa <rvansa@redhat.com> */ public class SchedulingSelector<T> { private final int[] invocations; private final long[] intervals; private final T[] operations; private final AtomicLongArray lastIntervals; private final AtomicIntegerArray todoInvocations; private volatile int offset; /** * @param operations Returned invocations * @param invocations Number of operations per interval * @param intervals Size of slot, in milliseconds */ public SchedulingSelector(T[] operations, int[] invocations, long[] intervals) { if (operations.length != invocations.length) throw new IllegalArgumentException(); this.operations = operations; this.invocations = invocations; this.intervals = intervals; lastIntervals = new AtomicLongArray(operations.length); todoInvocations = new AtomicIntegerArray(operations.length); for (int i = 0; i < operations.length; ++i) { lastIntervals.set(i, Long.MIN_VALUE); } } /** * @return * @throws InterruptedException */ public T next() throws InterruptedException { WAIT_LOOP: for (; ; ) { long now = TimeService.currentTimeMillis(); int myOffset = offset; INVOCATIONS_LOOP: for (int i = 0; i < operations.length; ++i) { int operationIndex = (i + myOffset) % operations.length; long myInterval = intervals[operationIndex]; long currentInterval = now / myInterval; long lastInterval; boolean hasSetLastInterval = false; do { lastInterval = lastIntervals.get(operationIndex); } while (currentInterval > lastInterval && !(hasSetLastInterval = lastIntervals.compareAndSet(operationIndex, lastInterval, currentInterval))); if (hasSetLastInterval) { int frequency = invocations[operationIndex]; // we ignore the requests that should have been executed in previous slots; // if the slot is too small // -1 for immediatelly executed request todoInvocations.set(operationIndex, frequency - 1); synchronized (this) { this.notifyAll(); } } else { int todos; do { todos = todoInvocations.get(operationIndex); if (todos <= 0) continue INVOCATIONS_LOOP; } while (!todoInvocations.compareAndSet(operationIndex, todos, todos - 1)); } offset++; return operations[operationIndex]; } synchronized (this) { // there is a race that thread A sets last timestamp but B checks // the other branch before A updates todos. Then B goes to sleep after // not finding any todos, but after A calling the notify. Therefore, // we check once more here, while synchronized for (int i = 0; i < todoInvocations.length(); ++i) { if (todoInvocations.get(i) != 0) { continue WAIT_LOOP; } } this.wait(1); } } } public static class Builder<T> { private final Class<T> clazz; private List<T> operations = new ArrayList<>(); private List<Integer> invocations = new ArrayList<>(); private List<Long> intervals = new ArrayList<>(); public Builder(Class<T> clazz) { this.clazz = clazz; } public Builder<T> add(T operation, int invocations, long interval) { if (operation == null) { throw new IllegalArgumentException(); } if (invocations <= 0) { return this; } if (interval <= 0) { throw new IllegalArgumentException(operation + ": interval " + String.valueOf(interval)); } operations.add(operation); this.invocations.add(invocations); intervals.add(interval); return this; } public SchedulingSelector<T> build() { if (operations.isEmpty()) throw new IllegalStateException("No operations set!"); return new SchedulingSelector(operations.toArray((T[]) Array.newInstance(clazz, operations.size())), invocations.stream().mapToInt(i -> i.intValue()).toArray(), intervals.stream().mapToLong(l -> l.longValue()).toArray()); } } }