diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/metrics/extensions/FractionalTimer.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/metrics/extensions/FractionalTimer.java index ab7b5fba49ee..65814ddd25b2 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/metrics/extensions/FractionalTimer.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/metrics/extensions/FractionalTimer.java @@ -16,21 +16,9 @@ package com.swirlds.common.metrics.extensions; -import com.swirlds.base.time.Time; -import com.swirlds.common.metrics.FloatFormats; import com.swirlds.common.metrics.FunctionGauge; import com.swirlds.common.metrics.Metrics; -import com.swirlds.common.time.IntegerEpochTime; -import com.swirlds.common.utility.ByteUtils; -import com.swirlds.common.utility.StackTrace; -import com.swirlds.common.utility.throttle.RateLimitedLogger; -import com.swirlds.logging.legacy.LogMarker; import edu.umd.cs.findbugs.annotations.NonNull; -import java.time.Duration; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.LongBinaryOperator; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; /** * A utility that measures the fraction of time that is spent in one of two phases. For example, can be used to track @@ -40,62 +28,7 @@ * This object must be measured at least once every 34 minutes or else it will overflow and return -1. *

*/ -public class FractionalTimer { - private static final Logger logger = LogManager.getLogger(FractionalTimer.class); - - /** - * the initial value of status when the instance is created - */ - private static final int INITIAL_STATUS = -1; - - /** - * the value of start time when the metric has overflowed - */ - private static final int OVERFLOW = -1; - - /** - * if an error occurs, do not write a log statement more often than this - */ - private static final Duration LOG_PERIOD = Duration.ofMinutes(5); - - /** - * An instance that provides the current time - */ - private final IntegerEpochTime time; - - /** - * Used to atomically update and reset the time and status - */ - private final AtomicLong accumulator; - - /** - * limits the frequency of error log statements - */ - private final RateLimitedLogger errorLogger; - - /** - * This lambda is used to enter an active state. - */ - private final LongBinaryOperator activationUpdate; - - /** - * This lambda is used to enter an inactive state. - */ - private final LongBinaryOperator deactivationUpdate; - - /** - * A constructor where a custom {@link Time} instance could be supplied - * - * @param time provides the current time - */ - public FractionalTimer(@NonNull final Time time) { - this.time = new IntegerEpochTime(time); - this.accumulator = new AtomicLong(ByteUtils.combineInts(this.time.getMicroTime(), INITIAL_STATUS)); - this.errorLogger = new RateLimitedLogger(logger, time, LOG_PERIOD); - - activationUpdate = (currentState, now) -> statusUpdate(currentState, true, now); - deactivationUpdate = (currentState, now) -> statusUpdate(currentState, false, now); - } +public interface FractionalTimer { /** * Registers a {@link FunctionGauge} that tracks the fraction of time that this object has been active (out of @@ -106,57 +39,41 @@ public FractionalTimer(@NonNull final Time time) { * @param name a short name for the {@code Metric} * @param description a one-sentence description of the {@code Metric} */ - public void registerMetric( + void registerMetric( @NonNull final Metrics metrics, @NonNull final String category, @NonNull final String name, - @NonNull final String description) { - metrics.getOrCreate(new FunctionGauge.Config<>(category, name, Double.class, this::getAndReset) - .withDescription(description) - .withUnit("fraction") - .withFormat(FloatFormats.FORMAT_1_3)); - } + @NonNull final String description); /** * Notifies the metric that we are entering an active period. * * @param now the current time in microseconds */ - public void activate(final long now) { - accumulator.accumulateAndGet(now, activationUpdate); - } + void activate(final long now); /** * Notifies the metric that we are entering an active period. */ - public void activate() { - this.activate(time.getMicroTime()); - } + void activate(); /** * Notifies the metric that we are entering an inactive period. * * @param now the current time in microseconds */ - public void deactivate(final long now) { - accumulator.accumulateAndGet(now, deactivationUpdate); - } + void deactivate(final long now); /** * Notifies the metric that we are entering an inactive period. */ - public void deactivate() { - this.deactivate(time.getMicroTime()); - } + void deactivate(); /** * @return the fraction of time that this object has been active, where 0.0 means not at all active, and 1.0 means * that this object has been 100% active. */ - public double getActiveFraction() { - final long pair = accumulator.get(); - return activeFraction(ByteUtils.extractLeftInt(pair), ByteUtils.extractRightInt(pair)); - } + double getActiveFraction(); /** * Same as {@link #getActiveFraction()} but also resets the metric. @@ -164,125 +81,5 @@ public double getActiveFraction() { * @return the fraction of time that this object has been active, where 0.0 means this object is not at all active, * and 1.0 means that this object has been is 100% active. */ - public double getAndReset() { - final long pair = accumulator.getAndUpdate(this::reset); - return activeFraction(ByteUtils.extractLeftInt(pair), ByteUtils.extractRightInt(pair)); - } - - /** - * Gets the fraction of time this object has been active since the last reset. - * - * @param measurementStart the micro epoch time when the last reset occurred - * @param status the current status of this object and the time spent in the opposite status - * @return the fraction of time that this object has been active, where 0.0 means this object is not at all active, - * and 1.0 means that this object is 100% active, or -1 if the metric has overflowed because it was not reset - */ - private double activeFraction(final int measurementStart, final int status) { - if (measurementStart < 0) { - return OVERFLOW; - } - final int elapsedTime = time.microsElapsed(measurementStart); - if (elapsedTime == 0) { - return 0; - } - final int activeTime; - if (isInactive(status)) { - activeTime = Math.abs(status) - 1; - } else { - activeTime = elapsedTime - (status - 1); - } - return ((double) activeTime) / elapsedTime; - } - - /** - * Check if this timer is currently inactive. - * - * @param status the current status of this object - * @return true if this timer is currently inactive - */ - private boolean isInactive(final int status) { - return status < 0; - } - - /** - * Update the state of this object. The state is the value stored in an atomic long. - * - * @param currentState the current value of the atomic long. Two integers are packed into this long value. The - * first four bytes represent the timestamp when we initially began the current measurement - * period. The last four bytes represent the total time spent in the opposite status. The - * right four bytes can be positive or negative. If positive, it means that the object is - * currently active. If negative, it means that the object is currently inactive. - * @param isBecomingActive true if the object is currently becoming active, false if it is currently becoming - * inactive - * @param now the current time in microseconds - * @return the new value that will be stored in the atomic long. - */ - private long statusUpdate(final long currentState, final boolean isBecomingActive, final long now) { - - // the epoch time when the last reset occurred - final int measurementStart = ByteUtils.extractLeftInt(currentState); - // The current status is represented by the (+ -) sign. The number represents the time spent in the - // opposite status. This is so that its time spent being active/inactive can be deduced whenever the sample is - // taken. If the time spent active is X, and the measurement time is Y, then inactive time is Y-X. Since zero - // is neither positive nor negative, the values are offset by 1 - final int currentStatus = ByteUtils.extractRightInt(currentState); - - // In order to fit the time into 4 bytes, we need to truncate the time to the lower 31 bits. - // This causes the timer to become inaccurate after 34 minutes, but that is not a problem, - // since this utility is intended to be used to measure time over much shorter intervals. - final int truncatedTime = (int) now; - - if ((isBecomingActive && !isInactive(currentStatus)) || (!isBecomingActive && isInactive(currentStatus))) { - // this means that the metric has not been updated correctly, we will not change the value - errorLogger.error( - LogMarker.EXCEPTION.getMarker(), - "FractionalTimer has been updated incorrectly. " - + "Current status: {}, is becoming active: {}, stack trace: \n{}", - currentStatus, - isBecomingActive, - StackTrace.getStackTrace().toString()); - return currentState; - } - // the time elapsed since the last reset - final int elapsedTime = IntegerEpochTime.elapsed(measurementStart, truncatedTime); - // the time spent in the opposite status, either active or inactive - final int statusTime = Math.abs(currentStatus) - 1; - // the time spent inactive is all the elapsed time minus the time spent active - // the time spent active is all the elapsed time minus the time spent inactive - final int newTime = elapsedTime - statusTime; - if (newTime < 0 || measurementStart < 0) { - // this is an overflow because the metric was not reset, we are in a state where we can no longer track - // the time spent inactive or active - return ByteUtils.combineInts(OVERFLOW, isBecomingActive ? 1 : -1); - } - if (isBecomingActive) { - // this means this was previously inactive and now started working - return ByteUtils.combineInts(measurementStart, newTime + 1); - } - // this means the object was previously active and now stopped working - return ByteUtils.combineInts(measurementStart, -newTime - 1); - } - - /** - * This lambda is used to reset all data stored by this object and begin a new measurement period. - * - * @param currentState the current state of this object - * @return the new state of this object - */ - private long reset(final long currentState) { - return ByteUtils.combineInts(time.getMicroTime(), resetStatus(ByteUtils.extractRightInt(currentState))); - } - - /** - * Used to generate the rightmost four bytes in the state during a restart. - * - * @param status the current rightmost four bytes - * @return the new rightmost four bytes - */ - private int resetStatus(final int status) { - if (status > 0) { - return 1; - } - return -1; - } + double getAndReset(); } diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/metrics/extensions/NoOpFractionalTimer.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/metrics/extensions/NoOpFractionalTimer.java new file mode 100644 index 000000000000..0d7739d83f54 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/metrics/extensions/NoOpFractionalTimer.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.metrics.extensions; + +import com.swirlds.common.metrics.Metrics; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A NoOp implementation of {@link FractionalTimer}. + */ +public class NoOpFractionalTimer implements FractionalTimer { + + private static final NoOpFractionalTimer INSTANCE = new NoOpFractionalTimer(); + + private NoOpFractionalTimer() {} + + /** + * Get the singleton instance. + * + * @return the singleton instance + */ + public static FractionalTimer getInstance() { + return INSTANCE; + } + + /** + * {@inheritDoc} + */ + @Override + public void registerMetric( + @NonNull Metrics metrics, @NonNull String category, @NonNull String name, @NonNull String description) {} + + /** + * {@inheritDoc} + */ + @Override + public void activate(final long now) {} + + /** + * {@inheritDoc} + */ + @Override + public void activate() {} + + /** + * {@inheritDoc} + */ + @Override + public void deactivate(final long now) {} + + /** + * {@inheritDoc} + */ + @Override + public void deactivate() {} + + /** + * {@inheritDoc} + */ + @Override + public double getActiveFraction() { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public double getAndReset() { + return 0; + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/metrics/extensions/PhaseTimer.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/metrics/extensions/PhaseTimer.java index 157ec1f89ba6..4980c6c5252d 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/metrics/extensions/PhaseTimer.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/metrics/extensions/PhaseTimer.java @@ -72,7 +72,7 @@ public class PhaseTimer> { if (fractionMetricsEnabled) { for (final T phase : builder.getPhases()) { - fractionalTimers.put(phase, new FractionalTimer(time)); + fractionalTimers.put(phase, new StandardFractionalTimer(time)); } } diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/metrics/extensions/StandardFractionalTimer.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/metrics/extensions/StandardFractionalTimer.java new file mode 100644 index 000000000000..57f043444ee5 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/metrics/extensions/StandardFractionalTimer.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.metrics.extensions; + +import com.swirlds.base.time.Time; +import com.swirlds.common.metrics.FloatFormats; +import com.swirlds.common.metrics.FunctionGauge; +import com.swirlds.common.metrics.Metrics; +import com.swirlds.common.time.IntegerEpochTime; +import com.swirlds.common.utility.ByteUtils; +import com.swirlds.common.utility.StackTrace; +import com.swirlds.common.utility.throttle.RateLimitedLogger; +import com.swirlds.logging.legacy.LogMarker; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.LongBinaryOperator; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * A utility that measures the fraction of time that is spent in one of two phases. For example, can be used to track + * the overall busy time of a thread, or the busy time of a specific subtask. The granularity of this metric is in + * microseconds. + *

+ * This object must be measured at least once every 34 minutes or else it will overflow and return -1. + *

+ */ +public class StandardFractionalTimer implements FractionalTimer { + private static final Logger logger = LogManager.getLogger(StandardFractionalTimer.class); + + /** + * the initial value of status when the instance is created + */ + private static final int INITIAL_STATUS = -1; + + /** + * the value of start time when the metric has overflowed + */ + private static final int OVERFLOW = -1; + + /** + * if an error occurs, do not write a log statement more often than this + */ + private static final Duration LOG_PERIOD = Duration.ofMinutes(5); + + /** + * An instance that provides the current time + */ + private final IntegerEpochTime time; + + /** + * Used to atomically update and reset the time and status + */ + private final AtomicLong accumulator; + + /** + * limits the frequency of error log statements + */ + private final RateLimitedLogger errorLogger; + + /** + * This lambda is used to enter an active state. + */ + private final LongBinaryOperator activationUpdate; + + /** + * This lambda is used to enter an inactive state. + */ + private final LongBinaryOperator deactivationUpdate; + + /** + * A constructor where a custom {@link Time} instance could be supplied + * + * @param time provides the current time + */ + public StandardFractionalTimer(@NonNull final Time time) { + this.time = new IntegerEpochTime(time); + this.accumulator = new AtomicLong(ByteUtils.combineInts(this.time.getMicroTime(), INITIAL_STATUS)); + this.errorLogger = new RateLimitedLogger(logger, time, LOG_PERIOD); + + activationUpdate = (currentState, now) -> statusUpdate(currentState, true, now); + deactivationUpdate = (currentState, now) -> statusUpdate(currentState, false, now); + } + + /** + * {@inheritDoc} + */ + @Override + public void registerMetric( + @NonNull final Metrics metrics, + @NonNull final String category, + @NonNull final String name, + @NonNull final String description) { + metrics.getOrCreate(new FunctionGauge.Config<>(category, name, Double.class, this::getAndReset) + .withDescription(description) + .withUnit("fraction") + .withFormat(FloatFormats.FORMAT_1_3)); + } + + /** + * {@inheritDoc} + */ + @Override + public void activate(final long now) { + accumulator.accumulateAndGet(now, activationUpdate); + } + + /** + * {@inheritDoc} + */ + @Override + public void activate() { + this.activate(time.getMicroTime()); + } + + /** + * {@inheritDoc} + */ + @Override + public void deactivate(final long now) { + accumulator.accumulateAndGet(now, deactivationUpdate); + } + + /** + * {@inheritDoc} + */ + @Override + public void deactivate() { + this.deactivate(time.getMicroTime()); + } + + /** + * {@inheritDoc} + */ + @Override + public double getActiveFraction() { + final long pair = accumulator.get(); + return activeFraction(ByteUtils.extractLeftInt(pair), ByteUtils.extractRightInt(pair)); + } + + /** + * {@inheritDoc} + */ + @Override + public double getAndReset() { + final long pair = accumulator.getAndUpdate(this::reset); + return activeFraction(ByteUtils.extractLeftInt(pair), ByteUtils.extractRightInt(pair)); + } + + /** + * Gets the fraction of time this object has been active since the last reset. + * + * @param measurementStart the micro epoch time when the last reset occurred + * @param status the current status of this object and the time spent in the opposite status + * @return the fraction of time that this object has been active, where 0.0 means this object is not at all active, + * and 1.0 means that this object is 100% active, or -1 if the metric has overflowed because it was not reset + */ + private double activeFraction(final int measurementStart, final int status) { + if (measurementStart < 0) { + return OVERFLOW; + } + final int elapsedTime = time.microsElapsed(measurementStart); + if (elapsedTime == 0) { + return 0; + } + final int activeTime; + if (isInactive(status)) { + activeTime = Math.abs(status) - 1; + } else { + activeTime = elapsedTime - (status - 1); + } + return ((double) activeTime) / elapsedTime; + } + + /** + * Check if this timer is currently inactive. + * + * @param status the current status of this object + * @return true if this timer is currently inactive + */ + private boolean isInactive(final int status) { + return status < 0; + } + + /** + * Update the state of this object. The state is the value stored in an atomic long. + * + * @param currentState the current value of the atomic long. Two integers are packed into this long value. The + * first four bytes represent the timestamp when we initially began the current measurement + * period. The last four bytes represent the total time spent in the opposite status. The + * right four bytes can be positive or negative. If positive, it means that the object is + * currently active. If negative, it means that the object is currently inactive. + * @param isBecomingActive true if the object is currently becoming active, false if it is currently becoming + * inactive + * @param now the current time in microseconds + * @return the new value that will be stored in the atomic long. + */ + private long statusUpdate(final long currentState, final boolean isBecomingActive, final long now) { + + // the epoch time when the last reset occurred + final int measurementStart = ByteUtils.extractLeftInt(currentState); + // The current status is represented by the (+ -) sign. The number represents the time spent in the + // opposite status. This is so that its time spent being active/inactive can be deduced whenever the sample is + // taken. If the time spent active is X, and the measurement time is Y, then inactive time is Y-X. Since zero + // is neither positive nor negative, the values are offset by 1 + final int currentStatus = ByteUtils.extractRightInt(currentState); + + // In order to fit the time into 4 bytes, we need to truncate the time to the lower 31 bits. + // This causes the timer to become inaccurate after 34 minutes, but that is not a problem, + // since this utility is intended to be used to measure time over much shorter intervals. + final int truncatedTime = (int) now; + + if ((isBecomingActive && !isInactive(currentStatus)) || (!isBecomingActive && isInactive(currentStatus))) { + // this means that the metric has not been updated correctly, we will not change the value + errorLogger.error( + LogMarker.EXCEPTION.getMarker(), + "FractionalTimer has been updated incorrectly. " + + "Current status: {}, is becoming active: {}, stack trace: \n{}", + currentStatus, + isBecomingActive, + StackTrace.getStackTrace().toString()); + return currentState; + } + // the time elapsed since the last reset + final int elapsedTime = IntegerEpochTime.elapsed(measurementStart, truncatedTime); + // the time spent in the opposite status, either active or inactive + final int statusTime = Math.abs(currentStatus) - 1; + // the time spent inactive is all the elapsed time minus the time spent active + // the time spent active is all the elapsed time minus the time spent inactive + final int newTime = elapsedTime - statusTime; + if (newTime < 0 || measurementStart < 0) { + // this is an overflow because the metric was not reset, we are in a state where we can no longer track + // the time spent inactive or active + return ByteUtils.combineInts(OVERFLOW, isBecomingActive ? 1 : -1); + } + if (isBecomingActive) { + // this means this was previously inactive and now started working + return ByteUtils.combineInts(measurementStart, newTime + 1); + } + // this means the object was previously active and now stopped working + return ByteUtils.combineInts(measurementStart, -newTime - 1); + } + + /** + * This lambda is used to reset all data stored by this object and begin a new measurement period. + * + * @param currentState the current state of this object + * @return the new state of this object + */ + private long reset(final long currentState) { + return ByteUtils.combineInts(time.getMicroTime(), resetStatus(ByteUtils.extractRightInt(currentState))); + } + + /** + * Used to generate the rightmost four bytes in the state during a restart. + * + * @param status the current rightmost four bytes + * @return the new rightmost four bytes + */ + private int resetStatus(final int status) { + if (status > 0) { + return 1; + } + return -1; + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/threading/framework/internal/QueueThreadMetrics.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/threading/framework/internal/QueueThreadMetrics.java index 3082c8696087..9fd2cfda55ea 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/threading/framework/internal/QueueThreadMetrics.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/threading/framework/internal/QueueThreadMetrics.java @@ -17,6 +17,7 @@ package com.swirlds.common.threading.framework.internal; import com.swirlds.common.metrics.extensions.FractionalTimer; +import com.swirlds.common.metrics.extensions.StandardFractionalTimer; import com.swirlds.common.threading.framework.config.QueueThreadMetricsConfiguration; import edu.umd.cs.findbugs.annotations.NonNull; @@ -39,7 +40,7 @@ public QueueThreadMetrics(@NonNull final AbstractQueueThreadConfiguration this.busyTime = null; return; } - this.busyTime = new FractionalTimer(metricsConfig.getTime()); + this.busyTime = new StandardFractionalTimer(metricsConfig.getTime()); busyTime.registerMetric( metricsConfig.getMetrics(), metricsConfig.getCategory(), diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/InputWire.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/InputWire.java new file mode 100644 index 000000000000..0e26414f4c38 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/InputWire.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * An object that can insert work to be handled by a {@link TaskScheduler}. + * + * @param the type of data that passes into the wire + * @param the type of the data that comes out of the parent {@link TaskScheduler}'s primary output wire + */ +public class InputWire { + + private final TaskScheduler taskScheduler; + private Consumer handler; + private final String name; + + /** + * Constructor. + * + * @param taskScheduler the scheduler to insert data into + * @param name the name of the input wire + */ + InputWire(@NonNull final TaskScheduler taskScheduler, @NonNull final String name) { + this.taskScheduler = Objects.requireNonNull(taskScheduler); + this.name = Objects.requireNonNull(name); + } + + /** + * Get the name of this input wire. + * + * @return the name of this input wire + */ + @NonNull + public String getName() { + return name; + } + + /** + * Get the name of the task scheduler this input channel is bound to. + * + * @return the name of the wire this input channel is bound to + */ + @NonNull + public String getTaskSchedulerName() { + return taskScheduler.getName(); + } + + /** + * Cast this input wire into whatever a variable is expecting. Sometimes the compiler gets confused with generics, + * and path of least resistance is to just cast to the proper data type. + * + *

+ * Warning: this will appease the compiler, but it is possible to cast a wire into a data type that will cause + * runtime exceptions. Use with appropriate caution. + * + * @param the input type to cast to + * @param the output type to cast to + * @return this, cast into whatever type is requested + */ + @NonNull + @SuppressWarnings("unchecked") + public final InputWire cast() { + return (InputWire) this; + } + + /** + * Convenience method for creating an input wire with a specific input type. This method is useful for when the + * compiler can't figure out the generic type of the input wire. This method is a no op. + * + * @param inputType the input type of the input wire + * @param the input type of the input wire + * @return this + */ + @SuppressWarnings("unchecked") + public InputWire withInputType(@NonNull final Class inputType) { + return (InputWire) this; + } + + /** + * Bind this input wire to a handler. A handler must be bound to this input wire prior to inserting data via any + * method. + * + * @param handler the handler to bind to this input wire + * @return this + * @throws IllegalStateException if a handler is already bound and this method is called a second time + */ + @SuppressWarnings("unchecked") + @NonNull + public InputWire bind(@NonNull final Consumer handler) { + if (this.handler != null) { + throw new IllegalStateException("Input wire \"" + name + "\" already bound"); + } + this.handler = (Consumer) Objects.requireNonNull(handler); + + return this; + } + + /** + * Bind this input wire to a handler. A handler must be bound to this inserter prior to inserting data via any + * method. + * + * @param handler the handler to bind to this input task scheduler + * @return this + * @throws IllegalStateException if a handler is already bound and this method is called a second time + */ + @SuppressWarnings("unchecked") + @NonNull + public InputWire bind(@NonNull final Function handler) { + if (this.handler != null) { + throw new IllegalStateException("Handler already bound"); + } + this.handler = i -> { + final OUT output = handler.apply((IN) i); + if (output != null) { + taskScheduler.forward(output); + } + }; + + return this; + } + + /** + * Add a task to the task scheduler. May block if back pressure is enabled. + * + * @param data the data to be processed by the task scheduler + */ + public void put(@Nullable final IN data) { + taskScheduler.put(handler, data); + } + + /** + * Add a task to the task scheduler. If backpressure is enabled and there is not immediately capacity available, + * this method will not accept the data. + * + * @param data the data to be processed by the task scheduler + * @return true if the data was accepted, false otherwise + */ + public boolean offer(@Nullable final IN data) { + return taskScheduler.offer(handler, data); + } + + /** + * Inject data into the task scheduler, doing so even if it causes the number of unprocessed tasks to exceed the + * capacity specified by configured back pressure. If backpressure is disabled, this operation is logically + * equivalent to {@link #put(Object)}. + * + * @param data the data to be processed by the task scheduler + */ + public void inject(@Nullable final IN data) { + taskScheduler.inject(handler, data); + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/ModelGroup.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/ModelGroup.java new file mode 100644 index 000000000000..6c0b31ee4235 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/ModelGroup.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Set; + +/** + * Describes a group of components that should be visualized together in a wiring diagram. Specified via strings since + * this configuration is presumably read from the command line. + * + * @param name the name of the group + * @param elements the set of subcomponents in the group + * @param collapse true if the group should be collapsed into a single box + */ +public record ModelGroup(@NonNull String name, @NonNull Set elements, boolean collapse) + implements Comparable { + + /** + * Sorts groups by name. + */ + @Override + public int compareTo(@NonNull final ModelGroup that) { + return name.compareTo(that.name); + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/OutputWire.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/OutputWire.java new file mode 100644 index 000000000000..3ac19d3263a8 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/OutputWire.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring; + +import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; + +import com.swirlds.common.wiring.transformers.WireFilter; +import com.swirlds.common.wiring.transformers.WireListSplitter; +import com.swirlds.common.wiring.transformers.WireTransformer; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Describes the output of a task scheduler. Can be soldered to wire inputs or lambdas. + * + * @param the output type of the object + */ +public final class OutputWire { + + private static final Logger logger = LogManager.getLogger(OutputWire.class); + + private final WiringModel model; + private final String name; + private final List> forwardingDestinations = new ArrayList<>(); + + /** + * Constructor. + * + * @param model the wiring model containing this output wire + * @param name the name of the output wire + */ + public OutputWire(@NonNull final WiringModel model, @NonNull final String name) { + + this.model = Objects.requireNonNull(model); + this.name = Objects.requireNonNull(name); + } + + /** + * Get the name of this output wire. If this object is a task scheduler, this is the same as the name of the task + * scheduler. + * + * @return the name + */ + @NonNull + public String getName() { + return name; + } + + /** + * Forward output data to any wires/consumers that are listening for it. + *

+ * Although it will technically work, it is a violation of convention to directly put data into this output wire + * except from within code being executed by the task scheduler that owns this output wire. Don't do it. + * + * @param data the output data to forward + */ + public void forward(@NonNull final OUT data) { + for (final Consumer destination : forwardingDestinations) { + try { + destination.accept(data); + } catch (final Exception e) { + logger.error( + EXCEPTION.getMarker(), + "Exception thrown on output wire {} while forwarding data {}", + name, + data, + e); + } + } + } + + /** + * Specify an input wire where output data should be passed. This forwarding operation respects back pressure. + * + *

+ * Soldering is the act of connecting two wires together, usually by melting a metal alloy between them. See + * wikipedia's entry on soldering. + * + *

+ * Forwarding should be fully configured prior to data being inserted into the system. Adding forwarding + * destinations after data has been inserted into the system is not thread safe and has undefined behavior. + * + * @param inputWire the input wire to forward output data to + */ + public final void solderTo(@NonNull final InputWire inputWire) { + solderTo(inputWire, false); + } + + /** + * Specify an input wire where output data should be passed. This forwarding operation respects back pressure. + * + *

+ * Soldering is the act of connecting two wires together, usually by melting a metal alloy between them. See + * wikipedia's entry on soldering. + * + *

+ * Forwarding should be fully configured prior to data being inserted into the system. Adding forwarding + * destinations after data has been inserted into the system is not thread safe and has undefined behavior. + * + * @param inputWire the input wire to forward output data to + * @param inject if true, then the output data will be injected into the input wire, ignoring back pressure + */ + public final void solderTo(@NonNull final InputWire inputWire, final boolean inject) { + model.registerEdge(name, inputWire.getTaskSchedulerName(), inputWire.getName(), inject); + if (inject) { + forwardingDestinations.add(inputWire::inject); + } else { + forwardingDestinations.add(inputWire::put); + } + } + + /** + * Specify a consumer where output data should be forwarded. + * + *

+ * Soldering is the act of connecting two wires together, usually by melting a metal alloy between them. See + * wikipedia's entry on soldering. + * + *

+ * Forwarding should be fully configured prior to data being inserted into the system. Adding forwarding + * destinations after data has been inserted into the system is not thread safe and has undefined behavior. + * + * @param handlerName the name of the consumer + * @param handler the consumer to forward output data to + */ + public void solderTo(@NonNull final String handlerName, @NonNull final Consumer handler) { + model.registerEdge(name, handlerName, "", false); + forwardingDestinations.add(Objects.requireNonNull(handler)); + } + + /** + * Build a {@link WireFilter}. The input wire to the filter is automatically soldered to this output wire (i.e. all + * data that comes out of the wire will be inserted into the filter). The output wire of the filter is returned by + * this method. + * + * @param name the name of the filter + * @param predicate the predicate that filters the output of this wire + * @return the output wire of the filter + */ + @NonNull + public OutputWire buildFilter(@NonNull final String name, @NonNull final Predicate predicate) { + final WireFilter filter = + new WireFilter<>(model, Objects.requireNonNull(name), Objects.requireNonNull(predicate)); + solderTo(name, filter); + return filter.getOutputWire(); + } + + /** + * Build a {@link WireListSplitter}. Creating a splitter for wires without a list output type will cause runtime + * exceptions. The input wire to the splitter is automatically soldered to this output wire (i.e. all data that + * comes out of the wire will be inserted into the splitter). The output wire of the splitter is returned by this + * method. + * + * @param the type of the list elements + * @return output wire of the splitter + */ + @SuppressWarnings("unchecked") + @NonNull + public OutputWire buildSplitter() { + final String splitterName = name + "_splitter"; + final WireListSplitter splitter = new WireListSplitter<>(model, splitterName); + solderTo(splitterName, (Consumer) splitter); + return splitter.getOutputWire(); + } + + /** + * Build a {@link WireListSplitter} that is soldered to the output of this wire. Creating a splitter for wires + * without a list output type will cause runtime exceptions. The input wire to the splitter is automatically + * soldered to this output wire (i.e. all data that comes out of the wire will be inserted into the splitter). The + * output wire of the splitter is returned by this method. + * + * @param clazz the class of the list elements, convince parameter for hinting generic type to the compiler + * @param the type of the list elements + */ + @NonNull + public OutputWire buildSplitter(@NonNull final Class clazz) { + return buildSplitter(); + } + + /** + * Build a {@link WireTransformer}. The input wire to the transformer is automatically soldered to this output wire + * (i.e. all data that comes out of the wire will be inserted into the transformer). The output wire of the + * transformer is returned by this method. + * + * @param name the name of the transformer + * @param transform the function that transforms the output of this wire into the output of the transformer + * @param the output type of the transformer + * @return the output wire of the transformer + */ + @NonNull + public OutputWire buildTransformer(@NonNull final String name, @NonNull final Function transform) { + final WireTransformer transformer = + new WireTransformer<>(model, Objects.requireNonNull(name), Objects.requireNonNull(transform)); + solderTo(name, transformer); + return transformer.getOutputWire(); + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/TaskScheduler.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/TaskScheduler.java new file mode 100644 index 000000000000..851178e1adf2 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/TaskScheduler.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring; + +import com.swirlds.common.wiring.counters.ObjectCounter; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Objects; +import java.util.function.Consumer; + +/** + * Schedules tasks for a component. + *

+ * The lifecycle of a task is as follows: + *

    + *
  1. Unscheduled: the task has not been passed to the scheduler yet (e.g. via {@link InputWire#put(Object)})
  2. + *
  3. Scheduled but not processed: the task has been passed to the scheduler but the corresponding handler has not + * yet returned (either because the handler has not yet been called or because the handler has been called but hasn't finished + * yet)
  4. + *
  5. Processed: the corresponding handle method for the task has been called and has returned.
  6. + *
+ * + * @param the output type of the primary output wire (use {@link Void} if no output is needed) + */ +public abstract class TaskScheduler { + + private final boolean flushEnabled; + private final WiringModel model; + private final String name; + private final OutputWire primaryOutputWire; + + /** + * Constructor. + * + * @param model the wiring model containing this task scheduler + * @param name the name of the task scheduler + * @param flushEnabled if true, then {@link #flush()} will be enabled, otherwise it will throw. + * @param insertionIsBlocking when data is inserted into this task scheduler, will it block until capacity is + * available? + */ + protected TaskScheduler( + @NonNull final WiringModel model, + @NonNull final String name, + final boolean flushEnabled, + final boolean insertionIsBlocking) { + + this.model = Objects.requireNonNull(model); + this.name = Objects.requireNonNull(name); + this.flushEnabled = flushEnabled; + primaryOutputWire = new OutputWire<>(model, name); + model.registerVertex(name, insertionIsBlocking); + } + + /** + * Get the name of this task scheduler. + * + * @return the name of this task scheduler + */ + @NonNull + public String getName() { + return name; + } + + /** + * Cast this scheduler into whatever a variable is expecting. Sometimes the compiler gets confused with generics, + * and path of least resistance is to just cast to the proper data type. + * + *

+ * Warning: this will appease the compiler, but it is possible to cast a scheduler into a data type that will cause + * runtime exceptions. Use with appropriate caution. + * + * @param the type to cast to + * @return this, cast into whatever type is requested + */ + @NonNull + @SuppressWarnings("unchecked") + public final TaskScheduler cast() { + return (TaskScheduler) this; + } + + /** + * Build an input wire for passing data to this task scheduler. In order to use this wire, a handler must be bound + * via {@link InputWire#bind(Consumer)}. + * + * @param name the name of the input wire + * @param the type of data that is inserted via this input wire + * @return the input wire + */ + @NonNull + public final InputWire buildInputWire(@NonNull final String name) { + return new InputWire<>(this, name); + } + + /** + * Get the number of unprocessed tasks. A task is considered to be unprocessed until the data has been passed to the + * handler method (i.e. the one given to {@link InputWire#bind(Consumer)}) and that handler method has returned. + *

+ * Returns -1 if this task scheduler is not monitoring the number of unprocessed tasks. Schedulers do not track the + * number of unprocessed tasks by default. This method will always return -1 unless one of the following is true: + *

    + *
  • {@link TaskSchedulerMetricsBuilder#withUnhandledTaskMetricEnabled(boolean)} is called with the value true
  • + *
  • {@link TaskSchedulerBuilder#withUnhandledTaskCapacity(long)} is passed a positive value
  • + *
  • {@link TaskSchedulerBuilder#withOnRamp(ObjectCounter)} is passed a counter that is not a no op counter
  • + *
+ */ + public abstract long getUnprocessedTaskCount(); + + /** + * Flush all data in the task scheduler. Blocks until all data currently in flight has been processed. + * + *

+ * Note: must be enabled by passing true to {@link TaskSchedulerBuilder#withFlushingEnabled(boolean)}. + * + *

+ * Warning: some implementations of flush may block indefinitely if new work is continuously added to the scheduler + * while flushing. Such implementations are guaranteed to finish flushing once new work is no longer being added. + * Some implementations do not have this restriction, and will return as soon as all of the in flight work has been + * processed, regardless of whether or not new work is being added. + * + * @throws UnsupportedOperationException if {@link TaskSchedulerBuilder#withFlushingEnabled(boolean)} was set to + * false (or was unset, default is false) + */ + public abstract void flush(); + + /** + * Get the default output wire for this task scheduler. Sometimes referred to as the "primary" output wire.All data + * returned by handlers is passed ot this output wire. Calling this method more than once will always return the + * same object. + * + * @return the primary output wire + */ + @NonNull + public OutputWire getOutputWire() { + return primaryOutputWire; + } + + /** + * By default a component has a single output wire (i.e. the primary output wire). This method allows additional + * output wires to be created. + * + *

+ * Unlike primary wires, secondary output wires need to be passed to a component's constructor. It is considered a + * violation of convention to push data into a secondary output wire from any code that is not executing within this + * task scheduler. + * + * @param the type of data that is transmitted over this output wire + * @return the secondary output wire + */ + @NonNull + public OutputWire buildSecondaryOutputWire() { + // Intentionally do not register this with the model. Connections using this output wire will be represented + // in the model in the same way as connections to the primary output wire. + return new OutputWire<>(model, name); + } + + /** + * Add a task to the scheduler. May block if back pressure is enabled. + * + * @param handler handles the provided data + * @param data the data to be processed by the task scheduler + */ + protected abstract void put(@NonNull Consumer handler, @Nullable Object data); + + /** + * Add a task to the scheduler. If backpressure is enabled and there is not immediately capacity available, this + * method will not accept the data. + * + * @param handler handles the provided data + * @param data the data to be processed by the scheduler + * @return true if the data was accepted, false otherwise + */ + protected abstract boolean offer(@NonNull Consumer handler, @Nullable Object data); + + /** + * Inject data into the scheduler, doing so even if it causes the number of unprocessed tasks to exceed the capacity + * specified by configured back pressure. If backpressure is disabled, this operation is logically equivalent to + * {@link #put(Consumer, Object)}. + * + * @param handler handles the provided data + * @param data the data to be processed by the scheduler + */ + protected abstract void inject(@NonNull Consumer handler, @Nullable Object data); + + /** + * Throw an {@link UnsupportedOperationException} if flushing is not enabled. + */ + protected final void throwIfFlushDisabled() { + if (!flushEnabled) { + throw new UnsupportedOperationException("Flushing is not enabled for the task scheduler " + name); + } + } + + /** + * Pass data to this scheduler's primary output wire. + *

+ * This method is implemented here to allow classes in this package to call forward(), which otherwise would not be + * visible. + */ + protected final void forward(@NonNull final OUT data) { + primaryOutputWire.forward(data); + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/TaskSchedulerBuilder.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/TaskSchedulerBuilder.java new file mode 100644 index 000000000000..31d66d15f77b --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/TaskSchedulerBuilder.java @@ -0,0 +1,346 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring; + +import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; + +import com.swirlds.common.metrics.extensions.FractionalTimer; +import com.swirlds.common.metrics.extensions.NoOpFractionalTimer; +import com.swirlds.common.wiring.counters.BackpressureObjectCounter; +import com.swirlds.common.wiring.counters.MultiObjectCounter; +import com.swirlds.common.wiring.counters.NoOpObjectCounter; +import com.swirlds.common.wiring.counters.ObjectCounter; +import com.swirlds.common.wiring.counters.StandardObjectCounter; +import com.swirlds.common.wiring.schedulers.ConcurrentTaskScheduler; +import com.swirlds.common.wiring.schedulers.SequentialTaskScheduler; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.lang.Thread.UncaughtExceptionHandler; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.ForkJoinPool; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * A builder for {@link TaskScheduler}s. + * + * @param the output type of the primary output wire for this task scheduler (use {@link Void} for a scheduler with + * no output) + */ +public class TaskSchedulerBuilder { + + private static final Logger logger = LogManager.getLogger(TaskSchedulerBuilder.class); + + public static final long UNLIMITED_CAPACITY = -1; + + private final WiringModel model; + + private boolean concurrent = false; + private final String name; + private TaskSchedulerMetricsBuilder metricsBuilder; + private long unhandledTaskCapacity = UNLIMITED_CAPACITY; + private boolean flushingEnabled = false; + private boolean externalBackPressure = false; + private ObjectCounter onRamp; + private ObjectCounter offRamp; + private ForkJoinPool pool = ForkJoinPool.commonPool(); + private UncaughtExceptionHandler uncaughtExceptionHandler; + + private Duration sleepDuration = Duration.ofNanos(100); + + /** + * Constructor. + * + * @param name the name of the task scheduler. Used for metrics and debugging. Must be unique. Must only contain + * alphanumeric characters and underscores. + */ + TaskSchedulerBuilder(@NonNull final WiringModel model, @NonNull final String name) { + this.model = Objects.requireNonNull(model); + + // The reason why wire names have a restricted character set is because downstream consumers of metrics + // are very fussy about what characters are allowed in metric names. + if (!name.matches("^[a-zA-Z0-9_]*$")) { + throw new IllegalArgumentException( + "Task Schedulers name must only contain alphanumeric characters and underscores"); + } + if (name.isEmpty()) { + throw new IllegalArgumentException("TaskScheduler name must not be empty"); + } + this.name = name; + } + + /** + * Set whether the scheduler should be concurrent or not. Default false. + * + * @param concurrent true if the task scheduler should be concurrent, false otherwise + * @return this + */ + @NonNull + public TaskSchedulerBuilder withConcurrency(final boolean concurrent) { + this.concurrent = concurrent; + return this; + } + + /** + * Set the maximum number of permitted scheduled tasks. Default is unlimited. + * + * @param unhandledTaskCapacity the maximum number of permitted unhandled tasks + * @return this + */ + @NonNull + public TaskSchedulerBuilder withUnhandledTaskCapacity(final long unhandledTaskCapacity) { + this.unhandledTaskCapacity = unhandledTaskCapacity; + return this; + } + + /** + * Set whether the task scheduler should enable flushing. Default false. Flushing a scheduler with this disabled + * will cause the scheduler to throw an exception. Enabling flushing may add overhead. + * + * @param requireFlushCapability true if the wire should require flush capability, false otherwise + * @return this + */ + @NonNull + public TaskSchedulerBuilder withFlushingEnabled(final boolean requireFlushCapability) { + this.flushingEnabled = requireFlushCapability; + return this; + } + + /** + * Specify an object counter that should be notified when data is added to the task scheduler. This is useful for + * implementing backpressure that spans multiple schedulers. + * + * @param onRamp the object counter that should be notified when data is added to the task scheduler + * @return this + */ + @NonNull + public TaskSchedulerBuilder withOnRamp(@NonNull final ObjectCounter onRamp) { + this.onRamp = Objects.requireNonNull(onRamp); + return this; + } + + /** + * Specify an object counter that should be notified when data is removed from the task scheduler. This is useful + * for implementing backpressure that spans multiple schedulers. + * + * @param offRamp the object counter that should be notified when data is removed from the wire + * @return this + */ + @NonNull + public TaskSchedulerBuilder withOffRamp(@NonNull final ObjectCounter offRamp) { + this.offRamp = Objects.requireNonNull(offRamp); + return this; + } + + /** + * If true then the framework will assume that back pressure is being applied via external mechanisms. + *

+ * This method does not change the runtime behavior of the wiring framework. But it does affect cyclical back + * pressure detection and automatically generated wiring diagrams. + * + * @param externalBackPressure true if back pressure is being applied externally, false otherwise + * @return this + */ + public TaskSchedulerBuilder withExternalBackPressure(final boolean externalBackPressure) { + this.externalBackPressure = externalBackPressure; + return this; + } + + /** + * If a method needs to block, this is the amount of time that is slept while waiting for the needed condition. + * + * @param backpressureSleepDuration the length of time to sleep when backpressure needs to be applied + * @return this + */ + @NonNull + public TaskSchedulerBuilder withSleepDuration(@NonNull final Duration backpressureSleepDuration) { + if (backpressureSleepDuration.isNegative()) { + throw new IllegalArgumentException("Backpressure sleep duration must not be negative"); + } + this.sleepDuration = backpressureSleepDuration; + return this; + } + + /** + * Provide a builder for metrics. If none is provided then no metrics will be enabled. + * + * @param metricsBuilder the metrics builder + * @return this + */ + @NonNull + public TaskSchedulerBuilder withMetricsBuilder(@NonNull final TaskSchedulerMetricsBuilder metricsBuilder) { + this.metricsBuilder = Objects.requireNonNull(metricsBuilder); + return this; + } + + /** + * Provide a custom thread pool for this task scheduler. If none is provided then the common fork join pool will be + * used. + * + * @param pool the thread pool + * @return this + */ + @NonNull + public TaskSchedulerBuilder withPool(@NonNull final ForkJoinPool pool) { + this.pool = Objects.requireNonNull(pool); + return this; + } + + /** + * Provide a custom uncaught exception handler for this task scheduler. If none is provided then the default + * uncaught exception handler will be used. The default handler will write a message to the log. + * + * @param uncaughtExceptionHandler the uncaught exception handler + * @return this + */ + @NonNull + public TaskSchedulerBuilder withUncaughtExceptionHandler( + @NonNull final UncaughtExceptionHandler uncaughtExceptionHandler) { + this.uncaughtExceptionHandler = Objects.requireNonNull(uncaughtExceptionHandler); + return this; + } + + /** + * Build an uncaught exception handler if one was not provided. + * + * @return the uncaught exception handler + */ + @NonNull + private UncaughtExceptionHandler buildUncaughtExceptionHandler() { + if (uncaughtExceptionHandler != null) { + return uncaughtExceptionHandler; + } else { + return (thread, throwable) -> + logger.error(EXCEPTION.getMarker(), "Uncaught exception in scheduler {}", name, throwable); + } + } + + private record Counters(@NonNull ObjectCounter onRamp, @NonNull ObjectCounter offRamp) {} + + /** + * Combine two counters into one. + * + * @param innerCounter the counter needed for internal implementation details, or null if not needed + * @param outerCounter the counter provided by the outer scope, or null if not provided + * @return the combined counter, or a no op counter if both are null + */ + @NonNull + private static ObjectCounter combineCounters( + @Nullable final ObjectCounter innerCounter, @Nullable final ObjectCounter outerCounter) { + if (innerCounter == null) { + if (outerCounter == null) { + return NoOpObjectCounter.getInstance(); + } else { + return outerCounter; + } + } else { + if (outerCounter == null) { + return innerCounter; + } else { + return new MultiObjectCounter(innerCounter, outerCounter); + } + } + } + + /** + * Figure out which counters to use for this task scheduler (if any), constructing them if they need to be + * constructed. + */ + @NonNull + private Counters buildCounters() { + final ObjectCounter innerCounter; + + // If we need to enforce a maximum capacity, we have no choice but to use a backpressure object counter. + // + // If we don't need to enforce a maximum capacity, we need to use a standard object counter if any + // of the following conditions are true: + // - we have unhandled task metrics enabled + // - the scheduler is concurrent and flushing is enabled. This is because the concurrent scheduler's + // flush implementation requires a counter that is not a no-op counter. + // + // In all other cases, better to use a no-op counter. Counters have overhead, and if we don't need one + // then we shouldn't use one. + + if (unhandledTaskCapacity != UNLIMITED_CAPACITY) { + innerCounter = new BackpressureObjectCounter(name, unhandledTaskCapacity, sleepDuration); + } else if ((metricsBuilder != null && metricsBuilder.isUnhandledTaskMetricEnabled()) + || (concurrent && flushingEnabled)) { + innerCounter = new StandardObjectCounter(sleepDuration); + } else { + innerCounter = null; + } + + return new Counters(combineCounters(innerCounter, onRamp), combineCounters(innerCounter, offRamp)); + } + + /** + * Build a busy timer if enabled. + * + * @return the busy timer, or null if not enabled + */ + @NonNull + private FractionalTimer buildBusyTimer() { + if (metricsBuilder == null || !metricsBuilder.isBusyFractionMetricEnabled()) { + return NoOpFractionalTimer.getInstance(); + } + if (concurrent) { + throw new IllegalStateException("Busy fraction metric is not compatible with concurrent wires"); + } + return metricsBuilder.buildBusyTimer(); + } + + /** + * Build the task scheduler. + * + * @return the task scheduler + */ + @NonNull + public TaskScheduler build() { + final Counters counters = buildCounters(); + final FractionalTimer busyFractionTimer = buildBusyTimer(); + + if (metricsBuilder != null) { + metricsBuilder.registerMetrics(name, counters.onRamp()); + } + + final boolean insertionIsBlocking = unhandledTaskCapacity != UNLIMITED_CAPACITY || externalBackPressure; + + if (concurrent) { + return new ConcurrentTaskScheduler<>( + model, + name, + pool, + buildUncaughtExceptionHandler(), + counters.onRamp(), + counters.offRamp(), + flushingEnabled, + insertionIsBlocking); + } else { + return new SequentialTaskScheduler<>( + model, + name, + pool, + buildUncaughtExceptionHandler(), + counters.onRamp(), + counters.offRamp(), + busyFractionTimer, + flushingEnabled, + insertionIsBlocking); + } + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/TaskSchedulerMetricsBuilder.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/TaskSchedulerMetricsBuilder.java new file mode 100644 index 000000000000..98b17ddf0778 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/TaskSchedulerMetricsBuilder.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring; + +import com.swirlds.base.time.Time; +import com.swirlds.common.metrics.FunctionGauge; +import com.swirlds.common.metrics.Metrics; +import com.swirlds.common.metrics.extensions.FractionalTimer; +import com.swirlds.common.metrics.extensions.NoOpFractionalTimer; +import com.swirlds.common.metrics.extensions.StandardFractionalTimer; +import com.swirlds.common.wiring.counters.ObjectCounter; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Objects; + +/** + * Configures metrics for a {@link TaskScheduler}. + */ +public class TaskSchedulerMetricsBuilder { + + private final Metrics metrics; + private final Time time; + private boolean unhandledTaskMetricEnabled = false; + private boolean busyFractionMetricEnabled = false; + private StandardFractionalTimer busyFractionTimer; + + /** + * Constructor. + * + * @param metrics the metrics object to configure + * @param time the time object to use for metrics + */ + TaskSchedulerMetricsBuilder(@NonNull final Metrics metrics, @NonNull final Time time) { + this.metrics = Objects.requireNonNull(metrics); + this.time = Objects.requireNonNull(time); + } + + /** + * Set whether the unhandled task count metric should be enabled. Default false. + * + * @param enabled true if the unhandled task count metric should be enabled, false otherwise + * @return this + */ + @NonNull + public TaskSchedulerMetricsBuilder withUnhandledTaskMetricEnabled(final boolean enabled) { + this.unhandledTaskMetricEnabled = enabled; + return this; + } + + /** + * Set whether the busy fraction metric should be enabled. Default false. + *

+ * Note: this metric is currently only compatible with non-concurrent task scheduler implementations. At a future + * time this metric may be updated to work with concurrent scheduler implementations. + * + * @param enabled true if the busy fraction metric should be enabled, false otherwise + * @return this + */ + public TaskSchedulerMetricsBuilder withBusyFractionMetricsEnabled(final boolean enabled) { + this.busyFractionMetricEnabled = enabled; + return this; + } + + /** + * Check if the scheduled task count metric is enabled. + * + * @return true if the scheduled task count metric is enabled, false otherwise + */ + boolean isUnhandledTaskMetricEnabled() { + return unhandledTaskMetricEnabled; + } + + /** + * Check if the busy fraction metric is enabled. + * + * @return true if the busy fraction metric is enabled, false otherwise + */ + boolean isBusyFractionMetricEnabled() { + return busyFractionMetricEnabled; + } + + /** + * Build a fractional timer (if enabled) + * + * @return the fractional timer + */ + @NonNull + FractionalTimer buildBusyTimer() { + if (busyFractionMetricEnabled) { + busyFractionTimer = new StandardFractionalTimer(time); + return busyFractionTimer; + } else { + return NoOpFractionalTimer.getInstance(); + } + } + + /** + * Register all configured metrics. + * + * @param taskSchedulerName the name of the task scheduler + * @param unhandledTaskCounter the counter that is used to track the number of scheduled tasks + */ + void registerMetrics(@NonNull final String taskSchedulerName, @Nullable final ObjectCounter unhandledTaskCounter) { + if (unhandledTaskMetricEnabled) { + Objects.requireNonNull(unhandledTaskCounter); + + final FunctionGauge.Config config = new FunctionGauge.Config<>( + "platform", + taskSchedulerName + "_unhandled_task_count", + Long.class, + unhandledTaskCounter::getCount) + .withDescription("The number of scheduled tasks that have not been fully handled for the scheduler " + + taskSchedulerName); + metrics.getOrCreate(config); + } + + if (busyFractionMetricEnabled) { + busyFractionTimer.registerMetric( + metrics, + "platform", + taskSchedulerName + "_busy_fraction", + "Fraction (out of 1.0) of time spent processing tasks for the task scheduler " + taskSchedulerName); + } + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/WiringModel.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/WiringModel.java new file mode 100644 index 000000000000..bccb67057909 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/WiringModel.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring; + +import com.swirlds.base.time.Time; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.wiring.model.StandardWiringModel; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Objects; +import java.util.Set; + +/** + * A wiring model is a collection of task schedulers and the wires connecting them. It can be used to analyze the wiring + * of a system and to generate diagrams. + */ +public abstract class WiringModel { + + private final PlatformContext platformContext; + private final Time time; + + /** + * Constructor. + * + * @param platformContext the platform context + * @param time provides wall clock time + */ + protected WiringModel(@NonNull final PlatformContext platformContext, @NonNull final Time time) { + this.platformContext = Objects.requireNonNull(platformContext); + this.time = Objects.requireNonNull(time); + } + + /** + * Build a new wiring model instance. + * + * @param platformContext the platform context + * @param time provides wall clock time + * @return a new wiring model instance + */ + @NonNull + public static WiringModel create(@NonNull final PlatformContext platformContext, @NonNull final Time time) { + return new StandardWiringModel(platformContext, time); + } + + /** + * Get a new task scheduler builder. + * + * @param name the name of the task scheduler. Used for metrics and debugging. Must be unique. Must only contain + * alphanumeric characters and underscores. + * @return a new wire builder + */ + @NonNull + public final TaskSchedulerBuilder schedulerBuilder(@NonNull final String name) { + return new TaskSchedulerBuilder<>(this, name); + } + + /** + * Get a new task scheduler metrics builder. Can be passed to + * {@link TaskSchedulerBuilder#withMetricsBuilder(TaskSchedulerMetricsBuilder)} to add metrics to the task + * scheduler. + * + * @return a new task scheduler metrics builder + */ + @NonNull + public final TaskSchedulerMetricsBuilder metricsBuilder() { + return new TaskSchedulerMetricsBuilder(platformContext.getMetrics(), time); + } + + /** + * Check to see if there is cyclic backpressure in the wiring model. Cyclical back pressure can lead to deadlocks, + * and so it should be avoided at all costs. + * + *

+ * If this method finds cyclical backpressure, it will log a message that will fail standard platform tests. + * + * @return true if there is cyclical backpressure, false otherwise + */ + public abstract boolean checkForCyclicalBackpressure(); + + /** + * Generate a mermaid style wiring diagram. + * + * @param groups optional groupings of vertices + * @return a mermaid style wiring diagram + */ + @NonNull + public abstract String generateWiringDiagram(@NonNull final Set groups); + + /** + * Reserved for internal framework use. Do not call this method directly. + *

+ * Register a vertex in the wiring model. These are either task schedulers or wire transformers. Vertices + * always have a single Java object output type, although there may be many consumers of that output. Vertices may + * have many input types. + * + * @param vertexName the name of the vertex + * @param insertionIsBlocking if true then insertion may block until capacity is available + */ + public abstract void registerVertex(@NonNull String vertexName, final boolean insertionIsBlocking); + + /** + * Reserved for internal framework use. Do not call this method directly. + *

+ * Register an edge between two vertices. + * + * @param originVertex the origin vertex + * @param destinationVertex the destination vertex + * @param label the label of the edge + * @param injection true if this edge is an injection edge, false otherwise + */ + public abstract void registerEdge( + @NonNull String originVertex, + @NonNull String destinationVertex, + @NonNull String label, + final boolean injection); +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/BackpressureBlocker.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/BackpressureBlocker.java new file mode 100644 index 000000000000..e949df143d22 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/BackpressureBlocker.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.counters; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Objects; +import java.util.concurrent.ForkJoinPool.ManagedBlocker; +import java.util.concurrent.atomic.AtomicLong; + +/** + * This class is used to implement backpressure in a {@link java.util.concurrent.ForkJoinPool} friendly way. + */ +class BackpressureBlocker implements ManagedBlocker { + + /** + * The current count. This object will attempt to block until this count can be increased by one without exceeding + * the maximum capacity. + */ + private final AtomicLong count; + + /** + * The maximum desired capacity. It is possible that the current count may exceed this capacity (i.e. if + * {@link ObjectCounter#forceOnRamp()} is used to bypass the capacity). + */ + private final long capacity; + + /** + * The amount of time to sleep while waiting for capacity to become available, or 0 to not sleep. + */ + private final long sleepNanos; + + /** + * Constructor. + * + * @param count the counter to use + * @param capacity the maximum number of objects that can be in the part of the system that this object is being + * used to monitor before backpressure is applied + * @param sleepNanos the number of nanoseconds to sleep while blocking, or 0 to not sleep + */ + public BackpressureBlocker(@NonNull final AtomicLong count, final long capacity, final long sleepNanos) { + this.count = Objects.requireNonNull(count); + this.capacity = capacity; + this.sleepNanos = sleepNanos; + } + + /** + * This method just needs to block for a while. It sleeps to avoid burning CPU cycles. Since this method always + * returns false, the fork join pool will use {@link #isReleasable()} exclusively for determining if it's time to + * stop blocking. + */ + @Override + public boolean block() throws InterruptedException { + if (sleepNanos > 0) { + try { + NANOSECONDS.sleep(sleepNanos); + } catch (final InterruptedException e) { + // Don't throw an interrupted exception, but allow the thread to maintain its interrupted status. + Thread.currentThread().interrupt(); + } + } + + // Although we could technically check the count here and stop the back pressure if the count is below the + // threshold, it's simpler not to. Immediately after this method is called, isReleasable() will be called, + // which will do the checking for us. Easier to just let that method do the work, and have this method + // only be responsible for sleeping. + return false; + } + + /** + * Checks if it's ok to stop blocking. + * + * @return true if capacity has been reserved and it's ok to stop blocking, false if capacity could not be reserved + * and we need to continue blocking. + */ + @Override + public boolean isReleasable() { + while (true) { + final long currentCount = count.get(); + + if (currentCount >= capacity) { + // We've reached capacity, so we need to block. + return false; + } + + final boolean success = count.compareAndSet(currentCount, currentCount + 1); + if (success) { + // We've successfully incremented the count, so we're done. + return true; + } + + // We were unable to increment the count because another thread concurrently modified it. + // Try again. We will keep trying until we are either successful or we observe there is + // insufficient capacity. + } + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/BackpressureObjectCounter.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/BackpressureObjectCounter.java new file mode 100644 index 000000000000..c3cc87abaa4c --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/BackpressureObjectCounter.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.counters; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinPool.ManagedBlocker; +import java.util.concurrent.atomic.AtomicLong; + +/** + * A utility for counting the number of objects in various parts of the pipeline. Will apply backpressure if the number + * of objects exceeds a specified capacity. + */ +public class BackpressureObjectCounter extends ObjectCounter { + + private final String name; + private final AtomicLong count = new AtomicLong(0); + private final long capacity; + + /** + * When back pressure needs to be applied due to lack of capacity, this object is used to efficiently sleep on the + * fork join pool. + */ + private final ManagedBlocker onRampBlocker; + + /** + * When waiting for the count to reach zero, this object is used to efficiently sleep on the fork join pool. + */ + private final ManagedBlocker waitUntilEmptyBlocker; + + /** + * Constructor. + * + * @param name the name of the object counter, used creating more informative exceptions + * @param capacity the maximum number of objects that can be in the part of the system that this object is + * being used to monitor before backpressure is applied + * @param sleepDuration when a method needs to block, the duration to sleep while blocking + */ + public BackpressureObjectCounter( + @NonNull final String name, final long capacity, @NonNull final Duration sleepDuration) { + if (capacity <= 0) { + throw new IllegalArgumentException("Capacity must be greater than zero"); + } + + this.name = Objects.requireNonNull(name); + this.capacity = capacity; + + final long sleepNanos = sleepDuration.toNanos(); + + onRampBlocker = new BackpressureBlocker(count, capacity, sleepNanos); + waitUntilEmptyBlocker = new EmptyBlocker(count, sleepNanos); + } + + /** + * {@inheritDoc} + */ + @Override + public void onRamp() { + final long currentCount = count.get(); + if (currentCount < capacity) { + final boolean success = count.compareAndSet(currentCount, currentCount + 1); + if (success) { + return; + } + } + + // Slow case. Capacity wasn't reserved, so we may need to block. + + try { + // This will block until capacity is available and the count has been incremented. + // + // This is logically equivalent to the following pseudocode. + // Note that the managed block is thread safe when onRamp() is being called from multiple threads, + // even though this pseudocode is not. + // + // while (count >= capacity) { + // Thread.sleep(sleepNanos); + // } + // count++; + // + // The reason why we use the managedBlock() strategy instead of something simpler has to do with + // the fork join pool paradigm. Unlike traditional thread pools where we have more threads than + // CPUs, blocking (e.g. Thread.sleep()) on a fork join pool may monopolize an entire CPU core. + // The managedBlock() pattern allows us to block while yielding the physical CPU core to other + // tasks. + ForkJoinPool.managedBlock(onRampBlocker); + } catch (final InterruptedException ex) { + // This should be impossible. + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while blocking on an onRamp() for " + name); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean attemptOnRamp() { + final long currentCount = count.get(); + if (currentCount >= capacity) { + return false; + } + return count.compareAndSet(currentCount, currentCount + 1); + } + + /** + * {@inheritDoc} + */ + @Override + public void forceOnRamp() { + count.incrementAndGet(); + } + + /** + * {@inheritDoc} + */ + @Override + public void offRamp() { + count.decrementAndGet(); + } + + /** + * {@inheritDoc} + */ + @Override + public long getCount() { + return count.get(); + } + + /** + * {@inheritDoc} + */ + @Override + public void waitUntilEmpty() { + if (count.get() == 0) { + return; + } + + try { + ForkJoinPool.managedBlock(waitUntilEmptyBlocker); + } catch (final InterruptedException e) { + // This should be impossible. + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while blocking on an waitUntilEmpty() for " + name); + } + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/EmptyBlocker.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/EmptyBlocker.java new file mode 100644 index 000000000000..ef87c723d749 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/EmptyBlocker.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.counters; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Objects; +import java.util.concurrent.ForkJoinPool.ManagedBlocker; +import java.util.concurrent.atomic.AtomicLong; + +/** + * This class is used to implement flushing in a {@link java.util.concurrent.ForkJoinPool} friendly way. Blocks until + * the count reaches zero. + */ +class EmptyBlocker implements ManagedBlocker { + + private final AtomicLong count; + private final long sleepNanos; + + /** + * Constructor. + * + * @param count the counter to use + * @param sleepNanos the number of nanoseconds to sleep while blocking, or 0 to not sleep + */ + public EmptyBlocker(@NonNull final AtomicLong count, final long sleepNanos) { + this.count = Objects.requireNonNull(count); + this.sleepNanos = sleepNanos; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean block() throws InterruptedException { + if (sleepNanos > 0) { + try { + NANOSECONDS.sleep(sleepNanos); + } catch (final InterruptedException e) { + // Don't throw an interrupted exception, but allow the thread to maintain its interrupted status. + Thread.currentThread().interrupt(); + } + } + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isReleasable() { + return count.get() == 0; + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/MultiObjectCounter.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/MultiObjectCounter.java new file mode 100644 index 000000000000..d16e2924a29f --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/MultiObjectCounter.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.counters; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Objects; + +/** + * This object counter combines multiple counters into a single counter. Every time a method on this object is called, + * the same method is also called on all child counters. + */ +public class MultiObjectCounter extends ObjectCounter { + + private final ObjectCounter[] counters; + + /** + * Constructor. + * + * @param counters one or more counters. The first counter in the array is the primary counter. {@link #getCount()} + * always return the count of the primary counter. When {@link #attemptOnRamp()} is called, + * on-ramping is attempted in the primary counter. If that fails, no other counter is on-ramped. If + * that succeeds then on-ramping is forced in all other counters. + */ + public MultiObjectCounter(@NonNull final ObjectCounter... counters) { + this.counters = Objects.requireNonNull(counters); + if (counters.length == 0) { + throw new IllegalArgumentException("Must have at least one counter"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void onRamp() { + for (final ObjectCounter counter : counters) { + counter.onRamp(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean attemptOnRamp() { + final boolean success = counters[0].attemptOnRamp(); + if (!success) { + return false; + } + + for (int i = 1; i < counters.length; i++) { + counters[i].forceOnRamp(); + } + + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public void forceOnRamp() { + for (final ObjectCounter counter : counters) { + counter.forceOnRamp(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void offRamp() { + for (final ObjectCounter counter : counters) { + counter.offRamp(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public long getCount() { + return counters[0].getCount(); + } + + /** + * {@inheritDoc} + */ + @Override + public void waitUntilEmpty() { + for (final ObjectCounter counter : counters) { + counter.waitUntilEmpty(); + } + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/NoCapacityException.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/NoCapacityException.java new file mode 100644 index 000000000000..99b4dec018c3 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/NoCapacityException.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.counters; + +/** + * An exception thrown when an attempt is made to increment a counter that is already at capacity. + */ +class NoCapacityException extends RuntimeException {} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/NoOpObjectCounter.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/NoOpObjectCounter.java new file mode 100644 index 000000000000..62207a0c5cb6 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/NoOpObjectCounter.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.counters; + +/** + * A counter that doesn't actually count. Saves us from having to do a (counter == null) check in the standard case. + */ +public class NoOpObjectCounter extends ObjectCounter { + + private static final NoOpObjectCounter INSTANCE = new NoOpObjectCounter(); + + /** + * Get the singleton instance. + * + * @return the singleton instance + */ + public static NoOpObjectCounter getInstance() { + return INSTANCE; + } + + /** + * Constructor. + */ + private NoOpObjectCounter() {} + + /** + * {@inheritDoc} + */ + @Override + public void onRamp() {} + + /** + * {@inheritDoc} + */ + @Override + public boolean attemptOnRamp() { + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public void forceOnRamp() {} + + /** + * {@inheritDoc} + */ + @Override + public void offRamp() {} + + /** + * {@inheritDoc} + */ + @Override + public long getCount() { + return -1; + } + + /** + * {@inheritDoc} + */ + @Override + public void waitUntilEmpty() {} +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/ObjectCounter.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/ObjectCounter.java new file mode 100644 index 000000000000..d6e328ef37f4 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/ObjectCounter.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.counters; + +/** + * A class that counts the number of objects in various parts of the pipeline. + */ +public abstract class ObjectCounter { + + /** + * Signal that an object is entering the part of the system that this object is being used to monitor. + */ + public abstract void onRamp(); + + /** + * Signal that an object is entering the part of the system that this object is being used to monitor. Object is not + * "on ramped" if it is not immediately possible to do so without violating capacity constraints. + * + * @return true if there was available capacity to on ramp the object, false otherwise + */ + public abstract boolean attemptOnRamp(); + + /** + * Signal that an object is entering the part of the system that this object is being used to monitor. If there is + * not enough capacity to on ramp the object, on ramp it anyway and ignore all capacity restrictions. + */ + public abstract void forceOnRamp(); + + /** + * Signal that an object is leaving the part of the system that this object is being used to monitor. + */ + public abstract void offRamp(); + + /** + * Get the number of objects in the part of the system that this object is being used to monitor. + */ + public abstract long getCount(); + + /** + * Blocks until the number of objects off-ramped is equal to the number of objects on-ramped. Does not prevent + * new objects from being on-ramped. If new objects are continuously on-ramped, it is possible that this method + * may block indefinitely. + */ + public abstract void waitUntilEmpty(); +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/StandardObjectCounter.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/StandardObjectCounter.java new file mode 100644 index 000000000000..921ccbf9c23c --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/StandardObjectCounter.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.counters; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Duration; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinPool.ManagedBlocker; +import java.util.concurrent.atomic.AtomicLong; + +/** + * A utility for counting the number of objects in various parts of the pipeline. + */ +public class StandardObjectCounter extends ObjectCounter { + + private final AtomicLong count = new AtomicLong(0); + private final ManagedBlocker waitUntilEmptyBlocker; + + /** + * Constructor. + * + * @param sleepDuration when a method needs to block, the duration to sleep while blocking + */ + public StandardObjectCounter(@NonNull final Duration sleepDuration) { + final long sleepNanos = sleepDuration.toNanos(); + waitUntilEmptyBlocker = new EmptyBlocker(count, sleepNanos); + } + + /** + * {@inheritDoc} + */ + @Override + public void onRamp() { + count.incrementAndGet(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean attemptOnRamp() { + count.getAndIncrement(); + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public void forceOnRamp() { + count.getAndIncrement(); + } + + /** + * {@inheritDoc} + */ + @Override + public void offRamp() { + count.decrementAndGet(); + } + + /** + * {@inheritDoc} + */ + @Override + public long getCount() { + return count.get(); + } + + /** + * {@inheritDoc} + */ + @Override + public void waitUntilEmpty() { + if (count.get() == 0) { + return; + } + + try { + ForkJoinPool.managedBlock(waitUntilEmptyBlocker); + } catch (final InterruptedException e) { + // This should be impossible. + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while blocking on an waitUntilEmpty()"); + } + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/model/CycleFinder.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/model/CycleFinder.java new file mode 100644 index 000000000000..0df9cd0e1c1c --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/model/CycleFinder.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.model; + +import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; +import static com.swirlds.logging.legacy.LogMarker.STARTUP; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * A utility for finding cyclical back pressure in a wiring model. + */ +public final class CycleFinder { + + private static final Logger logger = LogManager.getLogger(CycleFinder.class); + + private CycleFinder() {} + + /** + * Check for cyclical back pressure in a wiring model. + * + * @param vertices the vertices in the wiring model + * @return true if there is cyclical backpressure + */ + public static boolean checkForCyclicalBackPressure(@NonNull final Collection vertices) { + for (final ModelVertex vertex : vertices) { + if (checkForCycleStartingFromVertex(vertex)) { + return true; + } + } + logger.info(STARTUP.getMarker(), "No cyclical back pressure detected in wiring model."); + return false; + } + + /** + * Check for a cycle starting from a vertex. + * + * @param start the vertex to start from + * @return true if there is a cycle + */ + private static boolean checkForCycleStartingFromVertex(@NonNull final ModelVertex start) { + + // Perform a depth first traversal of the graph starting from the given vertex. + // Ignore any edge that doesn't apply back pressure. + + final Deque stack = new LinkedList<>(); + stack.addLast(start); + + final Set visited = new HashSet<>(); + + // Track the parent of each vertex. Useful for tracing the cycle after it's detected. + final Map parents = new HashMap<>(); + + while (!stack.isEmpty()) { + + final ModelVertex parent = stack.removeLast(); + + for (final ModelEdge childEdge : parent) { + if (!childEdge.insertionIsBlocking()) { + // Ignore non-blocking edges. + continue; + } + + final ModelVertex child = childEdge.destination(); + + if (child.equals(start)) { + // We've found a cycle! + parents.put(child, parent); + logCycle(start, parents); + return true; + } + + if (visited.add(child)) { + stack.addLast(child); + parents.put(child, parent); + } + } + } + return false; + } + + /** + * Logs a warning message when cyclical back pressure is detected. Is intended to fail standard test validators. + * + * @param start the vertex where the cycle was detected + * @param parents records the parents for the traversal, used to trace the cycle + */ + private static void logCycle( + @NonNull final ModelVertex start, @NonNull final Map parents) { + + final StringBuilder sb = new StringBuilder(); + sb.append("Cyclical back pressure detected in wiring model. Cycle: "); + + // Following parent links will walk the cycle in reverse order. + final List path = new ArrayList<>(); + path.add(start); + ModelVertex target = start; + + while (!target.equals(start) || path.size() == 1) { + target = parents.get(target); + path.add(target); + } + + for (int i = path.size() - 1; i >= 0; i--) { + sb.append(path.get(i).getName()); + if (i > 0) { + sb.append(" -> "); + } + } + + logger.error(EXCEPTION.getMarker(), sb.toString()); + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/model/ModelEdge.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/model/ModelEdge.java new file mode 100644 index 000000000000..66d520754634 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/model/ModelEdge.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.model; + +import static com.swirlds.common.utility.NonCryptographicHashing.hash32; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A directed edge between to vertices. + * + * @param source the source vertex + * @param destination the destination vertex + * @param label the label of the edge, if a label is not needed for an edge then holds the value "" + * @param insertionIsBlocking true if the insertion of this edge may block until capacity is available + */ +public record ModelEdge( + @NonNull ModelVertex source, + @NonNull ModelVertex destination, + @NonNull String label, + boolean insertionIsBlocking) + implements Comparable { + + @Override + public boolean equals(Object obj) { + if (obj instanceof ModelEdge that) { + return this.source.equals(that.source) + && this.destination.equals(that.destination) + && this.label.equals(that.label); + } + return false; + } + + @Override + public int hashCode() { + return hash32(source.hashCode(), destination.hashCode(), label.hashCode()); + } + + /** + * Useful for looking at a model in a debugger. + */ + @Override + public String toString() { + return source + " --" + label + "-->" + (insertionIsBlocking ? "" : ">") + " " + destination; + } + + /** + * Sorts first by source, then by destination, then by label. + */ + @Override + public int compareTo(@NonNull final ModelEdge that) { + if (!this.source.equals(that.source)) { + return this.source.compareTo(that.source); + } + if (!this.destination.equals(that.destination)) { + return this.destination.compareTo(that.destination); + } + return this.label.compareTo(that.label); + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/model/ModelVertex.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/model/ModelVertex.java new file mode 100644 index 000000000000..79dc0e26b917 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/model/ModelVertex.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.model; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +/** + * A vertex in a wiring model. + */ +public class ModelVertex implements Iterable, Comparable { + + private final String name; + private final boolean insertionIsBlocking; + + private final List outgoingEdges = new ArrayList<>(); + + /** + * Constructor. + * + * @param name the name of the vertex + * @param insertionIsBlocking true if the insertion of this vertex may block until capacity is available + */ + public ModelVertex(@NonNull final String name, final boolean insertionIsBlocking) { + this.name = name; + this.insertionIsBlocking = insertionIsBlocking; + } + + /** + * Get the name of the vertex. + * + * @return the name + */ + @NonNull + public String getName() { + return name; + } + + /** + * Get whether the insertion of this vertex may block until capacity is available. + * + * @return true if the insertion of this vertex may block until capacity is available + */ + public boolean isInsertionIsBlocking() { + return insertionIsBlocking; + } + + /** + * Add an outgoing edge to this vertex. + * + * @param vertex the edge to connect to + */ + public void connectToEdge(@NonNull final ModelEdge vertex) { + outgoingEdges.add(Objects.requireNonNull(vertex)); + } + + /** + * Get an iterator that walks over the outgoing edges of this vertex. + * + * @return an iterator that walks over the outgoing edges of this vertex + */ + @Override + @NonNull + public Iterator iterator() { + return outgoingEdges.iterator(); + } + + /** + * Get the outgoing edges of this vertex. + * + * @return the outgoing edges of this vertex + */ + @NonNull + public List getOutgoingEdges() { + return outgoingEdges; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return name.hashCode(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof final ModelVertex that) { + return name.equals(that.name); + } + return false; + } + + /** + * Makes the vertex nicer to look at in a debugger. + */ + @Override + public String toString() { + if (insertionIsBlocking) { + return "[" + name + "]"; + } else { + return "(" + name + ")"; + } + } + + /** + * Sorts vertices by alphabetical order. + */ + @Override + public int compareTo(@NonNull final ModelVertex that) { + return name.compareTo(that.name); + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/model/StandardWiringModel.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/model/StandardWiringModel.java new file mode 100644 index 000000000000..0ff00ae7c19d --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/model/StandardWiringModel.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.model; + +import com.swirlds.base.time.Time; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.wiring.ModelGroup; +import com.swirlds.common.wiring.WiringModel; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * A standard implementation of a wiring model. + */ +public class StandardWiringModel extends WiringModel { + + /** + * A map of vertex names to vertices. + */ + private final Map vertices = new HashMap<>(); + + /** + * A set of all edges in the model. + */ + private final Set edges = new HashSet<>(); + + /** + * Constructor. + * + * @param platformContext the platform context + * @param time provides wall clock time + */ + public StandardWiringModel(@NonNull final PlatformContext platformContext, @NonNull final Time time) { + super(platformContext, time); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean checkForCyclicalBackpressure() { + return CycleFinder.checkForCyclicalBackPressure(vertices.values()); + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public String generateWiringDiagram(@NonNull final Set groups) { + return WiringFlowchart.generateWiringDiagram(vertices, edges, groups); + } + + /** + * {@inheritDoc} + */ + @Override + public void registerVertex(@NonNull final String vertexName, final boolean insertionIsBlocking) { + Objects.requireNonNull(vertexName); + final boolean unique = vertices.put(vertexName, new ModelVertex(vertexName, insertionIsBlocking)) == null; + if (!unique) { + throw new IllegalArgumentException("Duplicate vertex name: " + vertexName); + } + } + + /** + * Find an existing vertex + * + * @param vertexName the name of the vertex + * @return the vertex + */ + @NonNull + private ModelVertex getVertex(@NonNull final String vertexName) { + final ModelVertex vertex = vertices.get(vertexName); + if (vertex == null) { + throw new IllegalArgumentException("Unknown vertex name: " + vertexName); + } + return vertex; + } + + /** + * {@inheritDoc} + */ + @Override + public void registerEdge( + @NonNull final String originVertex, + @NonNull final String destinationVertex, + @NonNull final String label, + final boolean injection) { + + final ModelVertex origin = getVertex(originVertex); + final ModelVertex destination = getVertex(destinationVertex); + final boolean blocking = !injection && destination.isInsertionIsBlocking(); + + final ModelEdge edge = new ModelEdge(origin, destination, label, blocking); + origin.connectToEdge(edge); + + final boolean unique = edges.add(edge); + if (!unique) { + throw new IllegalArgumentException( + "Duplicate edge: " + originVertex + " -> " + destinationVertex + ", label = " + label); + } + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/model/WiringFlowchart.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/model/WiringFlowchart.java new file mode 100644 index 000000000000..ccdb87e1c20c --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/model/WiringFlowchart.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.model; + +import com.swirlds.common.wiring.ModelGroup; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A utility for drawing mermaid style flowcharts of wiring models. + */ +public final class WiringFlowchart { + + private WiringFlowchart() {} + + private static final String INDENTATION = " "; + private static final String COMPONENT_COLOR = "362"; + private static final String GROUP_COLOR = "555"; + + /** + * Draw an edge. + * + * @param sb a string builder where the mermaid file is being assembled + * @param edge the edge to draw + * @param collapsedVertexMap a map from vertices that are in collapsed groups to the group name that they should be + * replaced with + */ + private static void drawEdge( + @NonNull final StringBuilder sb, + @NonNull final ModelEdge edge, + @NonNull final Map collapsedVertexMap) { + + final String source; + if (collapsedVertexMap.containsKey(edge.source())) { + source = collapsedVertexMap.get(edge.source()); + } else { + source = edge.source().getName(); + } + + final String destination; + if (collapsedVertexMap.containsKey(edge.destination())) { + destination = collapsedVertexMap.get(edge.destination()); + + } else { + destination = edge.destination().getName(); + } + + if (source.equals(destination)) { + // Don't draw arrows from a component back to itself. + return; + } + + sb.append(INDENTATION).append(source); + + if (edge.insertionIsBlocking()) { + if (edge.label().isEmpty()) { + sb.append(" --> "); + } else { + sb.append(" -- \"").append(edge.label()).append("\" --> "); + } + } else { + if (edge.label().isEmpty()) { + sb.append(" -.-> "); + } else { + sb.append(" -. \"").append(edge.label()).append("\" .-> "); + } + } + sb.append(destination).append("\n"); + } + + /** + * Draw a vertex. + * + * @param sb a string builder where the mermaid file is being assembled + * @param vertex the vertex to draw + * @param collapsedVertexMap a map from vertices that are in collapsed groups to the group name that they should be + * replaced with + * @param indentLevel the level of indentation + */ + private static void drawVertex( + @NonNull final StringBuilder sb, + @NonNull final ModelVertex vertex, + @NonNull final Map collapsedVertexMap, + final int indentLevel) { + + if (!collapsedVertexMap.containsKey(vertex)) { + sb.append(INDENTATION.repeat(indentLevel)).append(vertex.getName()).append("\n"); + sb.append(INDENTATION.repeat(indentLevel)) + .append("style ") + .append(vertex.getName()) + .append(" fill:#") + .append(COMPONENT_COLOR) + .append(",stroke:#000,stroke-width:2px,color:#fff\n"); + } + } + + private static void drawGroup( + @NonNull final StringBuilder sb, + @NonNull final ModelGroup group, + @NonNull final Set vertices, + @NonNull final Map collapsedVertexMap) { + + sb.append(INDENTATION).append("subgraph ").append(group.name()).append("\n"); + + final String color; + if (group.collapse()) { + color = COMPONENT_COLOR; + } else { + color = GROUP_COLOR; + } + + sb.append(INDENTATION.repeat(2)) + .append("style ") + .append(group.name()) + .append(" fill:#") + .append(color) + .append(",stroke:#000,stroke-width:2px,color:#fff\n"); + + vertices.stream().sorted().forEachOrdered(vertex -> drawVertex(sb, vertex, collapsedVertexMap, 2)); + sb.append(INDENTATION).append("end\n"); + } + + /** + * Get the actual list of vertices for each group (as opposed to just the names of the vertices in the groups). + * + * @return the map from group name to the vertices in that group + */ + @NonNull + private static Map> buildGroupMap( + @NonNull final Map vertices, @NonNull final Set groups) { + + final Map> groupMap = new HashMap<>(); + + for (final ModelGroup group : groups) { + groupMap.put(group.name(), new HashSet<>()); + for (final String vertexName : group.elements()) { + groupMap.get(group.name()).add(vertices.get(vertexName)); + } + } + + return groupMap; + } + + /** + * Get the list of vertices that are not in any group. + * + * @param vertices a map from vertex names to vertices + * @param groupMap a map of group names to the vertices in those groups + * @return the list of vertices that are not in any group + */ + private static List getUngroupedVertices( + @NonNull final Map vertices, + @NonNull Map> groupMap) { + + final Set uniqueVertices = new HashSet<>(vertices.values()); + + for (final Set group : groupMap.values()) { + for (final ModelVertex vertex : group) { + final boolean removed = uniqueVertices.remove(vertex); + if (!removed) { + throw new IllegalStateException("Vertex " + vertex.getName() + " is in multiple groups."); + } + } + } + + return new ArrayList<>(uniqueVertices); + } + + /** + * For all vertices that are in collapsed groups, we want to draw edges to the collapsed group instead of to the + * individual vertices in the group. This method builds a map from the collapsed vertices to the group name that + * they should be replaced with. + * + * @param groups the groups + * @param vertices a map from vertex names to vertices + * @return a map from collapsed vertices to the group name that they should be replaced with + */ + @NonNull + private static Map getCollapsedVertexMap( + @NonNull final Set groups, @NonNull final Map vertices) { + + final HashMap collapsedVertexMap = new HashMap<>(); + + for (final ModelGroup group : groups) { + if (!group.collapse()) { + continue; + } + + for (final String vertexName : group.elements()) { + collapsedVertexMap.put(vertices.get(vertexName), group.name()); + } + } + + return collapsedVertexMap; + } + + /** + * Generate a mermaid flowchart of the wiring model. + * + * @param vertices the vertices in the wiring model + * @param edges the edges in the wiring model + * @param groups the groups in the wiring model + * @return a mermaid flowchart of the wiring model, in string form + */ + @NonNull + public static String generateWiringDiagram( + @NonNull final Map vertices, + @NonNull final Set edges, + @NonNull final Set groups) { + + final StringBuilder sb = new StringBuilder(); + sb.append("flowchart LR\n"); + + final Map> groupMap = buildGroupMap(vertices, groups); + final List ungroupedVertices = getUngroupedVertices(vertices, groupMap); + final Map collapsedVertexMap = getCollapsedVertexMap(groups, vertices); + + groups.stream() + .sorted() + .forEachOrdered(group -> drawGroup(sb, group, groupMap.get(group.name()), collapsedVertexMap)); + ungroupedVertices.stream().sorted().forEachOrdered(vertex -> drawVertex(sb, vertex, collapsedVertexMap, 1)); + edges.stream().sorted().forEachOrdered(edge -> drawEdge(sb, edge, collapsedVertexMap)); + + return sb.toString(); + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/schedulers/AbstractTask.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/schedulers/AbstractTask.java new file mode 100644 index 000000000000..33d49fe4c373 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/schedulers/AbstractTask.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.schedulers; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinTask; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A unit of work that is processed by a task scheduler. + */ +public abstract class AbstractTask extends ForkJoinTask { + + /** + * Counts outstanding dependencies. When it reaches 0, the task is ready to run. + */ + private final AtomicInteger dependencyCount; + + /** + * The fork join pool that will execute this task. + */ + private final ForkJoinPool pool; + + /** + * Constructor. + * + * @param pool the fork join pool that will execute this task + * @param dependencyCount the number of dependencies that must be satisfied before this task is eligible for + * execution + */ + protected AbstractTask(@NonNull final ForkJoinPool pool, final int dependencyCount) { + this.pool = pool; + this.dependencyCount = dependencyCount > 0 ? new AtomicInteger(dependencyCount) : null; + } + + /** + * Unused. + */ + @Override + public final Void getRawResult() { + return null; + } + + /** + * Unused. + */ + @Override + protected final void setRawResult(Void value) {} + + /** + * If the task has no dependencies then execute it. If the task has dependencies, decrement the dependency count and + * execute it if the resulting number of dependencies is zero. + */ + protected void send() { + if (dependencyCount == null || dependencyCount.decrementAndGet() == 0) { + pool.execute(this); + } + } + + /** + * Execute the work represented by this task. + * + * @return true if this task is known to have been completed normally + */ + @Override + protected abstract boolean exec(); +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/schedulers/ConcurrentTask.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/schedulers/ConcurrentTask.java new file mode 100644 index 000000000000..cf256cd84726 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/schedulers/ConcurrentTask.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.schedulers; + +import com.swirlds.common.wiring.counters.ObjectCounter; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.concurrent.ForkJoinPool; +import java.util.function.Consumer; + +/** + * A task in a {@link ConcurrentTaskScheduler}. + */ +class ConcurrentTask extends AbstractTask { + + private final Consumer handler; + private final Object data; + private final ObjectCounter offRamp; + private final UncaughtExceptionHandler uncaughtExceptionHandler; + + /** + * Constructor. + * + * @param pool the fork join pool that will execute this task + * @param offRamp an object counter that is decremented when this task is executed + * @param uncaughtExceptionHandler the handler for uncaught exceptions + * @param handler the method that will be called when this task is executed + * @param data the data to be passed to the consumer for this task + */ + protected ConcurrentTask( + @NonNull final ForkJoinPool pool, + @NonNull final ObjectCounter offRamp, + @NonNull final UncaughtExceptionHandler uncaughtExceptionHandler, + @NonNull final Consumer handler, + @Nullable final Object data) { + super(pool, 0); + this.handler = handler; + this.data = data; + this.offRamp = offRamp; + this.uncaughtExceptionHandler = uncaughtExceptionHandler; + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean exec() { + try { + handler.accept(data); + } catch (final Throwable t) { + uncaughtExceptionHandler.uncaughtException(Thread.currentThread(), t); + } finally { + offRamp.offRamp(); + } + return true; + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/schedulers/ConcurrentTaskScheduler.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/schedulers/ConcurrentTaskScheduler.java new file mode 100644 index 000000000000..bc880b93ae83 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/schedulers/ConcurrentTaskScheduler.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.schedulers; + +import com.swirlds.common.wiring.TaskScheduler; +import com.swirlds.common.wiring.WiringModel; +import com.swirlds.common.wiring.counters.ObjectCounter; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.Objects; +import java.util.concurrent.ForkJoinPool; +import java.util.function.Consumer; + +/** + * A {@link TaskScheduler} that permits parallel execution of tasks. + * + * @param the output type of the scheduler (use {@link Void} for a task scheduler with no output type) + */ +public class ConcurrentTaskScheduler extends TaskScheduler { + + private final ObjectCounter onRamp; + private final ObjectCounter offRamp; + private final UncaughtExceptionHandler uncaughtExceptionHandler; + private final ForkJoinPool pool; + + /** + * Constructor. + * + * @param model the wiring model containing this scheduler + * @param name the name of the scheduler + * @param pool the fork join pool that will execute tasks on this scheduler + * @param uncaughtExceptionHandler the handler for uncaught exceptions + * @param onRamp an object counter that is incremented when data is added to the scheduler + * @param offRamp an object counter that is decremented when data is removed from the scheduler + * @param flushEnabled if true, then {@link #flush()} will be enabled, otherwise it will throw. + * @param insertionIsBlocking when data is inserted into this scheduler, will it block until capacity is available? + */ + public ConcurrentTaskScheduler( + @NonNull final WiringModel model, + @NonNull final String name, + @NonNull ForkJoinPool pool, + @NonNull UncaughtExceptionHandler uncaughtExceptionHandler, + @NonNull final ObjectCounter onRamp, + @NonNull final ObjectCounter offRamp, + final boolean flushEnabled, + final boolean insertionIsBlocking) { + + super(model, name, flushEnabled, insertionIsBlocking); + + this.pool = Objects.requireNonNull(pool); + this.uncaughtExceptionHandler = Objects.requireNonNull(uncaughtExceptionHandler); + this.onRamp = Objects.requireNonNull(onRamp); + this.offRamp = Objects.requireNonNull(offRamp); + } + + /** + * {@inheritDoc} + */ + @Override + protected void put(@NonNull final Consumer handler, @Nullable final Object data) { + onRamp.onRamp(); + new ConcurrentTask(pool, offRamp, uncaughtExceptionHandler, handler, data).send(); + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean offer(@NonNull final Consumer handler, @Nullable final Object data) { + boolean accepted = onRamp.attemptOnRamp(); + if (accepted) { + new ConcurrentTask(pool, offRamp, uncaughtExceptionHandler, handler, data).send(); + } + return accepted; + } + + /** + * {@inheritDoc} + */ + @Override + protected void inject(@NonNull final Consumer handler, @Nullable final Object data) { + onRamp.forceOnRamp(); + new ConcurrentTask(pool, offRamp, uncaughtExceptionHandler, handler, data).send(); + } + + /** + * {@inheritDoc} + */ + @Override + public long getUnprocessedTaskCount() { + return onRamp.getCount(); + } + + /** + * {@inheritDoc} + */ + @Override + public void flush() { + throwIfFlushDisabled(); + onRamp.waitUntilEmpty(); + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/schedulers/SequentialTask.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/schedulers/SequentialTask.java new file mode 100644 index 000000000000..fa142c4bbcbb --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/schedulers/SequentialTask.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.schedulers; + +import com.swirlds.common.metrics.extensions.FractionalTimer; +import com.swirlds.common.wiring.counters.ObjectCounter; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.concurrent.ForkJoinPool; +import java.util.function.Consumer; + +/** + * A task in a {@link SequentialTaskScheduler}. + */ +class SequentialTask extends AbstractTask { + private Consumer handler; + private Object data; + private SequentialTask nextTask; + private final ObjectCounter offRamp; + private final FractionalTimer busyTimer; + private final UncaughtExceptionHandler uncaughtExceptionHandler; + + /** + * Constructor. + * + * @param pool the fork join pool that will execute tasks on this task scheduler + * @param offRamp an object counter that is decremented when data is removed from the task + * scheduler + * @param busyTimer a timer that tracks the amount of time the task scheduler is busy + * @param uncaughtExceptionHandler the uncaught exception handler + * @param firstTask true if this is the first task in the scheduler, false otherwise + */ + SequentialTask( + @NonNull final ForkJoinPool pool, + @NonNull final ObjectCounter offRamp, + @NonNull final FractionalTimer busyTimer, + @NonNull final UncaughtExceptionHandler uncaughtExceptionHandler, + final boolean firstTask) { + super(pool, firstTask ? 1 : 2); + this.offRamp = offRamp; + this.busyTimer = busyTimer; + this.uncaughtExceptionHandler = uncaughtExceptionHandler; + } + + /** + * Provide a reference to the next task and the data that will be processed during the handling of this task. + * + * @param nextTask the task that will execute after this task + * @param handler the method that will be called when this task is executed + * @param data the data to be passed to the consumer for this task + */ + void send( + @NonNull final SequentialTask nextTask, + @NonNull final Consumer handler, + @Nullable final Object data) { + this.nextTask = nextTask; + this.handler = handler; + this.data = data; + + // This method provides us with the data we intend to send to the consumer + // when this task is executed, thus resolving one of the two dependencies + // required for the task to be executed. This call will decrement the + // dependency count. If this causes the dependency count to reach 0 + // (i.e. if the previous task has already been executed), then this call + // will cause this task to be immediately eligible for execution. + send(); + } + + /** + * Execute this task. + */ + @Override + public boolean exec() { + busyTimer.activate(); + try { + handler.accept(data); + } catch (final Throwable t) { + uncaughtExceptionHandler.uncaughtException(Thread.currentThread(), t); + } finally { + offRamp.offRamp(); + busyTimer.deactivate(); + + // Reduce the dependency count of the next task. If the next task already has its data, then this + // method will cause the next task to be immediately eligible for execution. + nextTask.send(); + } + return true; + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/schedulers/SequentialTaskScheduler.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/schedulers/SequentialTaskScheduler.java new file mode 100644 index 000000000000..5785d4b3c694 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/schedulers/SequentialTaskScheduler.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.schedulers; + +import com.swirlds.common.metrics.extensions.FractionalTimer; +import com.swirlds.common.wiring.TaskScheduler; +import com.swirlds.common.wiring.WiringModel; +import com.swirlds.common.wiring.counters.ObjectCounter; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.Objects; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +/** + * A {@link TaskScheduler} that guarantees that tasks are executed sequentially in the order they are received. + * + * @param the output type of the scheduler (use {@link Void} for a task scheduler with no output type) + */ +public class SequentialTaskScheduler extends TaskScheduler { + + /** + * The next task to be scheduled will be inserted into this placeholder task. When that happens, a new task will be + * created and inserted into this placeholder. + */ + private final AtomicReference nextTaskPlaceholder; + + private final ObjectCounter onRamp; + private final ObjectCounter offRamp; + private final FractionalTimer busyTimer; + private final UncaughtExceptionHandler uncaughtExceptionHandler; + private final ForkJoinPool pool; + + /** + * Constructor. + * + * @param model the wiring model containing this scheduler + * @param name the name of the task scheduler + * @param pool the fork join pool that will execute tasks on this scheduler + * @param uncaughtExceptionHandler the uncaught exception handler + * @param onRamp an object counter that is incremented when data is added to the task scheduler + * @param offRamp an object counter that is decremented when data is removed from the task + * scheduler + * @param busyTimer a timer that tracks the amount of time the scheduler is busy + * @param flushEnabled if true, then {@link #flush()} will be enabled, otherwise it will throw. + * @param insertionIsBlocking when data is inserted into this task scheduler, will it block until capacity is + * available? + */ + public SequentialTaskScheduler( + @NonNull final WiringModel model, + @NonNull final String name, + @NonNull ForkJoinPool pool, + @NonNull final UncaughtExceptionHandler uncaughtExceptionHandler, + @NonNull final ObjectCounter onRamp, + @NonNull final ObjectCounter offRamp, + @NonNull final FractionalTimer busyTimer, + final boolean flushEnabled, + final boolean insertionIsBlocking) { + + super(model, name, flushEnabled, insertionIsBlocking); + + this.pool = Objects.requireNonNull(pool); + this.uncaughtExceptionHandler = Objects.requireNonNull(uncaughtExceptionHandler); + this.onRamp = Objects.requireNonNull(onRamp); + this.offRamp = Objects.requireNonNull(offRamp); + this.busyTimer = Objects.requireNonNull(busyTimer); + + this.nextTaskPlaceholder = + new AtomicReference<>(new SequentialTask(pool, offRamp, busyTimer, uncaughtExceptionHandler, true)); + } + + /** + * {@inheritDoc} + */ + @Override + protected void put(@NonNull final Consumer handler, @Nullable final Object data) { + onRamp.onRamp(); + scheduleTask(handler, data); + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean offer(@NonNull final Consumer handler, @Nullable final Object data) { + final boolean accepted = onRamp.attemptOnRamp(); + if (accepted) { + scheduleTask(handler, data); + } + return accepted; + } + + /** + * {@inheritDoc} + */ + @Override + protected void inject(@NonNull final Consumer handler, @Nullable final Object data) { + onRamp.forceOnRamp(); + scheduleTask(handler, data); + } + + /** + * Schedule a task to be handled. This should only be called after successfully on-ramping (one way or another). + * + * @param handler the method that will be called when this task is executed + * @param data the data to be passed to the consumer for this task + */ + private void scheduleTask(@NonNull final Consumer handler, @Nullable final Object data) { + // This method may be called by many threads, but actual execution is required to happen serially. This method + // organizes tasks into a linked list. Tasks in this linked list are executed one at a time in order. + // When execution of one task is completed, execution of the next task is scheduled on the pool. + + final SequentialTask nextTask = new SequentialTask(pool, offRamp, busyTimer, uncaughtExceptionHandler, false); + SequentialTask currentTask; + do { + currentTask = nextTaskPlaceholder.get(); + } while (!nextTaskPlaceholder.compareAndSet(currentTask, nextTask)); + currentTask.send(nextTask, handler, data); + } + + /** + * {@inheritDoc} + */ + @Override + public long getUnprocessedTaskCount() { + return onRamp.getCount(); + } + + /** + * {@inheritDoc} + */ + @Override + public void flush() { + throwIfFlushDisabled(); + flushWithSemaphore().acquireUninterruptibly(); + } + + /** + * Start a flush operation with a semaphore. The flush is completed when the semaphore can be acquired. + * + * @return the semaphore that will be released when the flush is complete + */ + @NonNull + private Semaphore flushWithSemaphore() { + onRamp.forceOnRamp(); + final Semaphore semaphore = new Semaphore(1); + semaphore.acquireUninterruptibly(); + + final SequentialTask nextTask = new SequentialTask(pool, offRamp, busyTimer, uncaughtExceptionHandler, false); + SequentialTask currentTask; + do { + currentTask = nextTaskPlaceholder.get(); + } while (!nextTaskPlaceholder.compareAndSet(currentTask, nextTask)); + currentTask.send(nextTask, x -> semaphore.release(), null); + + return semaphore; + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/transformers/WireFilter.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/transformers/WireFilter.java new file mode 100644 index 000000000000..fa2a065209ba --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/transformers/WireFilter.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.transformers; + +import com.swirlds.common.wiring.OutputWire; +import com.swirlds.common.wiring.WiringModel; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Filters out data, allowing some objects to pass and blocking others. + */ +public class WireFilter implements Consumer { + + private final Predicate predicate; + private final OutputWire outputWire; + + /** + * Constructor. + * + * @param model the wiring model containing this output channel + * @param name the name of the output wire + * @param predicate only data that causes this method to return true is forwarded. This method must be very fast. + * Putting large amounts of work into this transformer violates the intended usage pattern of the + * wiring framework and may result in very poor system performance. + */ + public WireFilter( + @NonNull final WiringModel model, @NonNull final String name, @NonNull final Predicate predicate) { + this.predicate = Objects.requireNonNull(predicate); + this.outputWire = new OutputWire<>(model, name); + model.registerVertex(name, true); + } + + /** + * {@inheritDoc} + */ + @Override + public void accept(@NonNull final T t) { + if (predicate.test(t)) { + outputWire.forward(t); + } + } + + /** + * Get the output wire for this transformer. + * + * @return the output wire + */ + @NonNull + public OutputWire getOutputWire() { + return outputWire; + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/transformers/WireListSplitter.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/transformers/WireListSplitter.java new file mode 100644 index 000000000000..7bfddba21a1c --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/transformers/WireListSplitter.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.transformers; + +import com.swirlds.common.wiring.OutputWire; +import com.swirlds.common.wiring.WiringModel; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; +import java.util.function.Consumer; + +/** + * Transforms a list of items to a sequence of individual items. Expects that there will not be any null values in the + * collection. + */ +public class WireListSplitter implements Consumer> { + + private final OutputWire outputWire; + + /** + * Constructor. + * + * @param model the wiring model containing this output wire + * @param name the name of the output channel + */ + public WireListSplitter(@NonNull final WiringModel model, @NonNull final String name) { + model.registerVertex(name, true); + outputWire = new OutputWire<>(model, name); + } + + /** + * {@inheritDoc} + */ + @Override + public void accept(@NonNull final List list) { + for (final T t : list) { + outputWire.forward(t); + } + } + + /** + * Get the output wire for this transformer. + * + * @return the output wire + */ + @NonNull + public OutputWire getOutputWire() { + return outputWire; + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/transformers/WireTransformer.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/transformers/WireTransformer.java new file mode 100644 index 000000000000..7946fe05ddad --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/transformers/WireTransformer.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.transformers; + +import com.swirlds.common.wiring.OutputWire; +import com.swirlds.common.wiring.WiringModel; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Transforms data on a wire from one type to another. + * + * @param the input type + * @param the output type + */ +public class WireTransformer implements Consumer { + + private final Function transformer; + private final OutputWire outputWire; + + /** + * Constructor. + * + * @param model the wiring model containing this output channel + * @param name the name of the output wire + * @param transformer an object that transforms from type A to type B. If this method returns null then no data is + * forwarded. This method must be very fast. Putting large amounts of work into this transformer + * violates the intended usage pattern of the wiring framework and may result in very poor system + * performance. + */ + public WireTransformer( + @NonNull final WiringModel model, @NonNull final String name, @NonNull final Function transformer) { + model.registerVertex(name, true); + this.transformer = Objects.requireNonNull(transformer); + outputWire = new OutputWire<>(model, name); + } + + /** + * {@inheritDoc} + */ + @Override + public void accept(@NonNull final A a) { + final B b = transformer.apply(a); + if (b != null) { + outputWire.forward(b); + } + } + + /** + * Get the output wire for this transformer. + * + * @return the output wire + */ + @NonNull + public OutputWire getOutputWire() { + return outputWire; + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/module-info.java b/platform-sdk/swirlds-common/src/main/java/module-info.java index 0ced6e45eace..97da6038f703 100644 --- a/platform-sdk/swirlds-common/src/main/java/module-info.java +++ b/platform-sdk/swirlds-common/src/main/java/module-info.java @@ -78,6 +78,8 @@ exports com.swirlds.common.utility.throttle; exports com.swirlds.common.jackson; exports com.swirlds.common.units; + exports com.swirlds.common.wiring; + exports com.swirlds.common.wiring.counters; /* Targeted exports */ exports com.swirlds.common.internal to diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/benchmark/WiringBenchmark.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/benchmark/WiringBenchmark.java new file mode 100644 index 000000000000..79256dc8a71f --- /dev/null +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/benchmark/WiringBenchmark.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.benchmark; + +import static java.util.concurrent.ForkJoinPool.defaultForkJoinWorkerThreadFactory; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.swirlds.base.time.Time; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.wiring.InputWire; +import com.swirlds.common.wiring.TaskScheduler; +import com.swirlds.common.wiring.WiringModel; +import com.swirlds.common.wiring.counters.BackpressureObjectCounter; +import com.swirlds.common.wiring.counters.ObjectCounter; +import com.swirlds.test.framework.context.TestPlatformContextBuilder; +import java.time.Duration; +import java.util.concurrent.ForkJoinPool; + +class WiringBenchmark { + + /* Data flow for this benchmark: + + gossip -> event verifier -> orphan buffer + ^ | + | | + --------------------------------- + + */ + + static void basicBenchmark() throws InterruptedException { + + // We will use this executor for starting all threads. Maybe we should only use it for temporary threads? + final ForkJoinPool executor = new ForkJoinPool( + Runtime.getRuntime().availableProcessors(), + defaultForkJoinWorkerThreadFactory, + (t, e) -> { + System.out.println("Uncaught exception in thread " + t.getName()); + e.printStackTrace(); + }, + true); + + final PlatformContext platformContext = + TestPlatformContextBuilder.create().build(); + final WiringModel model = WiringModel.create(platformContext, Time.getCurrent()); + + // Ensures that we have no more than 10,000 events in the pipeline at any given time + final ObjectCounter backpressure = new BackpressureObjectCounter("backpressure", 10_000, Duration.ZERO); + + final TaskScheduler verificationTaskScheduler = model.schedulerBuilder("verification") + .withPool(executor) + .withConcurrency(true) + .withOnRamp(backpressure) + .withExternalBackPressure(true) + .build() + .cast(); + + final TaskScheduler orphanBufferTaskScheduler = model.schedulerBuilder("orphanBuffer") + .withPool(executor) + .withConcurrency(false) + .withExternalBackPressure(true) + .build() + .cast(); + + final TaskScheduler eventPoolTaskScheduler = model.schedulerBuilder("eventPool") + .withPool(executor) + .withConcurrency(false) + .withOffRamp(backpressure) + .withExternalBackPressure(true) + .build() + .cast(); + + final InputWire eventsToOrphanBuffer = + orphanBufferTaskScheduler.buildInputWire("unordered events"); + + final InputWire eventsToBeVerified = + verificationTaskScheduler.buildInputWire("unverified events"); + + final InputWire eventsToInsertBackIntoEventPool = + eventPoolTaskScheduler.buildInputWire("verified events"); + + verificationTaskScheduler.getOutputWire().solderTo(eventsToOrphanBuffer); + orphanBufferTaskScheduler.getOutputWire().solderTo(eventsToInsertBackIntoEventPool); + + final WiringBenchmarkEventPool eventPool = new WiringBenchmarkEventPool(); + final WiringBenchmarkTopologicalEventSorter orphanBuffer = new WiringBenchmarkTopologicalEventSorter(); + final WiringBenchmarkEventVerifier verifier = new WiringBenchmarkEventVerifier(); + final WiringBenchmarkGossip gossip = new WiringBenchmarkGossip(executor, eventPool, eventsToBeVerified::put); + + eventsToOrphanBuffer.bind(orphanBuffer); + eventsToBeVerified.bind(verifier); + eventsToInsertBackIntoEventPool.bind(eventPool::checkin); + + // Create a user thread for running "gossip". It will continue to generate events until explicitly stopped. + System.out.println("Starting gossip"); + gossip.start(); + SECONDS.sleep(120); + gossip.stop(); + + // Validate that all events have been seen by orphanBuffer + final long timeout = System.currentTimeMillis() + 1000; + boolean success = false; + while (System.currentTimeMillis() < timeout) { + if (orphanBuffer.getCheckSum() == gossip.getCheckSum()) { + success = true; + break; + } + } + assertTrue(success); + } + + public static void main(String[] args) { + try { + basicBenchmark(); + } catch (final Throwable t) { + t.printStackTrace(); + } + } +} diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/benchmark/WiringBenchmarkEvent.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/benchmark/WiringBenchmarkEvent.java new file mode 100644 index 000000000000..1f874eb82a25 --- /dev/null +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/benchmark/WiringBenchmarkEvent.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.benchmark; + +public final class WiringBenchmarkEvent { + private long number = -1; // We'll let the orphan buffer assign this, although I think consensus actually does + private final byte[] data = new byte[1024 * 32]; // Just gotta have some bytes. Whatever. + + public WiringBenchmarkEvent() {} + + void reset(long number) { + this.number = number; + } + + @Override + public String toString() { + return "Event {number=" + number + "}"; + } + + public long number() { + return number; + } +} diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/benchmark/WiringBenchmarkEventPool.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/benchmark/WiringBenchmarkEventPool.java new file mode 100644 index 000000000000..5805a5db628b --- /dev/null +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/benchmark/WiringBenchmarkEventPool.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.benchmark; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +public final class WiringBenchmarkEventPool { + private final BlockingQueue pool = new LinkedBlockingQueue<>(); + + public WiringBenchmarkEventPool() {} + + @NonNull + public WiringBenchmarkEvent checkout(long number) { + WiringBenchmarkEvent event = pool.poll(); + if (event == null) { + event = new WiringBenchmarkEvent(); + } + event.reset(number); + return event; + } + + public void checkin(WiringBenchmarkEvent event) { + pool.add(event); + } +} diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/benchmark/WiringBenchmarkEventVerifier.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/benchmark/WiringBenchmarkEventVerifier.java new file mode 100644 index 000000000000..088d5b3c5cf7 --- /dev/null +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/benchmark/WiringBenchmarkEventVerifier.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.benchmark; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.function.Function; + +public class WiringBenchmarkEventVerifier implements Function { + + public WiringBenchmarkEventVerifier() {} + + @Override + @NonNull + public WiringBenchmarkEvent apply(@NonNull final WiringBenchmarkEvent event) { + // Pretend like we did verification by sleeping for a few microseconds + busySleep(2000); + return event; + } + + public static void busySleep(long nanos) { + long elapsed; + final long startTime = System.nanoTime(); + do { + elapsed = System.nanoTime() - startTime; + } while (elapsed < nanos); + } +} diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/benchmark/WiringBenchmarkGossip.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/benchmark/WiringBenchmarkGossip.java new file mode 100644 index 000000000000..c96000b4124e --- /dev/null +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/benchmark/WiringBenchmarkGossip.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.benchmark; + +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +/** + * A quick and dirty simulation of gossip :-). It will generate events like crazy. + */ +public class WiringBenchmarkGossip { + private final Executor executor; + private final WiringBenchmarkEventPool eventPool; + private final Consumer toEventVerifier; + private final AtomicLong eventNumber = new AtomicLong(); + private volatile boolean stopped = false; + private volatile long checkSum; + + public WiringBenchmarkGossip( + Executor executor, WiringBenchmarkEventPool eventPool, Consumer toEventVerifier) { + this.executor = executor; + this.toEventVerifier = toEventVerifier; + this.eventPool = eventPool; + } + + public void start() { + eventNumber.set(0); + checkSum = 0; + executor.execute(this::generateEvents); + } + + private void generateEvents() { + while (!stopped) { + final var event = eventPool.checkout(eventNumber.getAndIncrement()); + toEventVerifier.accept(event); + } + long lastNumber = eventNumber.get(); + checkSum = lastNumber * (lastNumber + 1) / 2; + } + + public void stop() { + stopped = true; + } + + public long getCheckSum() { + return checkSum; + } +} diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/benchmark/WiringBenchmarkTopologicalEventSorter.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/benchmark/WiringBenchmarkTopologicalEventSorter.java new file mode 100644 index 000000000000..79b026269f7e --- /dev/null +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/benchmark/WiringBenchmarkTopologicalEventSorter.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.benchmark; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.function.Function; + +public class WiringBenchmarkTopologicalEventSorter implements Function { + private static final int PRINT_FREQUENCY = 10_000_000; + private long lastTimestamp; + private long checkSum; + + public WiringBenchmarkTopologicalEventSorter() { + this.checkSum = 0; + } + + @NonNull + @Override + public WiringBenchmarkEvent apply(@NonNull final WiringBenchmarkEvent event) { + final long number = event.number(); + checkSum += number + 1; // make 0 contribute to the sum + if (number % PRINT_FREQUENCY == 0) { + long curTimestamp = System.currentTimeMillis(); + if (number != 0) { + System.out.format( + "Handled %d events, TPS: %d%n", + number, PRINT_FREQUENCY * 1000L / (curTimestamp - lastTimestamp)); + } + lastTimestamp = curTimestamp; + } + return event; + } + + public long getCheckSum() { + return checkSum; + } +} diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/counters/BackpressureObjectCounterTests.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/counters/BackpressureObjectCounterTests.java new file mode 100644 index 000000000000..3bcb18433f0a --- /dev/null +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/counters/BackpressureObjectCounterTests.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.counters; + +import static com.swirlds.common.test.fixtures.AssertionUtils.assertEventuallyEquals; +import static com.swirlds.common.test.fixtures.AssertionUtils.assertEventuallyTrue; +import static com.swirlds.common.test.fixtures.RandomUtils.getRandomPrintSeed; +import static com.swirlds.common.threading.manager.AdHocThreadManager.getStaticThreadManager; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.swirlds.common.threading.framework.config.ThreadConfiguration; +import java.time.Duration; +import java.util.Random; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class BackpressureObjectCounterTests { + + /** + * Choose a capacity that is sufficiently high as to never trigger. Validate that the counting part of this + * implementation works as expected. + */ + @Test + void countWithHighCapacityTest() { + final Random random = getRandomPrintSeed(); + + final ObjectCounter counter = new BackpressureObjectCounter("test", 1_000_000_000, Duration.ofMillis(1)); + + int count = 0; + for (int i = 0; i < 1000; i++) { + + final boolean increment = count == 0 || random.nextBoolean(); + + if (increment) { + count++; + + // All of these methods are logically equivalent with current capacity. + final int choice = random.nextInt(3); + switch (choice) { + case 0 -> counter.onRamp(); + case 1 -> counter.attemptOnRamp(); + case 2 -> counter.forceOnRamp(); + default -> throw new IllegalStateException("Unexpected value: " + choice); + } + + } else { + count--; + counter.offRamp(); + } + + assertEquals(count, counter.getCount()); + } + } + + @ParameterizedTest + @ValueSource(ints = {0, 1}) + void onRampTest(final int sleepMillis) throws InterruptedException { + final Duration sleepDuration = Duration.ofMillis(sleepMillis); + + final ObjectCounter counter = new BackpressureObjectCounter("test", 10, sleepDuration); + + // Fill up the counter to capacity + for (int i = 0; i < 10; i++) { + counter.onRamp(); + } + + assertEquals(10, counter.getCount()); + + // Attempt to add one more, should block. + final AtomicBoolean added = new AtomicBoolean(false); + final AtomicReference interrupted = new AtomicReference<>(); + final Thread thread = new ThreadConfiguration(getStaticThreadManager()) + .setRunnable(() -> { + counter.onRamp(); + added.set(true); + + interrupted.set(Thread.currentThread().isInterrupted()); + }) + .build(true); + + assertEquals(10, counter.getCount()); + + // Sleep for a little while. Thread should be unable to on ramp another element. + MILLISECONDS.sleep(50); + assertEquals(10, counter.getCount()); + + // Interrupting the thread should not unblock us. + thread.interrupt(); + MILLISECONDS.sleep(50); + assertEquals(10, counter.getCount()); + + // Off ramp one element. Thread should become unblocked. + counter.offRamp(); + + assertEventuallyTrue(added::get, Duration.ofSeconds(1), "Thread should have been unblocked"); + + // even though the interrupt did not unblock the thread, the interrupt should not have been squelched. + assertEventuallyEquals(true, interrupted::get, Duration.ofSeconds(1), "Thread should have been interrupted"); + + assertEquals(10, counter.getCount()); + } + + @Test + void attemptOnRampTest() { + final ObjectCounter counter = new BackpressureObjectCounter("test", 10, Duration.ofMillis(1)); + + // Fill up the counter to capacity + for (int i = 0; i < 10; i++) { + assertTrue(counter.attemptOnRamp()); + } + + assertEquals(10, counter.getCount()); + + // Attempt to add one more, should block immediately fail and return false. + assertFalse(counter.attemptOnRamp()); + + assertEquals(10, counter.getCount()); + } + + @Test + void forceOnRampTest() { + final ObjectCounter counter = new BackpressureObjectCounter("test", 10, Duration.ofMillis(1)); + + // Fill up the counter to capacity + for (int i = 0; i < 10; i++) { + counter.forceOnRamp(); + } + + assertEquals(10, counter.getCount()); + + // Attempt to add one more, should work even though it violates capacity restrictions + counter.forceOnRamp(); + + assertEquals(11, counter.getCount()); + } + + @Test + void waitUntilEmptyTest() throws InterruptedException { + final ObjectCounter counter = new BackpressureObjectCounter("test", 1000, Duration.ofMillis(1)); + + for (int i = 0; i < 100; i++) { + counter.onRamp(); + } + + final AtomicBoolean empty = new AtomicBoolean(false); + final Thread thread = new ThreadConfiguration(getStaticThreadManager()) + .setRunnable(() -> { + counter.waitUntilEmpty(); + empty.set(true); + }) + .build(true); + + // Should be blocked. + MILLISECONDS.sleep(50); + assertFalse(empty.get()); + + // Draining most of the things from the counter should still block. + for (int i = 0; i < 90; i++) { + counter.offRamp(); + } + MILLISECONDS.sleep(50); + assertFalse(empty.get()); + + // Interrupting the thread should have no effect. + thread.interrupt(); + MILLISECONDS.sleep(50); + assertFalse(empty.get()); + + // Removing remaining things from the counter should unblock. + for (int i = 0; i < 10; i++) { + counter.offRamp(); + } + + assertEventuallyTrue(empty::get, Duration.ofSeconds(1), "Counter did not empty in time."); + } +} diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/counters/MultiObjectCounterTests.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/counters/MultiObjectCounterTests.java new file mode 100644 index 000000000000..a2e088e30058 --- /dev/null +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/counters/MultiObjectCounterTests.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.counters; + +import static com.swirlds.common.test.fixtures.AssertionUtils.assertEventuallyTrue; +import static com.swirlds.common.test.fixtures.RandomUtils.getRandomPrintSeed; +import static com.swirlds.common.threading.manager.AdHocThreadManager.getStaticThreadManager; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import com.swirlds.common.threading.framework.config.ThreadConfiguration; +import java.time.Duration; +import java.util.Random; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; + +class MultiObjectCounterTests { + + @Test + void onRampOffRampTest() { + final Random random = getRandomPrintSeed(); + + final ObjectCounter counterA = new StandardObjectCounter(Duration.ofSeconds(1)); + final ObjectCounter counterB = new StandardObjectCounter(Duration.ofSeconds(1)); + final ObjectCounter counterC = new StandardObjectCounter(Duration.ofSeconds(1)); + + final MultiObjectCounter counter = new MultiObjectCounter(counterA, counterB, counterC); + + int expectedCount = 0; + for (int i = 0; i < 1000; i++) { + + if (expectedCount == 0 || random.nextDouble() < 0.75) { + counter.onRamp(); + expectedCount++; + } else { + counter.offRamp(); + expectedCount--; + } + + assertEquals(expectedCount, counter.getCount()); + assertEquals(expectedCount, counterA.getCount()); + assertEquals(expectedCount, counterB.getCount()); + assertEquals(expectedCount, counterC.getCount()); + } + } + + @Test + void attemptOnRampTest() { + final Random random = getRandomPrintSeed(); + + // When attempting an on ramp, only the first counter's capacity should be consulted. + + final ObjectCounter counterA = new BackpressureObjectCounter("test", 10, Duration.ofSeconds(1)); + final ObjectCounter counterB = new BackpressureObjectCounter("test", 5, Duration.ofSeconds(1)); + final ObjectCounter counterC = new StandardObjectCounter(Duration.ofSeconds(1)); + + final MultiObjectCounter counter = new MultiObjectCounter(counterA, counterB, counterC); + + int expectedCount = 0; + for (int i = 0; i < 1000; i++) { + if (expectedCount == 0 || random.nextDouble() < 0.75) { + if (counter.attemptOnRamp()) { + expectedCount++; + } + } else { + counter.offRamp(); + expectedCount--; + } + + assertEquals(expectedCount, counter.getCount()); + assertEquals(expectedCount, counterA.getCount()); + assertEquals(expectedCount, counterB.getCount()); + assertEquals(expectedCount, counterC.getCount()); + } + } + + @Test + void forceOnRampTest() { + final Random random = getRandomPrintSeed(); + + // When attempting an on ramp, only the first counter's capacity should be consulted. + + final ObjectCounter counterA = new BackpressureObjectCounter("test", 10, Duration.ofSeconds(1)); + final ObjectCounter counterB = new BackpressureObjectCounter("test", 5, Duration.ofSeconds(1)); + final ObjectCounter counterC = new StandardObjectCounter(Duration.ofSeconds(1)); + + final MultiObjectCounter counter = new MultiObjectCounter(counterA, counterB, counterC); + + int expectedCount = 0; + for (int i = 0; i < 1000; i++) { + if (expectedCount == 0 || random.nextDouble() < 0.75) { + counter.forceOnRamp(); + expectedCount++; + } else { + counter.offRamp(); + expectedCount--; + } + + assertEquals(expectedCount, counter.getCount()); + assertEquals(expectedCount, counterA.getCount()); + assertEquals(expectedCount, counterB.getCount()); + assertEquals(expectedCount, counterC.getCount()); + } + } + + @Test + void waitUntilEmptyTest() throws InterruptedException { + final ObjectCounter counterA = new BackpressureObjectCounter("test", 10, Duration.ofSeconds(1)); + final ObjectCounter counterB = new BackpressureObjectCounter("test", 5, Duration.ofSeconds(1)); + final ObjectCounter counterC = new StandardObjectCounter(Duration.ofSeconds(1)); + + final MultiObjectCounter counter = new MultiObjectCounter(counterA, counterB, counterC); + + for (int i = 0; i < 100; i++) { + counter.forceOnRamp(); + } + + counterB.forceOnRamp(); + + counterC.forceOnRamp(); + counterC.forceOnRamp(); + + final AtomicBoolean empty = new AtomicBoolean(false); + final Thread thread = new ThreadConfiguration(getStaticThreadManager()) + .setRunnable(() -> { + counter.waitUntilEmpty(); + empty.set(true); + }) + .build(true); + + // Should be blocked. + MILLISECONDS.sleep(50); + assertFalse(empty.get()); + + // Draining most of the things from the counter should still block. + for (int i = 0; i < 90; i++) { + counter.offRamp(); + } + MILLISECONDS.sleep(50); + assertFalse(empty.get()); + + // Interrupting the thread should have no effect. + thread.interrupt(); + MILLISECONDS.sleep(50); + assertFalse(empty.get()); + + // Remove enough things so that counterA is unblocked. + for (int i = 0; i < 10; i++) { + counter.offRamp(); + } + + MILLISECONDS.sleep(50); + assertFalse(empty.get()); + + // Reduce counter B to zero. + counterB.offRamp(); + + MILLISECONDS.sleep(50); + assertFalse(empty.get()); + + // Finally, remove all elements from counter C. + counterC.offRamp(); + counterC.offRamp(); + + assertEventuallyTrue(empty::get, Duration.ofSeconds(1), "Counter did not empty in time."); + } +} diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/counters/NoOpObjectCounterTests.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/counters/NoOpObjectCounterTests.java new file mode 100644 index 000000000000..565d0cf29d22 --- /dev/null +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/counters/NoOpObjectCounterTests.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.counters; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class NoOpObjectCounterTests { + + /** + * The most important part of the no-op implementation is that it doesn't throw exceptions. + */ + @Test + void noThrowingTest() { + final NoOpObjectCounter counter = NoOpObjectCounter.getInstance(); + + counter.onRamp(); + counter.attemptOnRamp(); + counter.forceOnRamp(); + counter.offRamp(); + counter.waitUntilEmpty(); + assertEquals(-1, counter.getCount()); + } +} diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/counters/StandardObjectCounterTests.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/counters/StandardObjectCounterTests.java new file mode 100644 index 000000000000..da640570e8e6 --- /dev/null +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/counters/StandardObjectCounterTests.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.counters; + +import static com.swirlds.common.test.fixtures.AssertionUtils.assertEventuallyTrue; +import static com.swirlds.common.test.fixtures.RandomUtils.getRandomPrintSeed; +import static com.swirlds.common.threading.manager.AdHocThreadManager.getStaticThreadManager; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import com.swirlds.common.threading.framework.config.ThreadConfiguration; +import java.time.Duration; +import java.util.Random; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; + +class StandardObjectCounterTests { + + @Test + void basicOperationTest() { + final Random random = getRandomPrintSeed(); + + final ObjectCounter counter = new StandardObjectCounter(Duration.ofMillis(1)); + + int count = 0; + for (int i = 0; i < 1000; i++) { + + final boolean increment = count == 0 || random.nextBoolean(); + + if (increment) { + count++; + + // All of these methods are logically equivalent for this implementation. + final int choice = random.nextInt(3); + switch (choice) { + case 0 -> counter.onRamp(); + case 1 -> counter.attemptOnRamp(); + case 2 -> counter.forceOnRamp(); + default -> throw new IllegalStateException("Unexpected value: " + choice); + } + + } else { + count--; + counter.offRamp(); + } + + assertEquals(count, counter.getCount()); + } + } + + @Test + void waitUntilEmptyTest() throws InterruptedException { + final ObjectCounter counter = new StandardObjectCounter(Duration.ofMillis(1)); + + for (int i = 0; i < 100; i++) { + counter.onRamp(); + } + + final AtomicBoolean empty = new AtomicBoolean(false); + final Thread thread = new ThreadConfiguration(getStaticThreadManager()) + .setRunnable(() -> { + counter.waitUntilEmpty(); + empty.set(true); + }) + .build(true); + + // Should be blocked. + MILLISECONDS.sleep(50); + assertFalse(empty.get()); + + // Draining most of the things from the counter should still block. + for (int i = 0; i < 90; i++) { + counter.offRamp(); + } + MILLISECONDS.sleep(50); + assertFalse(empty.get()); + + // Interrupting the thread should have no effect. + thread.interrupt(); + MILLISECONDS.sleep(50); + assertFalse(empty.get()); + + // Removing remaining things from the counter should unblock. + for (int i = 0; i < 10; i++) { + counter.offRamp(); + } + + assertEventuallyTrue(empty::get, Duration.ofSeconds(1), "Counter did not empty in time."); + } +} diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/model/ModelTests.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/model/ModelTests.java new file mode 100644 index 000000000000..42d16d6da350 --- /dev/null +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/model/ModelTests.java @@ -0,0 +1,1258 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.swirlds.base.time.Time; +import com.swirlds.common.wiring.InputWire; +import com.swirlds.common.wiring.ModelGroup; +import com.swirlds.common.wiring.OutputWire; +import com.swirlds.common.wiring.TaskScheduler; +import com.swirlds.common.wiring.WiringModel; +import com.swirlds.test.framework.context.TestPlatformContextBuilder; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class ModelTests { + + /** + * For debugging with a human in the loop. + */ + private static final boolean printMermaidDiagram = false; + + /** + * Validate the model. + * + * @param model the model to validate + * @param cycleExpected true if a cycle is expected, false otherwise + */ + private static void validateModel(@NonNull final WiringModel model, boolean cycleExpected) { + final boolean cycleDetected = model.checkForCyclicalBackpressure(); + assertEquals(cycleExpected, cycleDetected); + + final Set groups = new HashSet<>(); + + // Should not throw. + final String diagram = model.generateWiringDiagram(groups); + if (printMermaidDiagram) { + System.out.println(diagram); + } + } + + @Test + void emptyModelTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + validateModel(model, false); + } + + @Test + void singleVertexTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").build().cast(); + + validateModel(model, false); + } + + @Test + void shortChainTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A -> B -> C + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").withUnhandledTaskCapacity(1).build().cast(); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("C").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputC = taskSchedulerC.buildInputWire("inputC"); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputC); + + validateModel(model, false); + } + + @Test + void loopSizeOneTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A --| + ^ | + |---| + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + + taskSchedulerA.getOutputWire().solderTo(inputA); + + validateModel(model, true); + } + + @Test + void loopSizeOneBrokenByInjectionTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A --| + ^ | + |---| + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + + taskSchedulerA.getOutputWire().solderTo(inputA, true); + + validateModel(model, false); + } + + @Test + void loopSizeTwoTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A -> B + ^ | + |----| + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputA); + + validateModel(model, true); + } + + @Test + void loopSizeTwoBrokenByInjectionTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A -> B + ^ | + |----| + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputA, true); + + validateModel(model, false); + } + + @Test + void loopSizeTwoBrokenByMissingBoundTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A -> B + ^ | + |----| + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").build().cast(); + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputA); + + validateModel(model, false); + } + + @Test + void loopSizeThreeTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A -> B -> C + ^ | + |---------| + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("C").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputC = taskSchedulerC.buildInputWire("inputC"); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputC); + taskSchedulerC.getOutputWire().solderTo(inputA); + + validateModel(model, true); + } + + @Test + void loopSizeThreeBrokenByInjectionTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A -> B -> C + ^ | + |---------| + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("C").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputC = taskSchedulerC.buildInputWire("inputC"); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputC); + taskSchedulerC.getOutputWire().solderTo(inputA, true); + + validateModel(model, false); + } + + @Test + void loopSizeThreeBrokenByMissingBoundTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A -> B -> C + ^ | + |---------| + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("C").build().cast(); + final InputWire inputC = taskSchedulerC.buildInputWire("inputC"); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputC); + taskSchedulerC.getOutputWire().solderTo(inputA); + + validateModel(model, false); + } + + @Test + void loopSizeFourTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A -----> B + ^ | + | v + D <----- C + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("C").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputC = taskSchedulerC.buildInputWire("inputC"); + + final TaskScheduler taskSchedulerD = + model.schedulerBuilder("D").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputD = taskSchedulerD.buildInputWire("inputD"); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputC); + taskSchedulerC.getOutputWire().solderTo(inputD); + taskSchedulerD.getOutputWire().solderTo(inputA); + + validateModel(model, true); + } + + @Test + void loopSizeFourBrokenByInjectionTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A -----> B + ^ | + | v + D <----- C + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("C").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputC = taskSchedulerC.buildInputWire("inputC"); + + final TaskScheduler taskSchedulerD = + model.schedulerBuilder("D").build().cast(); + final InputWire inputD = taskSchedulerD.buildInputWire("inputD"); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputC); + taskSchedulerC.getOutputWire().solderTo(inputD); + taskSchedulerD.getOutputWire().solderTo(inputA, true); + + validateModel(model, false); + } + + @Test + void loopSizeFourBrokenByMissingBoundTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A -----> B + ^ | + | v + D <----- C + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("C").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputC = taskSchedulerC.buildInputWire("inputC"); + + final TaskScheduler taskSchedulerD = + model.schedulerBuilder("D").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputD = taskSchedulerD.buildInputWire("inputD"); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputC); + taskSchedulerC.getOutputWire().solderTo(inputD); + taskSchedulerD.getOutputWire().solderTo(inputA); + + validateModel(model, true); + } + + @Test + void loopSizeFourWithChainTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A + | + v + B + | + v + C + | + v + D -----> E + ^ | + | v + G <----- F -----> H -----> I -----> J + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("C").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputC = taskSchedulerC.buildInputWire("inputC"); + + final TaskScheduler taskSchedulerD = + model.schedulerBuilder("D").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputD = taskSchedulerD.buildInputWire("inputD"); + + final TaskScheduler taskSchedulerE = + model.schedulerBuilder("E").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputE = taskSchedulerE.buildInputWire("inputE"); + + final TaskScheduler taskSchedulerF = + model.schedulerBuilder("F").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputF = taskSchedulerF.buildInputWire("inputF"); + + final TaskScheduler taskSchedulerG = + model.schedulerBuilder("G").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputG = taskSchedulerG.buildInputWire("inputG"); + + final TaskScheduler taskSchedulerH = + model.schedulerBuilder("H").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputH = taskSchedulerH.buildInputWire("inputH"); + + final TaskScheduler taskSchedulerI = + model.schedulerBuilder("I").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputI = taskSchedulerI.buildInputWire("inputI"); + + final TaskScheduler taskSchedulerJ = + model.schedulerBuilder("J").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputJ = taskSchedulerJ.buildInputWire("inputJ"); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputC); + taskSchedulerC.getOutputWire().solderTo(inputD); + taskSchedulerD.getOutputWire().solderTo(inputE); + taskSchedulerE.getOutputWire().solderTo(inputF); + taskSchedulerF.getOutputWire().solderTo(inputG); + taskSchedulerG.getOutputWire().solderTo(inputD); + + taskSchedulerF.getOutputWire().solderTo(inputH); + taskSchedulerH.getOutputWire().solderTo(inputI); + taskSchedulerI.getOutputWire().solderTo(inputJ); + + validateModel(model, true); + } + + @Test + void loopSizeFourWithChainBrokenByInjectionTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A + | + v + B + | + v + C + | + v + D -----> E + ^ | + | v + G <----- F -----> H -----> I -----> J + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("C").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputC = taskSchedulerC.buildInputWire("inputC"); + + final TaskScheduler taskSchedulerD = + model.schedulerBuilder("D").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputD = taskSchedulerD.buildInputWire("inputD"); + + final TaskScheduler taskSchedulerE = + model.schedulerBuilder("E").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputE = taskSchedulerE.buildInputWire("inputE"); + + final TaskScheduler taskSchedulerF = + model.schedulerBuilder("F").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputF = taskSchedulerF.buildInputWire("inputF"); + + final TaskScheduler taskSchedulerG = + model.schedulerBuilder("G").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputG = taskSchedulerG.buildInputWire("inputG"); + + final TaskScheduler taskSchedulerH = + model.schedulerBuilder("H").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputH = taskSchedulerH.buildInputWire("inputH"); + + final TaskScheduler taskSchedulerI = + model.schedulerBuilder("I").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputI = taskSchedulerI.buildInputWire("inputI"); + + final TaskScheduler taskSchedulerJ = + model.schedulerBuilder("J").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputJ = taskSchedulerJ.buildInputWire(""); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputC); + taskSchedulerC.getOutputWire().solderTo(inputD); + taskSchedulerD.getOutputWire().solderTo(inputE); + taskSchedulerE.getOutputWire().solderTo(inputF); + taskSchedulerF.getOutputWire().solderTo(inputG); + taskSchedulerG.getOutputWire().solderTo(inputD, true); + + taskSchedulerF.getOutputWire().solderTo(inputH); + taskSchedulerH.getOutputWire().solderTo(inputI); + taskSchedulerI.getOutputWire().solderTo(inputJ); + + validateModel(model, false); + } + + @Test + void loopSizeFourWithChainBrokenByMissingBoundTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A + | + v + B + | + v + C + | + v + D -----> E + ^ | + | v + G <----- F -----> H -----> I -----> J + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("C").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputC = taskSchedulerC.buildInputWire("inputC"); + + final TaskScheduler taskSchedulerD = + model.schedulerBuilder("D").build().cast(); + final InputWire inputD = taskSchedulerD.buildInputWire("inputD"); + + final TaskScheduler taskSchedulerE = + model.schedulerBuilder("E").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputE = taskSchedulerE.buildInputWire("inputE"); + + final TaskScheduler taskSchedulerF = + model.schedulerBuilder("F").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputF = taskSchedulerF.buildInputWire("inputF"); + + final TaskScheduler taskSchedulerG = + model.schedulerBuilder("G").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputG = taskSchedulerG.buildInputWire("inputG"); + + final TaskScheduler taskSchedulerH = + model.schedulerBuilder("H").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputH = taskSchedulerH.buildInputWire("inputH"); + + final TaskScheduler taskSchedulerI = + model.schedulerBuilder("I").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputI = taskSchedulerI.buildInputWire("inputI"); + + final TaskScheduler taskSchedulerJ = + model.schedulerBuilder("J").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputJ = taskSchedulerJ.buildInputWire("inputJ"); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputC); + taskSchedulerC.getOutputWire().solderTo(inputD); + taskSchedulerD.getOutputWire().solderTo(inputE); + taskSchedulerE.getOutputWire().solderTo(inputF); + taskSchedulerF.getOutputWire().solderTo(inputG); + taskSchedulerG.getOutputWire().solderTo(inputD); + + taskSchedulerF.getOutputWire().solderTo(inputH); + taskSchedulerH.getOutputWire().solderTo(inputI); + taskSchedulerI.getOutputWire().solderTo(inputJ); + + validateModel(model, false); + } + + @Test + void multiLoopTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A <---------------------------------| + | | + v | + B | + | | + v | + C | + | | + v | + D -----> E <---------------| | + ^ | | | + | v | | + G <----- F -----> H -----> I -----> J + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("C").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputC = taskSchedulerC.buildInputWire("inputC"); + + final TaskScheduler taskSchedulerD = + model.schedulerBuilder("D").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputD = taskSchedulerD.buildInputWire("inputD"); + + final TaskScheduler taskSchedulerE = + model.schedulerBuilder("E").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputE = taskSchedulerE.buildInputWire("inputE"); + + final TaskScheduler taskSchedulerF = + model.schedulerBuilder("F").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputF = taskSchedulerF.buildInputWire("inputF"); + + final TaskScheduler taskSchedulerG = + model.schedulerBuilder("G").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputG = taskSchedulerG.buildInputWire("inputG"); + + final TaskScheduler taskSchedulerH = + model.schedulerBuilder("H").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputH = taskSchedulerH.buildInputWire("inputH"); + + final TaskScheduler taskSchedulerI = + model.schedulerBuilder("I").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputI = taskSchedulerI.buildInputWire("inputI"); + + final TaskScheduler taskSchedulerJ = + model.schedulerBuilder("J").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputJ = taskSchedulerJ.buildInputWire("inputJ"); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputC); + taskSchedulerC.getOutputWire().solderTo(inputD); + taskSchedulerD.getOutputWire().solderTo(inputE); + taskSchedulerE.getOutputWire().solderTo(inputF); + taskSchedulerF.getOutputWire().solderTo(inputG); + taskSchedulerG.getOutputWire().solderTo(inputD); + + taskSchedulerF.getOutputWire().solderTo(inputH); + taskSchedulerH.getOutputWire().solderTo(inputI); + taskSchedulerI.getOutputWire().solderTo(inputJ); + + taskSchedulerJ.getOutputWire().solderTo(inputA); + + taskSchedulerI.getOutputWire().solderTo(inputE); + + validateModel(model, true); + } + + @Test + void multiLoopBrokenByInjectionTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A <---------------------------------| + | | + v | + B | + | | + v | + C | + | | + v | + D -----> E <---------------| | + ^ | | | + | v | | + G <----- F -----> H -----> I -----> J + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("C").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputC = taskSchedulerC.buildInputWire("inputC"); + + final TaskScheduler taskSchedulerD = + model.schedulerBuilder("D").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputD = taskSchedulerD.buildInputWire("inputD"); + + final TaskScheduler taskSchedulerE = + model.schedulerBuilder("E").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputE = taskSchedulerE.buildInputWire("inputE"); + + final TaskScheduler taskSchedulerF = + model.schedulerBuilder("F").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputF = taskSchedulerF.buildInputWire("inputF"); + + final TaskScheduler taskSchedulerG = + model.schedulerBuilder("G").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputG = taskSchedulerG.buildInputWire("inputG"); + + final TaskScheduler taskSchedulerH = + model.schedulerBuilder("H").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputH = taskSchedulerH.buildInputWire("inputH"); + + final TaskScheduler taskSchedulerI = + model.schedulerBuilder("I").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputI = taskSchedulerI.buildInputWire("inputI"); + + final TaskScheduler taskSchedulerJ = + model.schedulerBuilder("J").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputJ = taskSchedulerJ.buildInputWire("inputJ"); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputC); + taskSchedulerC.getOutputWire().solderTo(inputD); + taskSchedulerD.getOutputWire().solderTo(inputE); + taskSchedulerE.getOutputWire().solderTo(inputF); + taskSchedulerF.getOutputWire().solderTo(inputG); + taskSchedulerG.getOutputWire().solderTo(inputD, true); + + taskSchedulerF.getOutputWire().solderTo(inputH); + taskSchedulerH.getOutputWire().solderTo(inputI); + taskSchedulerI.getOutputWire().solderTo(inputJ); + + taskSchedulerJ.getOutputWire().solderTo(inputA, true); + + taskSchedulerI.getOutputWire().solderTo(inputE, true); + + validateModel(model, false); + } + + @Test + void multiLoopBrokenByMissingBoundTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A <---------------------------------| + | | + v | + B | + | | + v | + C | + | | + v | + D -----> E <---------------| | + ^ | | | + | v | | + G <----- F -----> H -----> I -----> J + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("C").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputC = taskSchedulerC.buildInputWire("inputC"); + + final TaskScheduler taskSchedulerD = + model.schedulerBuilder("D").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputD = taskSchedulerD.buildInputWire("inputD"); + + final TaskScheduler taskSchedulerE = + model.schedulerBuilder("E").build().cast(); + final InputWire inputE = taskSchedulerE.buildInputWire("inputE"); + + final TaskScheduler taskSchedulerF = + model.schedulerBuilder("F").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputF = taskSchedulerF.buildInputWire("inputF"); + + final TaskScheduler taskSchedulerG = + model.schedulerBuilder("G").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputG = taskSchedulerG.buildInputWire("inputG"); + + final TaskScheduler taskSchedulerH = + model.schedulerBuilder("H").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputH = taskSchedulerH.buildInputWire("inputH"); + + final TaskScheduler taskSchedulerI = + model.schedulerBuilder("I").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputI = taskSchedulerI.buildInputWire("inputI"); + + final TaskScheduler taskSchedulerJ = + model.schedulerBuilder("J").build().cast(); + final InputWire inputJ = taskSchedulerJ.buildInputWire("inputJ"); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputC); + taskSchedulerC.getOutputWire().solderTo(inputD); + taskSchedulerD.getOutputWire().solderTo(inputE); + taskSchedulerE.getOutputWire().solderTo(inputF); + taskSchedulerF.getOutputWire().solderTo(inputG); + taskSchedulerG.getOutputWire().solderTo(inputD); + + taskSchedulerF.getOutputWire().solderTo(inputH); + taskSchedulerH.getOutputWire().solderTo(inputI); + taskSchedulerI.getOutputWire().solderTo(inputJ); + + taskSchedulerJ.getOutputWire().solderTo(inputA); + + taskSchedulerI.getOutputWire().solderTo(inputE); + + validateModel(model, false); + } + + @Test + void filterInCycleTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A + | + v + B + | + v + C + | + v + D -----> E + ^ | + | v + G <----- F -----> H -----> I -----> J + + Connection D -> E uses a filter + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("C").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputC = taskSchedulerC.buildInputWire("inputC"); + + final TaskScheduler taskSchedulerD = + model.schedulerBuilder("D").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputD = taskSchedulerD.buildInputWire("inputD"); + + final TaskScheduler taskSchedulerE = + model.schedulerBuilder("E").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputE = taskSchedulerE.buildInputWire("inputE"); + + final TaskScheduler taskSchedulerF = + model.schedulerBuilder("F").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputF = taskSchedulerF.buildInputWire("inputF"); + + final TaskScheduler taskSchedulerG = + model.schedulerBuilder("G").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputG = taskSchedulerG.buildInputWire("inputG"); + + final TaskScheduler taskSchedulerH = + model.schedulerBuilder("H").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputH = taskSchedulerH.buildInputWire("inputH"); + + final TaskScheduler taskSchedulerI = + model.schedulerBuilder("I").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputI = taskSchedulerI.buildInputWire("inputI"); + + final TaskScheduler taskSchedulerJ = + model.schedulerBuilder("J").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputJ = taskSchedulerJ.buildInputWire(""); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputC); + taskSchedulerC.getOutputWire().solderTo(inputD); + taskSchedulerD.getOutputWire().buildFilter("onlyEven", x -> x % 2 == 0).solderTo(inputE); + taskSchedulerE.getOutputWire().solderTo(inputF); + taskSchedulerF.getOutputWire().solderTo(inputG); + taskSchedulerG.getOutputWire().solderTo(inputD); + + taskSchedulerF.getOutputWire().solderTo(inputH); + taskSchedulerH.getOutputWire().solderTo(inputI); + taskSchedulerI.getOutputWire().solderTo(inputJ); + + validateModel(model, true); + } + + @Test + void transformerInCycleTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A + | + v + B + | + v + C + | + v + D -----> E + ^ | + | v + G <----- F -----> H -----> I -----> J + + Connection D -> E uses a transformer + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("C").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputC = taskSchedulerC.buildInputWire("inputC"); + + final TaskScheduler taskSchedulerD = + model.schedulerBuilder("D").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputD = taskSchedulerD.buildInputWire("inputD"); + + final TaskScheduler taskSchedulerE = + model.schedulerBuilder("E").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputE = taskSchedulerE.buildInputWire("inputE"); + + final TaskScheduler taskSchedulerF = + model.schedulerBuilder("F").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputF = taskSchedulerF.buildInputWire("inputF"); + + final TaskScheduler taskSchedulerG = + model.schedulerBuilder("G").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputG = taskSchedulerG.buildInputWire("inputG"); + + final TaskScheduler taskSchedulerH = + model.schedulerBuilder("H").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputH = taskSchedulerH.buildInputWire("inputH"); + + final TaskScheduler taskSchedulerI = + model.schedulerBuilder("I").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputI = taskSchedulerI.buildInputWire("inputI"); + + final TaskScheduler taskSchedulerJ = + model.schedulerBuilder("J").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputJ = taskSchedulerJ.buildInputWire(""); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputC); + taskSchedulerC.getOutputWire().solderTo(inputD); + taskSchedulerD.getOutputWire().buildTransformer("inverter", x -> -x).solderTo(inputE); + taskSchedulerE.getOutputWire().solderTo(inputF); + taskSchedulerF.getOutputWire().solderTo(inputG); + taskSchedulerG.getOutputWire().solderTo(inputD); + + taskSchedulerF.getOutputWire().solderTo(inputH); + taskSchedulerH.getOutputWire().solderTo(inputI); + taskSchedulerI.getOutputWire().solderTo(inputJ); + + validateModel(model, true); + } + + @Test + void splitterInCycleTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A + | + v + B + | + v + C + | + v + D -----> E + ^ | + | v + G <----- F -----> H -----> I -----> J + + Connection D -> E uses a splitter + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("C").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputC = taskSchedulerC.buildInputWire("inputC"); + + final TaskScheduler> taskSchedulerD = + model.schedulerBuilder("D").withUnhandledTaskCapacity(1).build().cast(); + final InputWire> inputD = taskSchedulerD.buildInputWire("inputD"); + + final TaskScheduler taskSchedulerE = + model.schedulerBuilder("E").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputE = taskSchedulerE.buildInputWire("inputE"); + + final TaskScheduler taskSchedulerF = + model.schedulerBuilder("F").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputF = taskSchedulerF.buildInputWire("inputF"); + + final TaskScheduler taskSchedulerG = + model.schedulerBuilder("G").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputG = taskSchedulerG.buildInputWire("inputG"); + + final TaskScheduler taskSchedulerH = + model.schedulerBuilder("H").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputH = taskSchedulerH.buildInputWire("inputH"); + + final TaskScheduler taskSchedulerI = + model.schedulerBuilder("I").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputI = taskSchedulerI.buildInputWire("inputI"); + + final TaskScheduler taskSchedulerJ = + model.schedulerBuilder("J").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputJ = taskSchedulerJ.buildInputWire(""); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputC); + taskSchedulerC.getOutputWire().solderTo(inputD); + final OutputWire splitter = taskSchedulerD.getOutputWire().buildSplitter(); + splitter.solderTo(inputE); + taskSchedulerE.getOutputWire().solderTo(inputF); + taskSchedulerF.getOutputWire().solderTo(inputG); + taskSchedulerG.getOutputWire().solderTo(inputD); + + taskSchedulerF.getOutputWire().solderTo(inputH); + taskSchedulerH.getOutputWire().solderTo(inputI); + taskSchedulerI.getOutputWire().solderTo(inputJ); + + validateModel(model, true); + } + + @Test + void multipleOutputCycleTest() { + final WiringModel model = + WiringModel.create(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + + /* + + A <---------------------------------| + | | + v | + B | + | | + v | + C | + | | + v | + D -----> E <---------------| | + ^ | | | + | v | | + G <----- F -----> H -----> I -----> J + | ^ + | | + |--------| + + I has secondary output channels + + */ + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("C").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputC = taskSchedulerC.buildInputWire("inputC"); + + final TaskScheduler taskSchedulerD = + model.schedulerBuilder("D").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputD = taskSchedulerD.buildInputWire("inputD"); + + final TaskScheduler taskSchedulerE = + model.schedulerBuilder("E").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputE = taskSchedulerE.buildInputWire("inputE"); + + final TaskScheduler taskSchedulerF = + model.schedulerBuilder("F").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputF = taskSchedulerF.buildInputWire("inputF"); + + final TaskScheduler taskSchedulerG = + model.schedulerBuilder("G").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputG = taskSchedulerG.buildInputWire("inputG"); + + final TaskScheduler taskSchedulerH = + model.schedulerBuilder("H").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputH = taskSchedulerH.buildInputWire("inputH"); + + final TaskScheduler taskSchedulerI = + model.schedulerBuilder("I").withUnhandledTaskCapacity(1).build().cast(); + final OutputWire secondaryOutputI = taskSchedulerI.buildSecondaryOutputWire(); + final OutputWire tertiaryOutputI = taskSchedulerI.buildSecondaryOutputWire(); + final InputWire inputI = taskSchedulerI.buildInputWire("inputI"); + + final TaskScheduler taskSchedulerJ = + model.schedulerBuilder("J").withUnhandledTaskCapacity(1).build().cast(); + final InputWire inputJ = taskSchedulerJ.buildInputWire("inputJ"); + final InputWire inputJ2 = taskSchedulerJ.buildInputWire("inputJ2"); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputC); + taskSchedulerC.getOutputWire().solderTo(inputD); + taskSchedulerD.getOutputWire().solderTo(inputE); + taskSchedulerE.getOutputWire().solderTo(inputF); + taskSchedulerF.getOutputWire().solderTo(inputG); + taskSchedulerG.getOutputWire().solderTo(inputD); + + taskSchedulerF.getOutputWire().solderTo(inputH); + taskSchedulerH.getOutputWire().solderTo(inputI); + taskSchedulerI.getOutputWire().solderTo(inputJ); + + taskSchedulerJ.getOutputWire().solderTo(inputA); + + secondaryOutputI.solderTo(inputE); + tertiaryOutputI.solderTo(inputJ2); + + validateModel(model, true); + } +} diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/schedulers/ConcurrentTaskSchedulerTests.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/schedulers/ConcurrentTaskSchedulerTests.java new file mode 100644 index 000000000000..94307289ee19 --- /dev/null +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/schedulers/ConcurrentTaskSchedulerTests.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.schedulers; + +import static com.swirlds.common.test.fixtures.AssertionUtils.assertEventuallyEquals; +import static com.swirlds.common.test.fixtures.AssertionUtils.assertEventuallyTrue; +import static com.swirlds.common.test.fixtures.RandomUtils.getRandomPrintSeed; +import static java.util.concurrent.TimeUnit.MICROSECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.swirlds.common.wiring.InputWire; +import com.swirlds.common.wiring.TaskScheduler; +import com.swirlds.common.wiring.WiringModel; +import com.swirlds.test.framework.TestWiringModel; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.time.Duration; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; + +class ConcurrentTaskSchedulerTests { + + private static final WiringModel model = TestWiringModel.getInstance(); + + /** + * Add a bunch of operations to a wire and ensure that they are all eventually handled. + */ + @Test + void allOperationsHandledTest() { + final Random random = getRandomPrintSeed(); + + final AtomicLong count = new AtomicLong(); + final Consumer handler = x -> { + count.addAndGet(x); + try { + MICROSECONDS.sleep(random.nextInt(1000)); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }; + + final TaskScheduler taskScheduler = + model.schedulerBuilder("test").withConcurrency(true).build().cast(); + final InputWire channel = taskScheduler + .buildInputWire("channel") + .withInputType(Integer.class) + .bind(handler); + + assertEquals(-1, taskScheduler.getUnprocessedTaskCount()); + + long expecterdCount = 0; + for (int i = 0; i < 100; i++) { + final int value = random.nextInt(); + expecterdCount += value; + channel.put(value); + } + + assertEventuallyEquals(expecterdCount, count::get, Duration.ofSeconds(1), "count did not reach expected value"); + + assertEquals(-1, taskScheduler.getUnprocessedTaskCount()); + } + + /** + * Verify that operations can be handled in parallel. + */ + @Test + void parallelOperationTest() { + final Random random = getRandomPrintSeed(); + + // Each operation has a value that needs to be added the counter. + // Most operations will have a null latch & started variables. + // Operations that do not have a null latch & started variables will block + record Operation(int value, @Nullable CountDownLatch latch, @Nullable AtomicBoolean started) {} + + final AtomicLong count = new AtomicLong(); + final Consumer handler = x -> { + if (x.started != null) { + x.started.set(true); + try { + x.latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + count.addAndGet(x.value); + }; + + final TaskScheduler taskScheduler = + model.schedulerBuilder("test").withConcurrency(true).build().cast(); + final InputWire channel = taskScheduler + .buildInputWire("channel") + .withInputType(Operation.class) + .bind(handler); + + assertEquals(-1, taskScheduler.getUnprocessedTaskCount()); + + // Create two blocking operations. We should expect to see both operations started even though + // neither operation will be able to finish. + + final CountDownLatch latch0 = new CountDownLatch(1); + final AtomicBoolean started0 = new AtomicBoolean(); + final CountDownLatch latch1 = new CountDownLatch(1); + final AtomicBoolean started1 = new AtomicBoolean(); + + long expecterdCount = 0; + for (int i = 0; i < 100; i++) { + final int value = random.nextInt(); + expecterdCount += value; + if (i == 0) { + channel.put(new Operation(value, latch0, started0)); + } else if (i == 1) { + channel.put(new Operation(value, latch1, started1)); + } else { + channel.put(new Operation(value, null, null)); + } + } + + assertEventuallyTrue( + () -> started0.get() && started1.get(), Duration.ofSeconds(1), "operations did not all start"); + + assertEquals(-1, taskScheduler.getUnprocessedTaskCount()); + + latch0.countDown(); + latch1.countDown(); + + assertEventuallyEquals(expecterdCount, count::get, Duration.ofSeconds(1), "count did not reach expected value"); + + assertEquals(-1, taskScheduler.getUnprocessedTaskCount()); + } +} diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/schedulers/SequentialTaskSchedulerTests.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/schedulers/SequentialTaskSchedulerTests.java new file mode 100644 index 000000000000..ed8bdc186284 --- /dev/null +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/schedulers/SequentialTaskSchedulerTests.java @@ -0,0 +1,1968 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.schedulers; + +import static com.swirlds.common.test.fixtures.AssertionUtils.assertEventuallyEquals; +import static com.swirlds.common.test.fixtures.AssertionUtils.assertEventuallyTrue; +import static com.swirlds.common.test.fixtures.AssertionUtils.completeBeforeTimeout; +import static com.swirlds.common.test.fixtures.RandomUtils.getRandomPrintSeed; +import static com.swirlds.common.threading.manager.AdHocThreadManager.getStaticThreadManager; +import static com.swirlds.common.utility.NonCryptographicHashing.hash32; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.swirlds.common.threading.framework.config.ThreadConfiguration; +import com.swirlds.common.wiring.InputWire; +import com.swirlds.common.wiring.OutputWire; +import com.swirlds.common.wiring.TaskScheduler; +import com.swirlds.common.wiring.WiringModel; +import com.swirlds.common.wiring.counters.BackpressureObjectCounter; +import com.swirlds.common.wiring.counters.ObjectCounter; +import com.swirlds.test.framework.TestWiringModel; +import java.time.Duration; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Function; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class SequentialTaskSchedulerTests { + + private static final WiringModel model = TestWiringModel.getInstance(); + + @Test + void illegalNamesTest() { + assertThrows(NullPointerException.class, () -> model.schedulerBuilder(null)); + assertThrows(IllegalArgumentException.class, () -> model.schedulerBuilder("")); + assertThrows(IllegalArgumentException.class, () -> model.schedulerBuilder(" ")); + assertThrows(IllegalArgumentException.class, () -> model.schedulerBuilder("foo bar")); + assertThrows(IllegalArgumentException.class, () -> model.schedulerBuilder("foo?bar")); + assertThrows(IllegalArgumentException.class, () -> model.schedulerBuilder("foo:bar")); + assertThrows(IllegalArgumentException.class, () -> model.schedulerBuilder("foo*bar")); + assertThrows(IllegalArgumentException.class, () -> model.schedulerBuilder("foo/bar")); + assertThrows(IllegalArgumentException.class, () -> model.schedulerBuilder("foo\\bar")); + assertThrows(IllegalArgumentException.class, () -> model.schedulerBuilder("foo-bar")); + + // legal names that should not throw + model.schedulerBuilder("x"); + model.schedulerBuilder("fooBar"); + model.schedulerBuilder("foo_bar"); + model.schedulerBuilder("foo_bar123"); + model.schedulerBuilder("123"); + } + + /** + * Add values to the task scheduler, ensure that each value was processed in the correct order. + */ + @Test + void orderOfOperationsTest() { + final AtomicInteger wireValue = new AtomicInteger(); + final Consumer handler = x -> wireValue.set(hash32(wireValue.get(), x)); + + final TaskScheduler taskScheduler = + model.schedulerBuilder("test").withConcurrency(false).build().cast(); + final InputWire channel = taskScheduler + .buildInputWire("channel") + .withInputType(Integer.class) + .bind(handler); + assertEquals(-1, taskScheduler.getUnprocessedTaskCount()); + assertEquals("test", taskScheduler.getName()); + + int value = 0; + for (int i = 0; i < 100; i++) { + channel.put(i); + value = hash32(value, i); + } + + assertEventuallyEquals(value, wireValue::get, Duration.ofSeconds(1), "Wire sum did not match expected sum"); + } + + /** + * Add values to the task scheduler, ensure that each value was processed in the correct order. Add a delay to the + * handler. The delay should not effect the final value if things are happening as we expect. If the task scheduler + * is allowing things to happen with parallelism, then the delay is likely to result in a reordering of operations + * (which will fail the test). + */ + @Test + void orderOfOperationsWithDelayTest() { + final Random random = getRandomPrintSeed(); + + final AtomicInteger wireValue = new AtomicInteger(); + final Consumer handler = x -> { + wireValue.set(hash32(wireValue.get(), x)); + try { + // Sleep for up to a millisecond + NANOSECONDS.sleep(random.nextInt(1_000_000)); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }; + + final TaskScheduler taskScheduler = + model.schedulerBuilder("test").withConcurrency(false).build().cast(); + final InputWire channel = taskScheduler + .buildInputWire("channel") + .withInputType(Integer.class) + .bind(handler); + assertEquals(-1, taskScheduler.getUnprocessedTaskCount()); + assertEquals("test", taskScheduler.getName()); + + int value = 0; + for (int i = 0; i < 100; i++) { + channel.put(i); + value = hash32(value, i); + } + + assertEventuallyEquals(value, wireValue::get, Duration.ofSeconds(1), "Wire sum did not match expected sum"); + } + + /** + * Multiple threads adding work to the task scheduler shouldn't cause problems. Also, work should always be handled + * sequentially regardless of the number of threads adding work. + */ + @Test + void multipleChannelsTest() { + final AtomicInteger wireValue = new AtomicInteger(); + final AtomicInteger operationCount = new AtomicInteger(); + final Set arguments = ConcurrentHashMap.newKeySet(); // concurrent hash set + final Consumer handler = x -> { + arguments.add(x); + // This will result in a deterministic value if there is no parallelism + wireValue.set(hash32(wireValue.get(), operationCount.getAndIncrement())); + }; + + final TaskScheduler taskScheduler = + model.schedulerBuilder("test").withConcurrency(false).build().cast(); + final InputWire channel = taskScheduler + .buildInputWire("channel") + .withInputType(Integer.class) + .bind(handler); + assertEquals(-1, taskScheduler.getUnprocessedTaskCount()); + assertEquals("test", taskScheduler.getName()); + + final int operationsPerWorker = 1_000; + final int workers = 10; + + for (int i = 0; i < workers; i++) { + final int workerNumber = i; + new ThreadConfiguration(getStaticThreadManager()) + .setRunnable(() -> { + for (int j = 0; j < operationsPerWorker; j++) { + channel.put(workerNumber * j); + } + }) + .build(true); + } + + // Compute the values we expect to be computed by the wire + final Set expectedArguments = new HashSet<>(); + int expectedValue = 0; + int count = 0; + for (int i = 0; i < workers; i++) { + for (int j = 0; j < operationsPerWorker; j++) { + expectedArguments.add(i * j); + expectedValue = hash32(expectedValue, count); + count++; + } + } + + assertEventuallyEquals( + expectedValue, wireValue::get, Duration.ofSeconds(1), "Wire sum did not match expected sum"); + assertEventuallyEquals( + expectedArguments.size(), + arguments::size, + Duration.ofSeconds(1), + "Wire arguments did not match expected arguments"); + assertEquals(expectedArguments, arguments); + } + + /** + * Multiple threads adding work to the task scheduler shouldn't cause problems. Also, work should always be handled + * sequentially regardless of the number of threads adding work. Random delay is added to the workers. This should + * not effect the outcome. + */ + @Test + void multipleChannelsWithDelayTest() { + final Random random = getRandomPrintSeed(); + + final AtomicInteger wireValue = new AtomicInteger(); + final AtomicInteger operationCount = new AtomicInteger(); + final Set arguments = ConcurrentHashMap.newKeySet(); // concurrent hash set + final Consumer handler = x -> { + arguments.add(x); + // This will result in a deterministic value if there is no parallelism + wireValue.set(hash32(wireValue.get(), operationCount.getAndIncrement())); + }; + + final TaskScheduler taskScheduler = + model.schedulerBuilder("test").withConcurrency(false).build().cast(); + final InputWire channel = taskScheduler + .buildInputWire("channel") + .withInputType(Integer.class) + .bind(handler); + assertEquals(-1, taskScheduler.getUnprocessedTaskCount()); + assertEquals("test", taskScheduler.getName()); + + final int operationsPerWorker = 1_000; + final int workers = 10; + + for (int i = 0; i < workers; i++) { + final int workerNumber = i; + new ThreadConfiguration(getStaticThreadManager()) + .setRunnable(() -> { + for (int j = 0; j < operationsPerWorker; j++) { + if (random.nextDouble() < 0.1) { + try { + NANOSECONDS.sleep(random.nextInt(100)); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + channel.put(workerNumber * j); + } + }) + .build(true); + } + + // Compute the values we expect to be computed by the wire + final Set expectedArguments = new HashSet<>(); + int expectedValue = 0; + int count = 0; + for (int i = 0; i < workers; i++) { + for (int j = 0; j < operationsPerWorker; j++) { + expectedArguments.add(i * j); + expectedValue = hash32(expectedValue, count); + count++; + } + } + + assertEventuallyEquals( + expectedValue, wireValue::get, Duration.ofSeconds(1), "Wire sum did not match expected sum"); + assertEventuallyEquals( + expectedArguments.size(), + arguments::size, + Duration.ofSeconds(1), + "Wire arguments did not match expected arguments"); + assertEquals(expectedArguments, arguments); + } + + /** + * Ensure that the work happening on the task scheduler is not happening on the callers thread. + */ + @Test + void wireWordDoesNotBlockCallingThreadTest() throws InterruptedException { + final AtomicInteger wireValue = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(1); + final Consumer handler = x -> { + wireValue.set(hash32(wireValue.get(), x)); + if (x == 50) { + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }; + + final TaskScheduler taskScheduler = + model.schedulerBuilder("test").withConcurrency(false).build().cast(); + final InputWire channel = taskScheduler + .buildInputWire("channel") + .withInputType(Integer.class) + .bind(handler); + assertEquals(-1, taskScheduler.getUnprocessedTaskCount()); + assertEquals("test", taskScheduler.getName()); + + // The wire will stop processing at 50, but this should not block the calling thread. + final AtomicInteger value = new AtomicInteger(); + completeBeforeTimeout( + () -> { + for (int i = 0; i < 100; i++) { + channel.put(i); + value.set(hash32(value.get(), i)); + } + }, + Duration.ofSeconds(1), + "calling thread was blocked"); + + // Release the latch and allow the wire to finish + latch.countDown(); + + assertEventuallyEquals( + value.get(), wireValue::get, Duration.ofSeconds(1), "Wire sum did not match expected sum"); + } + + /** + * Sanity checks on the unprocessed event count. + */ + @Test + void unprocessedEventCountTest() { + final AtomicInteger wireValue = new AtomicInteger(); + final CountDownLatch latch0 = new CountDownLatch(1); + final CountDownLatch latch50 = new CountDownLatch(1); + final CountDownLatch latch98 = new CountDownLatch(1); + final Consumer handler = x -> { + try { + if (x == 0) { + latch0.await(); + } else if (x == 50) { + latch50.await(); + } else if (x == 98) { + latch98.await(); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + wireValue.set(hash32(wireValue.get(), x)); + }; + + final TaskScheduler taskScheduler = model.schedulerBuilder("test") + .withConcurrency(false) + .withMetricsBuilder(model.metricsBuilder().withUnhandledTaskMetricEnabled(true)) + .build() + .cast(); + final InputWire channel = taskScheduler + .buildInputWire("channel") + .withInputType(Integer.class) + .bind(handler); + assertEquals(0, taskScheduler.getUnprocessedTaskCount()); + assertEquals("test", taskScheduler.getName()); + + int value = 0; + for (int i = 0; i < 100; i++) { + channel.put(i); + value = hash32(value, i); + } + + assertEventuallyEquals( + 100L, + taskScheduler::getUnprocessedTaskCount, + Duration.ofSeconds(1), + "Wire unprocessed task count did not match expected value, count = " + + taskScheduler.getUnprocessedTaskCount()); + + latch0.countDown(); + + assertEventuallyEquals( + 50L, + taskScheduler::getUnprocessedTaskCount, + Duration.ofSeconds(1), + "Wire unprocessed task count did not match expected value"); + + latch50.countDown(); + + assertEventuallyEquals( + 2L, + taskScheduler::getUnprocessedTaskCount, + Duration.ofSeconds(1), + "Wire unprocessed task count did not match expected value"); + + latch98.countDown(); + + assertEventuallyEquals(value, wireValue::get, Duration.ofSeconds(1), "Wire sum did not match expected sum"); + + assertEquals(0, taskScheduler.getUnprocessedTaskCount()); + } + + /** + * Make sure backpressure works. + */ + @Test + void backpressureTest() throws InterruptedException { + final AtomicInteger wireValue = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(1); + final Consumer handler = x -> { + try { + if (x == 0) { + latch.await(); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + wireValue.set(hash32(wireValue.get(), x)); + }; + + final TaskScheduler taskScheduler = model.schedulerBuilder("test") + .withConcurrency(false) + .withUnhandledTaskCapacity(11) + .withSleepDuration(Duration.ofMillis(1)) + .build() + .cast(); + final InputWire channel = taskScheduler + .buildInputWire("channel") + .withInputType(Integer.class) + .bind(handler); + assertEquals(0, taskScheduler.getUnprocessedTaskCount()); + assertEquals("test", taskScheduler.getName()); + + final AtomicInteger value = new AtomicInteger(); + + // We will be stuck handling 0 and we will have the capacity for 10 more, for a total of 11 tasks in flight + completeBeforeTimeout( + () -> { + for (int i = 0; i < 11; i++) { + channel.put(i); + value.set(hash32(value.get(), i)); + } + }, + Duration.ofSeconds(1), + "unable to add tasks"); + assertEquals(11, taskScheduler.getUnprocessedTaskCount()); + + // Try to enqueue work on another thread. It should get stuck and be + // unable to add anything until we release the latch. + final AtomicBoolean allWorkAdded = new AtomicBoolean(false); + new ThreadConfiguration(getStaticThreadManager()) + .setRunnable(() -> { + for (int i = 11; i < 100; i++) { + channel.put(i); + value.set(hash32(value.get(), i)); + } + allWorkAdded.set(true); + }) + .build(true); + + // Adding work to an unblocked wire should be very fast. If we sleep for a while, we'd expect that an unblocked + // wire would have processed all of the work that was added to it. + MILLISECONDS.sleep(50); + assertFalse(allWorkAdded.get()); + assertEquals(11, taskScheduler.getUnprocessedTaskCount()); + + // Even if the wire has no capacity, neither offer() nor inject() should not block. + completeBeforeTimeout( + () -> { + assertFalse(channel.offer(1234)); + assertFalse(channel.offer(4321)); + assertFalse(channel.offer(-1)); + channel.inject(42); + value.set(hash32(value.get(), 42)); + }, + Duration.ofSeconds(1), + "unable to offer tasks"); + + // Release the latch, all work should now be added + latch.countDown(); + + assertEventuallyTrue(allWorkAdded::get, Duration.ofSeconds(1), "unable to add all work"); + assertEventuallyEquals( + 0L, + taskScheduler::getUnprocessedTaskCount, + Duration.ofSeconds(1), + "Wire unprocessed task count did not match expected value. " + taskScheduler.getUnprocessedTaskCount()); + assertEventuallyEquals( + value.get(), wireValue::get, Duration.ofSeconds(1), "Wire sum did not match expected sum"); + } + + /** + * Test interrupts with accept() when backpressure is being applied. + */ + @Test + void uninterruptableTest() throws InterruptedException { + final AtomicInteger wireValue = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(1); + final Consumer handler = x -> { + try { + if (x == 0) { + latch.await(); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + wireValue.set(hash32(wireValue.get(), x)); + }; + + final TaskScheduler taskScheduler = model.schedulerBuilder("test") + .withConcurrency(false) + .withUnhandledTaskCapacity(11) + .build() + .cast(); + final InputWire channel = taskScheduler + .buildInputWire("channel") + .withInputType(Integer.class) + .bind(handler); + assertEquals(0, taskScheduler.getUnprocessedTaskCount()); + assertEquals("test", taskScheduler.getName()); + + final AtomicInteger value = new AtomicInteger(); + + // We will be stuck handling 0 and we will have the capacity for 10 more, for a total of 11 tasks in flight + completeBeforeTimeout( + () -> { + for (int i = 0; i < 11; i++) { + channel.put(i); + value.set(hash32(value.get(), i)); + } + }, + Duration.ofSeconds(1), + "unable to add tasks"); + assertEquals(11, taskScheduler.getUnprocessedTaskCount()); + + // Try to enqueue work on another thread. It should get stuck and be + // unable to add anything until we release the latch. + final AtomicBoolean allWorkAdded = new AtomicBoolean(false); + final Thread thread = new ThreadConfiguration(getStaticThreadManager()) + .setRunnable(() -> { + for (int i = 11; i < 100; i++) { + channel.put(i); + value.set(hash32(value.get(), i)); + } + allWorkAdded.set(true); + }) + .build(true); + + // Interrupting the thread should have no effect. + thread.interrupt(); + + // Adding work to an unblocked wire should be very fast. If we sleep for a while, we'd expect that an unblocked + // wire would have processed all of the work that was added to it. + MILLISECONDS.sleep(50); + assertFalse(allWorkAdded.get()); + assertEquals(11, taskScheduler.getUnprocessedTaskCount()); + + // Release the latch, all work should now be added + latch.countDown(); + + assertEventuallyTrue(allWorkAdded::get, Duration.ofSeconds(1), "unable to add all work"); + assertEventuallyEquals( + 0L, + taskScheduler::getUnprocessedTaskCount, + Duration.ofSeconds(1), + "Wire unprocessed task count did not match expected value"); + assertEventuallyEquals( + value.get(), wireValue::get, Duration.ofSeconds(1), "Wire sum did not match expected sum"); + } + + /** + * Offering tasks is equivalent to calling accept() if there is no backpressure. + */ + @Test + void offerNoBackpressureTest() { + final AtomicInteger wireValue = new AtomicInteger(); + final Consumer handler = x -> wireValue.set(hash32(wireValue.get(), x)); + + final TaskScheduler taskScheduler = + model.schedulerBuilder("test").withConcurrency(false).build().cast(); + final InputWire channel = taskScheduler + .buildInputWire("channel") + .withInputType(Integer.class) + .bind(handler); + assertEquals(-1, taskScheduler.getUnprocessedTaskCount()); + assertEquals("test", taskScheduler.getName()); + + int value = 0; + for (int i = 0; i < 100; i++) { + assertTrue(channel.offer(i)); + value = hash32(value, i); + } + + assertEventuallyEquals(value, wireValue::get, Duration.ofSeconds(1), "Wire sum did not match expected sum"); + } + + /** + * Test a scenario where there is a circular data flow formed by wires. + *

+ * In this test, all data is passed from A to B to C to D. All data that is a multiple of 7 is passed from D to A as + * a negative value, but is not passed around the loop again. + * + *

+     * A -------> B
+     * ^          |
+     * |          |
+     * |          V
+     * D <------- C
+     * 
+ */ + @Test + void circularDataFlowTest() throws InterruptedException { + final Random random = getRandomPrintSeed(); + + final AtomicInteger countA = new AtomicInteger(); + final AtomicInteger negativeCountA = new AtomicInteger(); + final AtomicInteger countB = new AtomicInteger(); + final AtomicInteger countC = new AtomicInteger(); + final AtomicInteger countD = new AtomicInteger(); + + final TaskScheduler taskSchedulerToA = + model.schedulerBuilder("wireToA").build().cast(); + final TaskScheduler taskSchedulerToB = + model.schedulerBuilder("wireToB").build().cast(); + final TaskScheduler taskSchedulerToC = + model.schedulerBuilder("wireToC").build().cast(); + final TaskScheduler taskSchedulerToD = + model.schedulerBuilder("wireToD").build().cast(); + + final InputWire channelToA = taskSchedulerToA.buildInputWire("channelToA"); + final InputWire channelToB = taskSchedulerToB.buildInputWire("channelToB"); + final InputWire channelToC = taskSchedulerToC.buildInputWire("channelToC"); + final InputWire channelToD = taskSchedulerToD.buildInputWire("channelToD"); + + final Function handlerA = x -> { + if (x > 0) { + countA.set(hash32(x, countA.get())); + return x; + } else { + negativeCountA.set(hash32(x, negativeCountA.get())); + // negative values are values that have been passed around the loop + // Don't pass them on again or else we will get an infinite loop + return null; + } + }; + + final Function handlerB = x -> { + countB.set(hash32(x, countB.get())); + return x; + }; + + final Function handlerC = x -> { + countC.set(hash32(x, countC.get())); + return x; + }; + + final Function handlerD = x -> { + countD.set(hash32(x, countD.get())); + if (x % 7 == 0) { + return -x; + } else { + return null; + } + }; + + taskSchedulerToA.getOutputWire().solderTo(channelToB); + taskSchedulerToB.getOutputWire().solderTo(channelToC); + taskSchedulerToC.getOutputWire().solderTo(channelToD); + taskSchedulerToD.getOutputWire().solderTo(channelToA); + + channelToA.bind(handlerA); + channelToB.bind(handlerB); + channelToC.bind(handlerC); + channelToD.bind(handlerD); + + int expectedCountA = 0; + int expectedNegativeCountA = 0; + int expectedCountB = 0; + int expectedCountC = 0; + int expectedCountD = 0; + + for (int i = 1; i < 1000; i++) { + channelToA.put(i); + + expectedCountA = hash32(i, expectedCountA); + expectedCountB = hash32(i, expectedCountB); + expectedCountC = hash32(i, expectedCountC); + expectedCountD = hash32(i, expectedCountD); + + if (i % 7 == 0) { + expectedNegativeCountA = hash32(-i, expectedNegativeCountA); + } + + // Sleep to give data a chance to flow around the loop + // (as opposed to adding it so quickly that it is all enqueue prior to any processing) + if (random.nextDouble() < 0.1) { + MILLISECONDS.sleep(10); + } + } + + assertEventuallyEquals( + expectedCountA, countA::get, Duration.ofSeconds(1), "Wire A sum did not match expected value"); + assertEventuallyEquals( + expectedNegativeCountA, + negativeCountA::get, + Duration.ofSeconds(1), + "Wire A negative sum did not match expected value"); + assertEventuallyEquals( + expectedCountB, countB::get, Duration.ofSeconds(1), "Wire B sum did not match expected value"); + assertEventuallyEquals( + expectedCountC, countC::get, Duration.ofSeconds(1), "Wire C sum did not match expected value"); + assertEventuallyEquals( + expectedCountD, countD::get, Duration.ofSeconds(1), "Wire D sum did not match expected value"); + } + + /** + * Validate the behavior when there are multiple channels. + */ + @Test + void multipleChannelTypesTest() { + final AtomicInteger wireValue = new AtomicInteger(); + final Consumer integerHandler = x -> wireValue.set(hash32(wireValue.get(), x)); + final Consumer booleanHandler = x -> wireValue.set((x ? -1 : 1) * wireValue.get()); + final Consumer stringHandler = x -> wireValue.set(hash32(wireValue.get(), x.hashCode())); + + final TaskScheduler taskScheduler = + model.schedulerBuilder("test").withConcurrency(false).build().cast(); + + final InputWire integerChannel = taskScheduler + .buildInputWire("integerChannel") + .withInputType(Integer.class) + .bind(integerHandler); + final InputWire booleanChannel = taskScheduler + .buildInputWire("booleanChannel") + .withInputType(Boolean.class) + .bind(booleanHandler); + final InputWire stringChannel = taskScheduler + .buildInputWire("stringChannel") + .withInputType(String.class) + .bind(stringHandler); + + assertEquals(-1, taskScheduler.getUnprocessedTaskCount()); + assertEquals("test", taskScheduler.getName()); + + int value = 0; + for (int i = 0; i < 100; i++) { + integerChannel.put(i); + value = hash32(value, i); + + boolean invert = i % 2 == 0; + booleanChannel.put(invert); + value = (invert ? -1 : 1) * value; + + final String string = String.valueOf(i); + stringChannel.put(string); + value = hash32(value, string.hashCode()); + } + + assertEventuallyEquals(value, wireValue::get, Duration.ofSeconds(1), "Wire value did not match expected value"); + } + + /** + * Make sure backpressure works when there are multiple channels. + */ + @Test + void multipleChannelBackpressureTest() throws InterruptedException { + final AtomicInteger wireValue = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(1); + + final Consumer handler1 = x -> { + try { + if (x == 0) { + latch.await(); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + wireValue.set(hash32(wireValue.get(), x)); + }; + + final Consumer handler2 = x -> wireValue.set(hash32(wireValue.get(), -x)); + + final TaskScheduler taskScheduler = model.schedulerBuilder("test") + .withConcurrency(false) + .withUnhandledTaskCapacity(11) + .build() + .cast(); + + final InputWire channel1 = taskScheduler + .buildInputWire("channel1") + .withInputType(Integer.class) + .bind(handler1); + final InputWire channel2 = taskScheduler + .buildInputWire("channel2") + .withInputType(Integer.class) + .bind(handler2); + + assertEquals(0, taskScheduler.getUnprocessedTaskCount()); + assertEquals("test", taskScheduler.getName()); + + final AtomicInteger value = new AtomicInteger(); + + // We will be stuck handling 0 and we will have the capacity for 10 more, for a total of 11 tasks in flight + completeBeforeTimeout( + () -> { + for (int i = 0; i < 11; i++) { + channel1.put(i); + value.set(hash32(value.get(), i)); + } + }, + Duration.ofSeconds(1), + "unable to add tasks"); + assertEquals(11, taskScheduler.getUnprocessedTaskCount()); + + // Try to enqueue work on another thread. It should get stuck and be + // unable to add anything until we release the latch. + final AtomicBoolean allWorkAdded = new AtomicBoolean(false); + new ThreadConfiguration(getStaticThreadManager()) + .setRunnable(() -> { + for (int i = 11; i < 100; i++) { + channel2.put(i); + value.set(hash32(value.get(), -i)); + } + allWorkAdded.set(true); + }) + .build(true); + + // Adding work to an unblocked wire should be very fast. If we sleep for a while, we'd expect that an unblocked + // wire would have processed all of the work that was added to it. + MILLISECONDS.sleep(50); + assertFalse(allWorkAdded.get()); + assertEquals(11, taskScheduler.getUnprocessedTaskCount()); + + // Even if the wire has no capacity, neither offer() nor inject() should not block. + completeBeforeTimeout( + () -> { + assertFalse(channel1.offer(1234)); + assertFalse(channel1.offer(4321)); + assertFalse(channel1.offer(-1)); + channel1.inject(42); + value.set(hash32(value.get(), 42)); + }, + Duration.ofSeconds(1), + "unable to offer tasks"); + + // Release the latch, all work should now be added + latch.countDown(); + + assertEventuallyTrue(allWorkAdded::get, Duration.ofSeconds(1), "unable to add all work"); + assertEventuallyEquals( + 0L, + taskScheduler::getUnprocessedTaskCount, + Duration.ofSeconds(1), + "Wire unprocessed task count did not match expected value"); + assertEventuallyEquals( + value.get(), wireValue::get, Duration.ofSeconds(1), "Wire sum did not match expected sum"); + } + + /** + * Make sure backpressure works when a single counter spans multiple wires. + */ + @Test + void backpressureOverMultipleWiresTest() throws InterruptedException { + final AtomicInteger wireValueA = new AtomicInteger(); + final AtomicInteger wireValueB = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(1); + + final ObjectCounter backpressure = new BackpressureObjectCounter("test", 11, Duration.ofMillis(1)); + + final TaskScheduler taskSchedulerA = model.schedulerBuilder("testA") + .withConcurrency(false) + .withOnRamp(backpressure) + .build() + .cast(); + + final TaskScheduler taskSchedulerB = model.schedulerBuilder("testB") + .withConcurrency(false) + .withOffRamp(backpressure) + .build() + .cast(); + + final InputWire channelA = taskSchedulerA.buildInputWire("channelA"); + final InputWire channelB = taskSchedulerB.buildInputWire("channelB"); + + final Consumer handlerA = x -> { + wireValueA.set(hash32(wireValueA.get(), -x)); + channelB.put(x); + }; + + final Consumer handlerB = x -> { + try { + if (x == 0) { + latch.await(); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + wireValueB.set(hash32(wireValueB.get(), x)); + }; + + channelA.bind(handlerA); + channelB.bind(handlerB); + + assertEquals(0, backpressure.getCount()); + assertEquals("testA", taskSchedulerA.getName()); + assertEquals("testB", taskSchedulerB.getName()); + + final AtomicInteger valueA = new AtomicInteger(); + final AtomicInteger valueB = new AtomicInteger(); + + // We will be stuck handling 0 and we will have the capacity for 10 more, for a total of 11 tasks in flight + completeBeforeTimeout( + () -> { + for (int i = 0; i < 11; i++) { + channelA.put(i); + valueA.set(hash32(valueA.get(), -i)); + valueB.set(hash32(valueB.get(), i)); + } + }, + Duration.ofSeconds(1), + "unable to add tasks"); + assertEquals(11, backpressure.getCount()); + + // Try to enqueue work on another thread. It should get stuck and be + // unable to add anything until we release the latch. + final AtomicBoolean allWorkAdded = new AtomicBoolean(false); + new ThreadConfiguration(getStaticThreadManager()) + .setRunnable(() -> { + for (int i = 11; i < 100; i++) { + channelA.put(i); + valueA.set(hash32(valueA.get(), -i)); + valueB.set(hash32(valueB.get(), i)); + } + allWorkAdded.set(true); + }) + .build(true); + + // Adding work to an unblocked wire should be very fast. If we sleep for a while, we'd expect that an unblocked + // wire would have processed all of the work that was added to it. + MILLISECONDS.sleep(50); + assertFalse(allWorkAdded.get()); + assertEquals(11, backpressure.getCount()); + + // Even if the wire has no capacity, neither offer() nor inject() should not block. + completeBeforeTimeout( + () -> { + assertFalse(channelA.offer(1234)); + assertFalse(channelA.offer(4321)); + assertFalse(channelA.offer(-1)); + channelA.inject(42); + valueA.set(hash32(valueA.get(), -42)); + valueB.set(hash32(valueB.get(), 42)); + }, + Duration.ofSeconds(1), + "unable to offer tasks"); + + // Release the latch, all work should now be added + latch.countDown(); + + assertEventuallyTrue(allWorkAdded::get, Duration.ofSeconds(1), "unable to add all work"); + assertEventuallyEquals( + 0L, + backpressure::getCount, + Duration.ofSeconds(1), + "Wire unprocessed task count did not match expected value"); + assertEventuallyEquals( + valueA.get(), wireValueA::get, Duration.ofSeconds(1), "Wire sum did not match expected sum"); + assertEventuallyEquals( + valueB.get(), wireValueB::get, Duration.ofSeconds(1), "Wire sum did not match expected sum"); + } + + /** + * Validate the behavior of the flush() method. + */ + @Test + void flushTest() throws InterruptedException { + final AtomicInteger wireValue = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(1); + final Consumer handler = x -> { + try { + if (x == 0) { + latch.await(); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + wireValue.set(hash32(wireValue.get(), x)); + }; + + final TaskScheduler taskScheduler = model.schedulerBuilder("test") + .withConcurrency(false) + .withUnhandledTaskCapacity(11) + .withFlushingEnabled(true) + .build() + .cast(); + final InputWire channel = taskScheduler + .buildInputWire("channel") + .withInputType(Integer.class) + .bind(handler); + assertEquals(0, taskScheduler.getUnprocessedTaskCount()); + assertEquals("test", taskScheduler.getName()); + + final AtomicInteger value = new AtomicInteger(); + + // Flushing a wire with nothing in it should return quickly. + completeBeforeTimeout(taskScheduler::flush, Duration.ofSeconds(1), "unable to flush wire"); + + // We will be stuck handling 0 and we will have the capacity for 10 more, for a total of 11 tasks in flight + completeBeforeTimeout( + () -> { + for (int i = 0; i < 11; i++) { + channel.put(i); + value.set(hash32(value.get(), i)); + } + }, + Duration.ofSeconds(1), + "unable to add tasks"); + assertEquals(11, taskScheduler.getUnprocessedTaskCount()); + + // Try to enqueue work on another thread. It should get stuck and be + // unable to add anything until we release the latch. + final AtomicBoolean allWorkAdded = new AtomicBoolean(false); + new ThreadConfiguration(getStaticThreadManager()) + .setRunnable(() -> { + for (int i = 11; i < 100; i++) { + channel.put(i); + value.set(hash32(value.get(), i)); + } + allWorkAdded.set(true); + }) + .build(true); + + // On another thread, flush the wire. This should also get stuck. + final AtomicBoolean flushed = new AtomicBoolean(false); + new ThreadConfiguration(getStaticThreadManager()) + .setRunnable(() -> { + taskScheduler.flush(); + flushed.set(true); + }) + .build(true); + + // Adding work to an unblocked wire should be very fast. If we sleep for a while, we'd expect that an unblocked + // wire would have processed all of the work that was added to it. + MILLISECONDS.sleep(50); + assertFalse(allWorkAdded.get()); + assertFalse(flushed.get()); + // The flush operation puts a task on the wire, which bumps the number up to 12 from 11 + assertEquals(12, taskScheduler.getUnprocessedTaskCount()); + + // Even if the wire has no capacity, neither offer() nor inject() should not block. + completeBeforeTimeout( + () -> { + assertFalse(channel.offer(1234)); + assertFalse(channel.offer(4321)); + assertFalse(channel.offer(-1)); + channel.inject(42); + value.set(hash32(value.get(), 42)); + }, + Duration.ofSeconds(1), + "unable to offer tasks"); + + // Release the latch, all work should now be added + latch.countDown(); + + assertEventuallyTrue(allWorkAdded::get, Duration.ofSeconds(1), "unable to add all work"); + assertEventuallyTrue(flushed::get, Duration.ofSeconds(1), "unable to flush wire"); + assertEventuallyEquals( + 0L, + taskScheduler::getUnprocessedTaskCount, + Duration.ofSeconds(1), + "Wire unprocessed task count did not match expected value"); + assertEventuallyEquals( + value.get(), wireValue::get, Duration.ofSeconds(1), "Wire sum did not match expected sum"); + } + + @Test + void flushDisabledTest() { + final TaskScheduler taskScheduler = model.schedulerBuilder("test") + .withConcurrency(false) + .withUnhandledTaskCapacity(10) + .build() + .cast(); + + assertThrows(UnsupportedOperationException.class, taskScheduler::flush, "flush() should not be supported"); + } + + @Test + void exceptionHandlingTest() { + final AtomicInteger wireValue = new AtomicInteger(); + final Consumer handler = x -> { + if (x == 50) { + throw new IllegalStateException("intentional"); + } + wireValue.set(hash32(wireValue.get(), x)); + }; + + final AtomicInteger exceptionCount = new AtomicInteger(); + + final TaskScheduler taskScheduler = model.schedulerBuilder("test") + .withConcurrency(false) + .withUncaughtExceptionHandler((t, e) -> exceptionCount.incrementAndGet()) + .build() + .cast(); + final InputWire channel = taskScheduler + .buildInputWire("channel") + .withInputType(Integer.class) + .bind(handler); + assertEquals(-1, taskScheduler.getUnprocessedTaskCount()); + assertEquals("test", taskScheduler.getName()); + + int value = 0; + for (int i = 0; i < 100; i++) { + channel.put(i); + if (i != 50) { + value = hash32(value, i); + } + } + + assertEventuallyEquals(value, wireValue::get, Duration.ofSeconds(1), "Wire sum did not match expected sum"); + assertEquals(1, exceptionCount.get()); + } + + /** + * An early implementation could deadlock in a scenario with backpressure enabled and a thread count that was less + * than the number of blocking wires. + */ + @ParameterizedTest + @ValueSource(ints = {1, 3}) + void deadlockTest(final int parallelism) throws InterruptedException { + final ForkJoinPool pool = new ForkJoinPool(parallelism); + + // create 3 wires with the following bindings: + // a -> b -> c -> latch + final TaskScheduler a = model.schedulerBuilder("a") + .withConcurrency(false) + .withUnhandledTaskCapacity(2) + .withSleepDuration(Duration.ofMillis(1)) + .withPool(pool) + .build() + .cast(); + final TaskScheduler b = model.schedulerBuilder("b") + .withConcurrency(false) + .withUnhandledTaskCapacity(2) + .withSleepDuration(Duration.ofMillis(1)) + .withPool(pool) + .build() + .cast(); + final TaskScheduler c = model.schedulerBuilder("c") + .withConcurrency(false) + .withUnhandledTaskCapacity(2) + .withSleepDuration(Duration.ofMillis(1)) + .withPool(pool) + .build() + .cast(); + + final InputWire channelA = a.buildInputWire("channelA"); + final InputWire channelB = b.buildInputWire("channelB"); + final InputWire channelC = c.buildInputWire("channelC"); + + final CountDownLatch latch = new CountDownLatch(1); + + channelA.bind(channelB::put); + channelB.bind(channelC::put); + channelC.bind(o -> { + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + + // each wire has a capacity of 1, so we can have 1 task waiting on each wire + // insert a task into C, which will start executing and waiting on the latch + channelC.put(Object.class); + // fill up the queues for each wire + channelC.put(Object.class); + channelA.put(Object.class); + channelB.put(Object.class); + + completeBeforeTimeout( + () -> { + // release the latch, that should allow all tasks to complete + latch.countDown(); + // if tasks are completing, none of the wires should block + channelA.put(Object.class); + channelB.put(Object.class); + channelC.put(Object.class); + }, + Duration.ofSeconds(1), + "deadlock"); + + pool.shutdown(); + } + + /** + * Solder together a simple sequence of wires. + */ + @Test + void simpleSolderingTest() { + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").build().cast(); + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("A").build().cast(); + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("A").build().cast(); + final TaskScheduler taskSchedulerD = + model.schedulerBuilder("A").build().cast(); + + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + final InputWire inputC = taskSchedulerC.buildInputWire("inputC"); + final InputWire inputD = taskSchedulerD.buildInputWire("inputD"); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputC); + taskSchedulerC.getOutputWire().solderTo(inputD); + + final AtomicInteger countA = new AtomicInteger(); + final AtomicInteger countB = new AtomicInteger(); + final AtomicInteger countC = new AtomicInteger(); + final AtomicInteger countD = new AtomicInteger(); + + inputA.bind(x -> { + countA.set(hash32(countA.get(), x)); + return x; + }); + + inputB.bind(x -> { + countB.set(hash32(countB.get(), x)); + return x; + }); + + inputC.bind(x -> { + countC.set(hash32(countC.get(), x)); + return x; + }); + + inputD.bind(x -> { + countD.set(hash32(countD.get(), x)); + }); + + int expectedCount = 0; + + for (int i = 0; i < 100; i++) { + inputA.put(i); + expectedCount = hash32(expectedCount, i); + } + + assertEventuallyEquals( + expectedCount, countD::get, Duration.ofSeconds(1), "Wire sum did not match expected sum"); + assertEquals(expectedCount, countA.get()); + assertEquals(expectedCount, countB.get()); + assertEquals(expectedCount, countC.get()); + } + + /** + * Test soldering to a lambda function. + */ + @Test + void lambdaSolderingTest() { + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").build().cast(); + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").build().cast(); + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("C").build().cast(); + final TaskScheduler taskSchedulerD = + model.schedulerBuilder("D").build().cast(); + + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + final InputWire inputC = taskSchedulerC.buildInputWire("inputC"); + final InputWire inputD = taskSchedulerD.buildInputWire("inputD"); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputC); + taskSchedulerC.getOutputWire().solderTo(inputD); + + final AtomicInteger countA = new AtomicInteger(); + final AtomicInteger countB = new AtomicInteger(); + final AtomicInteger countC = new AtomicInteger(); + final AtomicInteger countD = new AtomicInteger(); + + final AtomicInteger lambdaSum = new AtomicInteger(); + taskSchedulerB.getOutputWire().solderTo("lambda", lambdaSum::getAndAdd); + + inputA.bind(x -> { + countA.set(hash32(countA.get(), x)); + return x; + }); + + inputB.bind(x -> { + countB.set(hash32(countB.get(), x)); + return x; + }); + + inputC.bind(x -> { + countC.set(hash32(countC.get(), x)); + return x; + }); + + inputD.bind(x -> { + countD.set(hash32(countD.get(), x)); + }); + + int expectedCount = 0; + + int sum = 0; + for (int i = 0; i < 100; i++) { + inputA.put(i); + expectedCount = hash32(expectedCount, i); + sum += i; + } + + assertEventuallyEquals( + expectedCount, countD::get, Duration.ofSeconds(1), "Wire sum did not match expected sum"); + assertEquals(expectedCount, countA.get()); + assertEquals(expectedCount, countB.get()); + assertEquals(sum, lambdaSum.get()); + assertEquals(expectedCount, countC.get()); + } + + /** + * Solder the output of a wire to the inputs of multiple other wires. + */ + @Test + void multiWireSolderingTest() { + // A passes data to X, Y, and Z + // X, Y, and Z pass data to B + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").build().cast(); + final InputWire addNewValueToA = taskSchedulerA.buildInputWire("addNewValueToA"); + final InputWire setInversionBitInA = taskSchedulerA.buildInputWire("setInversionBitInA"); + + final TaskScheduler taskSchedulerX = + model.schedulerBuilder("X").build().cast(); + final InputWire inputX = taskSchedulerX.buildInputWire("inputX"); + + final TaskScheduler taskSchedulerY = + model.schedulerBuilder("Y").build().cast(); + final InputWire inputY = taskSchedulerY.buildInputWire("inputY"); + + final TaskScheduler taskSchedulerZ = + model.schedulerBuilder("Z").build().cast(); + final InputWire inputZ = taskSchedulerZ.buildInputWire("inputZ"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").build().cast(); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + + taskSchedulerA.getOutputWire().solderTo(inputX); + taskSchedulerA.getOutputWire().solderTo(inputY); + taskSchedulerA.getOutputWire().solderTo(inputZ); + taskSchedulerX.getOutputWire().solderTo(inputB); + taskSchedulerY.getOutputWire().solderTo(inputB); + taskSchedulerZ.getOutputWire().solderTo(inputB); + + final AtomicInteger countA = new AtomicInteger(); + final AtomicBoolean invertA = new AtomicBoolean(); + addNewValueToA.bind(x -> { + final int possiblyInvertedValue = x * (invertA.get() ? -1 : 1); + countA.set(hash32(countA.get(), possiblyInvertedValue)); + return possiblyInvertedValue; + }); + setInversionBitInA.bind(x -> { + invertA.set(x); + return null; + }); + + final AtomicInteger countX = new AtomicInteger(); + inputX.bind(x -> { + countX.set(hash32(countX.get(), x)); + return x; + }); + + final AtomicInteger countY = new AtomicInteger(); + inputY.bind(x -> { + countY.set(hash32(countY.get(), x)); + return x; + }); + + final AtomicInteger countZ = new AtomicInteger(); + inputZ.bind(x -> { + countZ.set(hash32(countZ.get(), x)); + return x; + }); + + final AtomicInteger sumB = new AtomicInteger(); + inputB.bind(x -> { + sumB.getAndAdd(x); + }); + + int expectedCount = 0; + boolean expectedInversionBit = false; + int expectedSum = 0; + + for (int i = 0; i < 100; i++) { + if (i % 7 == 0) { + expectedInversionBit = !expectedInversionBit; + setInversionBitInA.put(expectedInversionBit); + } + addNewValueToA.put(i); + + final int possiblyInvertedValue = i * (expectedInversionBit ? -1 : 1); + + expectedCount = hash32(expectedCount, possiblyInvertedValue); + expectedSum = expectedSum + 3 * possiblyInvertedValue; + } + + assertEventuallyEquals( + expectedSum, + sumB::get, + Duration.ofSeconds(1), + "Wire sum did not match expected sum, " + expectedSum + " vs " + sumB.get()); + assertEquals(expectedCount, countA.get()); + assertEquals(expectedCount, countX.get()); + assertEquals(expectedCount, countY.get()); + assertEquals(expectedCount, countZ.get()); + assertEventuallyEquals( + expectedInversionBit, + invertA::get, + Duration.ofSeconds(1), + "Wire inversion bit did not match expected value"); + } + + /** + * Validate that a wire soldered to another using injection ignores backpressure constraints. + */ + @Test + void injectionSolderingTest() throws InterruptedException { + + // In this test, wires A and B are connected to the input of wire C, which has a maximum capacity. + // Wire A respects back pressure, but wire B uses injection and can ignore it. + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").build().cast(); + final InputWire inA = taskSchedulerA.buildInputWire("inA"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").build().cast(); + final InputWire inB = taskSchedulerB.buildInputWire("inB"); + + final TaskScheduler taskSchedulerC = model.schedulerBuilder("C") + .withUnhandledTaskCapacity(10) + .build() + .cast(); + final InputWire inC = taskSchedulerC.buildInputWire("inC"); + + taskSchedulerA.getOutputWire().solderTo(inC); // respects capacity + taskSchedulerB.getOutputWire().solderTo(inC, true); // ignores capacity + + final AtomicInteger countA = new AtomicInteger(); + inA.bind(x -> { + countA.set(hash32(countA.get(), x)); + return x; + }); + + final AtomicInteger countB = new AtomicInteger(); + inB.bind(x -> { + countB.set(hash32(countB.get(), x)); + return x; + }); + + final AtomicInteger sumC = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(1); + inC.bind(x -> { + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + sumC.getAndAdd(x); + }); + + // Add 5 elements to A and B. This will completely fill C's capacity. + int expectedCount = 0; + int expectedSum = 0; + for (int i = 0; i < 5; i++) { + inA.put(i); + inB.put(i); + expectedCount = hash32(expectedCount, i); + expectedSum += 2 * i; + } + + // Eventually, C should have 10 things that have not yet been fully processed. + assertEventuallyEquals( + 10L, + taskSchedulerC::getUnprocessedTaskCount, + Duration.ofSeconds(1), + "C should have 10 unprocessed tasks, currently has " + taskSchedulerC.getUnprocessedTaskCount()); + + assertEquals(expectedCount, countA.get()); + assertEquals(expectedCount, countB.get()); + + // Push some more data into A and B. A will get stuck trying to push it to C. + inA.put(5); + inB.put(5); + expectedCount = hash32(expectedCount, 5); + expectedSum += 2 * 5; + + assertEventuallyEquals(expectedCount, countA::get, Duration.ofSeconds(1), "A should have processed task"); + assertEventuallyEquals(expectedCount, countB::get, Duration.ofSeconds(1), "B should have processed task"); + + // If we wait some time, the task from B should have increased C's count to 11, but the task from A + // should have been unable to increase C's count. + MILLISECONDS.sleep(50); + assertEquals(11, taskSchedulerC.getUnprocessedTaskCount()); + + // Push some more data into A and B. A will be unable to process it because it's still + // stuck pushing the previous value. + inA.put(6); + inB.put(6); + final int expectedCountAfterHandling6 = hash32(expectedCount, 6); + expectedSum += 2 * 6; + + assertEventuallyEquals( + expectedCountAfterHandling6, countB::get, Duration.ofSeconds(1), "B should have processed task"); + + // Even if we wait, A should not have been able to process the task. + MILLISECONDS.sleep(50); + assertEquals(expectedCount, countA.get()); + assertEquals(12, taskSchedulerC.getUnprocessedTaskCount()); + + // Releasing the latch should allow data to flow through C. + latch.countDown(); + assertEventuallyEquals(expectedSum, sumC::get, Duration.ofSeconds(1), "C should have processed all tasks"); + assertEquals(expectedCountAfterHandling6, countA.get()); + } + + /** + * When a handler returns null, the wire should not forward the null value to the next wire. + */ + @Test + void squelchNullValuesInWiresTest() { + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").build().cast(); + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("A").build().cast(); + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("A").build().cast(); + final TaskScheduler taskSchedulerD = + model.schedulerBuilder("A").build().cast(); + + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + final InputWire inputC = taskSchedulerC.buildInputWire("inputC"); + final InputWire inputD = taskSchedulerD.buildInputWire("inputD"); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputC); + taskSchedulerC.getOutputWire().solderTo(inputD); + + final AtomicInteger countA = new AtomicInteger(); + final AtomicInteger countB = new AtomicInteger(); + final AtomicInteger countC = new AtomicInteger(); + final AtomicInteger countD = new AtomicInteger(); + + inputA.bind(x -> { + countA.set(hash32(countA.get(), x)); + if (x % 3 == 0) { + return null; + } + return x; + }); + + inputB.bind(x -> { + countB.set(hash32(countB.get(), x)); + if (x % 5 == 0) { + return null; + } + return x; + }); + + inputC.bind(x -> { + countC.set(hash32(countC.get(), x)); + if (x % 7 == 0) { + return null; + } + return x; + }); + + inputD.bind(x -> { + countD.set(hash32(countD.get(), x)); + }); + + int expectedCountA = 0; + int expectedCountB = 0; + int expectedCountC = 0; + int expectedCountD = 0; + + for (int i = 0; i < 100; i++) { + inputA.put(i); + expectedCountA = hash32(expectedCountA, i); + if (i % 3 == 0) { + continue; + } + expectedCountB = hash32(expectedCountB, i); + if (i % 5 == 0) { + continue; + } + expectedCountC = hash32(expectedCountC, i); + if (i % 7 == 0) { + continue; + } + expectedCountD = hash32(expectedCountD, i); + } + + assertEventuallyEquals( + expectedCountD, countD::get, Duration.ofSeconds(1), "Wire sum did not match expected sum"); + assertEquals(expectedCountA, countA.get()); + assertEquals(expectedCountB, countB.get()); + assertEquals(expectedCountC, countC.get()); + } + + /** + * Make sure we don't crash when metrics are enabled. Might be nice to eventually validate the metrics, but right + * now the metrics framework makes it complex to do so. + */ + @Test + void metricsEnabledTest() { + final TaskScheduler taskSchedulerA = model.schedulerBuilder("A") + .withMetricsBuilder(model.metricsBuilder() + .withBusyFractionMetricsEnabled(true) + .withUnhandledTaskMetricEnabled(true)) + .build() + .cast(); + final TaskScheduler taskSchedulerB = model.schedulerBuilder("B") + .withMetricsBuilder(model.metricsBuilder() + .withBusyFractionMetricsEnabled(true) + .withUnhandledTaskMetricEnabled(false)) + .build() + .cast(); + final TaskScheduler taskSchedulerC = model.schedulerBuilder("C") + .withMetricsBuilder(model.metricsBuilder() + .withBusyFractionMetricsEnabled(false) + .withUnhandledTaskMetricEnabled(true)) + .build() + .cast(); + final TaskScheduler taskSchedulerD = model.schedulerBuilder("D") + .withMetricsBuilder(model.metricsBuilder() + .withBusyFractionMetricsEnabled(false) + .withUnhandledTaskMetricEnabled(false)) + .build() + .cast(); + + final InputWire inputA = taskSchedulerA.buildInputWire("inputA"); + final InputWire inputB = taskSchedulerB.buildInputWire("inputB"); + final InputWire inputC = taskSchedulerC.buildInputWire("inputC"); + final InputWire inputD = taskSchedulerD.buildInputWire("inputD"); + + taskSchedulerA.getOutputWire().solderTo(inputB); + taskSchedulerB.getOutputWire().solderTo(inputC); + taskSchedulerC.getOutputWire().solderTo(inputD); + + final AtomicInteger countA = new AtomicInteger(); + final AtomicInteger countB = new AtomicInteger(); + final AtomicInteger countC = new AtomicInteger(); + final AtomicInteger countD = new AtomicInteger(); + + inputA.bind(x -> { + countA.set(hash32(countA.get(), x)); + return x; + }); + + inputB.bind(x -> { + countB.set(hash32(countB.get(), x)); + return x; + }); + + inputC.bind(x -> { + countC.set(hash32(countC.get(), x)); + return x; + }); + + inputD.bind(x -> { + countD.set(hash32(countD.get(), x)); + }); + + int expectedCount = 0; + + for (int i = 0; i < 100; i++) { + inputA.put(i); + expectedCount = hash32(expectedCount, i); + } + + assertEventuallyEquals( + expectedCount, countD::get, Duration.ofSeconds(1), "Wire sum did not match expected sum"); + assertEquals(expectedCount, countA.get()); + assertEquals(expectedCount, countB.get()); + assertEquals(expectedCount, countC.get()); + } + + @Test + void multipleOutputChannelsTest() { + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").build().cast(); + final InputWire aIn = taskSchedulerA.buildInputWire("aIn"); + final OutputWire aOutBoolean = taskSchedulerA.buildSecondaryOutputWire(); + final OutputWire aOutString = taskSchedulerA.buildSecondaryOutputWire(); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("A").build().cast(); + final InputWire bInInteger = taskSchedulerB.buildInputWire("bIn1"); + final InputWire bInBoolean = taskSchedulerB.buildInputWire("bIn2"); + final InputWire bInString = taskSchedulerB.buildInputWire("bIn3"); + + taskSchedulerA.getOutputWire().solderTo(bInInteger); + aOutBoolean.solderTo(bInBoolean); + aOutString.solderTo(bInString); + + aIn.bind(x -> { + if (x % 2 == 0) { + aOutBoolean.forward(x % 3 == 0); + } + + if (x % 5 == 0) { + aOutString.forward(Integer.toString(x)); + } + + return x; + }); + + final AtomicInteger count = new AtomicInteger(); + bInBoolean.bind(x -> { + count.set(hash32(count.get(), x ? 1 : 0)); + }); + bInString.bind(x -> { + count.set(hash32(count.get(), x.hashCode())); + }); + bInInteger.bind(x -> { + count.set(hash32(count.get(), x)); + }); + + int expectedCount = 0; + for (int i = 0; i < 100; i++) { + aIn.put(i); + if (i % 2 == 0) { + expectedCount = hash32(expectedCount, i % 3 == 0 ? 1 : 0); + } + if (i % 5 == 0) { + expectedCount = hash32(expectedCount, Integer.toString(i).hashCode()); + } + expectedCount = hash32(expectedCount, i); + } + + assertEventuallyEquals(expectedCount, count::get, Duration.ofSeconds(1), "Wire count did not match expected"); + } + + @Test + void externalBackPressureTest() throws InterruptedException { + + // There are three components, A, B, and C. + // We want to control the number of elements in all three, not individually. + + final ObjectCounter counter = new BackpressureObjectCounter("test", 10, Duration.ofMillis(1)); + + final TaskScheduler taskSchedulerA = model.schedulerBuilder("A") + .withOnRamp(counter) + .withExternalBackPressure(true) + .build() + .cast(); + final InputWire aIn = taskSchedulerA.buildInputWire("aIn"); + + final TaskScheduler taskSchedulerB = model.schedulerBuilder("B") + .withExternalBackPressure(true) + .build() + .cast(); + final InputWire bIn = taskSchedulerB.buildInputWire("bIn"); + + final TaskScheduler taskSchedulerC = model.schedulerBuilder("C") + .withOffRamp(counter) + .withExternalBackPressure(true) + .build() + .cast(); + final InputWire cIn = taskSchedulerC.buildInputWire("cIn"); + + taskSchedulerA.getOutputWire().solderTo(bIn); + taskSchedulerB.getOutputWire().solderTo(cIn); + + final AtomicInteger countA = new AtomicInteger(); + final CountDownLatch latchA = new CountDownLatch(1); + aIn.bind(x -> { + try { + latchA.await(); + } catch (final InterruptedException e) { + throw new RuntimeException(e); + } + countA.set(hash32(countA.get(), x)); + return x; + }); + + final AtomicInteger countB = new AtomicInteger(); + final CountDownLatch latchB = new CountDownLatch(1); + bIn.bind(x -> { + try { + latchB.await(); + } catch (final InterruptedException e) { + throw new RuntimeException(e); + } + countB.set(hash32(countB.get(), x)); + return x; + }); + + final AtomicInteger countC = new AtomicInteger(); + final CountDownLatch latchC = new CountDownLatch(1); + cIn.bind(x -> { + try { + latchC.await(); + } catch (final InterruptedException e) { + throw new RuntimeException(e); + } + countC.set(hash32(countC.get(), x)); + }); + + // Add enough data to fill all available capacity. + int expectedCount = 0; + for (int i = 0; i < 10; i++) { + aIn.put(i); + expectedCount = hash32(expectedCount, i); + } + + final AtomicBoolean moreWorkInserted = new AtomicBoolean(false); + new ThreadConfiguration(getStaticThreadManager()) + .setRunnable(() -> { + aIn.put(10); + moreWorkInserted.set(true); + }) + .build(true); + expectedCount = hash32(expectedCount, 10); + + assertEquals(10, counter.getCount()); + + // Work is currently stuck at A. No matter how much time passes, no new work should be added. + MILLISECONDS.sleep(50); + assertFalse(moreWorkInserted.get()); + assertEquals(10, counter.getCount()); + + // Unblock A. Work will flow forward and get blocked at B. No matter how much time passes, no new work should + // be added. + latchA.countDown(); + MILLISECONDS.sleep(50); + assertFalse(moreWorkInserted.get()); + assertEquals(10, counter.getCount()); + + // Unblock B. Work will flow forward and get blocked at C. No matter how much time passes, no new work should + // be added. + latchB.countDown(); + MILLISECONDS.sleep(50); + assertFalse(moreWorkInserted.get()); + + // Unblock C. Entire pipeline is now unblocked and new things will be added. + latchC.countDown(); + assertEventuallyEquals(0L, counter::getCount, Duration.ofSeconds(1), "Counter should be empty"); + assertEventuallyEquals(expectedCount, countA::get, Duration.ofSeconds(1), "A should have processed task"); + assertEventuallyEquals(expectedCount, countB::get, Duration.ofSeconds(1), "B should have processed task"); + assertEventuallyEquals(expectedCount, countC::get, Duration.ofSeconds(1), "C should have processed task"); + assertTrue(moreWorkInserted.get()); + } + + @Test + void multipleCountersInternalBackpressureTest() throws InterruptedException { + + // There are three components, A, B, and C. + // The pipeline as a whole has a capacity of 10. Each step individually has a capacity of 5; + + final ObjectCounter counter = new BackpressureObjectCounter("test", 10, Duration.ofMillis(1)); + + final TaskScheduler taskSchedulerA = model.schedulerBuilder("A") + .withOnRamp(counter) + .withExternalBackPressure(true) + .withUnhandledTaskCapacity(5) + .build() + .cast(); + final InputWire aIn = taskSchedulerA.buildInputWire("aIn"); + + final TaskScheduler taskSchedulerB = model.schedulerBuilder("B") + .withExternalBackPressure(true) + .withUnhandledTaskCapacity(5) + .build() + .cast(); + final InputWire bIn = taskSchedulerB.buildInputWire("bIn"); + + final TaskScheduler taskSchedulerC = model.schedulerBuilder("C") + .withOffRamp(counter) + .withExternalBackPressure(true) + .withUnhandledTaskCapacity(5) + .build() + .cast(); + final InputWire cIn = taskSchedulerC.buildInputWire("cIn"); + + taskSchedulerA.getOutputWire().solderTo(bIn); + taskSchedulerB.getOutputWire().solderTo(cIn); + + final AtomicInteger countA = new AtomicInteger(); + final CountDownLatch latchA = new CountDownLatch(1); + aIn.bind(x -> { + try { + latchA.await(); + } catch (final InterruptedException e) { + throw new RuntimeException(e); + } + countA.set(hash32(countA.get(), x)); + return x; + }); + + final AtomicInteger countB = new AtomicInteger(); + final CountDownLatch latchB = new CountDownLatch(1); + bIn.bind(x -> { + try { + latchB.await(); + } catch (final InterruptedException e) { + throw new RuntimeException(e); + } + countB.set(hash32(countB.get(), x)); + return x; + }); + + final AtomicInteger countC = new AtomicInteger(); + final CountDownLatch latchC = new CountDownLatch(1); + cIn.bind(x -> { + try { + latchC.await(); + } catch (final InterruptedException e) { + throw new RuntimeException(e); + } + countC.set(hash32(countC.get(), x)); + }); + + int expectedCount = 0; + for (int i = 0; i < 11; i++) { + expectedCount = hash32(expectedCount, i); + } + + // This thread wants to add 11 things to the pipeline. + final AtomicBoolean allWOrkInserted = new AtomicBoolean(false); + new ThreadConfiguration(getStaticThreadManager()) + .setRunnable(() -> { + for (int i = 0; i < 11; i++) { + aIn.put(i); + } + allWOrkInserted.set(true); + }) + .build(true); + + // Work is currently stuck at A. No matter how much time passes, we should not be able to exceed A's capacity. + MILLISECONDS.sleep(50); + assertFalse(allWOrkInserted.get()); + assertEquals(5, counter.getCount()); + + // Unblock A. Work will flow forward and get blocked at B. A can fit 5 items, B can fit another 5. + latchA.countDown(); + MILLISECONDS.sleep(50); + assertFalse(allWOrkInserted.get()); + assertEquals(10, counter.getCount()); + + // Unblock B. Work will flow forward and get blocked at C. We shouldn't be able to add additional items + // since that would violate the global capacity. + latchB.countDown(); + MILLISECONDS.sleep(50); + assertFalse(allWOrkInserted.get()); + assertEquals(10, counter.getCount()); + + // Unblock C. Entire pipeline is now unblocked and new things will be added. + latchC.countDown(); + assertEventuallyEquals(0L, counter::getCount, Duration.ofSeconds(1), "Counter should be empty"); + assertEquals(expectedCount, countA.get()); + assertEquals(expectedCount, countB.get()); + assertEquals(expectedCount, countC.get()); + assertTrue(allWOrkInserted.get()); + } +} diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/transformers/TaskSchedulerTransformersTests.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/transformers/TaskSchedulerTransformersTests.java new file mode 100644 index 000000000000..c10f5c1b73fa --- /dev/null +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/transformers/TaskSchedulerTransformersTests.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.common.wiring.transformers; + +import static com.swirlds.common.test.fixtures.AssertionUtils.assertEventuallyEquals; +import static com.swirlds.common.utility.NonCryptographicHashing.hash32; + +import com.swirlds.common.wiring.InputWire; +import com.swirlds.common.wiring.OutputWire; +import com.swirlds.common.wiring.TaskScheduler; +import com.swirlds.common.wiring.WiringModel; +import com.swirlds.test.framework.TestWiringModel; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +class TaskSchedulerTransformersTests { + + private static final WiringModel model = TestWiringModel.getInstance(); + + @Test + void wireListSplitterTest() { + + // Component A produces lists of integers. It passes data to B, C, and D. + // Components B and C want individual integers. Component D wants the full list of integers. + + final TaskScheduler> taskSchedulerA = + model.schedulerBuilder("A").build().cast(); + final InputWire> wireAIn = taskSchedulerA.buildInputWire("A in"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").build().cast(); + final InputWire wireBIn = taskSchedulerB.buildInputWire("B in"); + + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("C").build().cast(); + final InputWire wireCIn = taskSchedulerC.buildInputWire("C in"); + + final TaskScheduler taskSchedulerD = + model.schedulerBuilder("D").build().cast(); + final InputWire, Void> wireDIn = taskSchedulerD.buildInputWire("D in"); + + final OutputWire splitter = taskSchedulerA.getOutputWire().buildSplitter(); + splitter.solderTo(wireBIn); + splitter.solderTo(wireCIn); + taskSchedulerA.getOutputWire().solderTo(wireDIn); + + wireAIn.bind(x -> { + return List.of(x, x, x); + }); + + final AtomicInteger countB = new AtomicInteger(0); + wireBIn.bind(x -> { + countB.set(hash32(countB.get(), x)); + }); + + final AtomicInteger countC = new AtomicInteger(0); + wireCIn.bind(x -> { + countC.set(hash32(countC.get(), -x)); + }); + + final AtomicInteger countD = new AtomicInteger(0); + wireDIn.bind(x -> { + int product = 1; + for (final int i : x) { + product *= i; + } + countD.set(hash32(countD.get(), product)); + }); + + int expectedCountB = 0; + int expectedCountC = 0; + int expectedCountD = 0; + + for (int i = 0; i < 100; i++) { + wireAIn.put(i); + + for (int j = 0; j < 3; j++) { + expectedCountB = hash32(expectedCountB, i); + expectedCountC = hash32(expectedCountC, -i); + } + + expectedCountD = hash32(expectedCountD, i * i * i); + } + + assertEventuallyEquals(expectedCountB, countB::get, Duration.ofSeconds(1), "B did not receive all data"); + assertEventuallyEquals(expectedCountC, countC::get, Duration.ofSeconds(1), "C did not receive all data"); + assertEventuallyEquals(expectedCountD, countD::get, Duration.ofSeconds(1), "D did not receive all data"); + } + + @Test + void wireFilterTest() { + + // Wire A passes data to B, C, and a lambda. + // B wants all of A's data, but C and the lambda only want even values. + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").build().cast(); + final InputWire inA = taskSchedulerA.buildInputWire("A in"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").build().cast(); + final InputWire inB = taskSchedulerB.buildInputWire("B in"); + + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("C").build().cast(); + final InputWire inC = taskSchedulerC.buildInputWire("C in"); + + final AtomicInteger countA = new AtomicInteger(0); + final AtomicInteger countB = new AtomicInteger(0); + final AtomicInteger countC = new AtomicInteger(0); + final AtomicInteger countLambda = new AtomicInteger(0); + + taskSchedulerA.getOutputWire().solderTo(inB); + final OutputWire filter = taskSchedulerA.getOutputWire().buildFilter("onlyEven", x -> x % 2 == 0); + filter.solderTo(inC); + filter.solderTo("lambda", x -> countLambda.set(hash32(countLambda.get(), x))); + + inA.bind(x -> { + countA.set(hash32(countA.get(), x)); + return x; + }); + + inB.bind(x -> { + countB.set(hash32(countB.get(), x)); + }); + + inC.bind(x -> { + countC.set(hash32(countC.get(), x)); + }); + + int expectedCount = 0; + int expectedEvenCount = 0; + + for (int i = 0; i < 100; i++) { + inA.put(i); + expectedCount = hash32(expectedCount, i); + if (i % 2 == 0) { + expectedEvenCount = hash32(expectedEvenCount, i); + } + } + + assertEventuallyEquals(expectedCount, countA::get, Duration.ofSeconds(1), "A did not receive all data"); + assertEventuallyEquals(expectedCount, countB::get, Duration.ofSeconds(1), "B did not receive all data"); + assertEventuallyEquals(expectedEvenCount, countC::get, Duration.ofSeconds(1), "C did not receive all data"); + assertEventuallyEquals( + expectedEvenCount, countLambda::get, Duration.ofSeconds(1), "Lambda did not receive all data"); + } + + private record TestData(int value, boolean invert) {} + + @Test + void wireTransformerTest() { + + // A produces data of type TestData. + // B wants all of A's data, C wants the integer values, and D wants the boolean values. + + final TaskScheduler taskSchedulerA = + model.schedulerBuilder("A").build().cast(); + final InputWire inA = taskSchedulerA.buildInputWire("A in"); + + final TaskScheduler taskSchedulerB = + model.schedulerBuilder("B").build().cast(); + final InputWire inB = taskSchedulerB.buildInputWire("B in"); + + final TaskScheduler taskSchedulerC = + model.schedulerBuilder("C").build().cast(); + final InputWire inC = taskSchedulerC.buildInputWire("C in"); + + final TaskScheduler taskSchedulerD = + model.schedulerBuilder("D").build().cast(); + final InputWire inD = taskSchedulerD.buildInputWire("D in"); + + taskSchedulerA.getOutputWire().solderTo(inB); + taskSchedulerA + .getOutputWire() + .buildTransformer("getValue", TestData::value) + .solderTo(inC); + taskSchedulerA + .getOutputWire() + .buildTransformer("getInvert", TestData::invert) + .solderTo(inD); + + final AtomicInteger countA = new AtomicInteger(0); + inA.bind(x -> { + final int invert = x.invert() ? -1 : 1; + countA.set(hash32(countA.get(), x.value() * invert)); + return x; + }); + + final AtomicInteger countB = new AtomicInteger(0); + inB.bind(x -> { + final int invert = x.invert() ? -1 : 1; + countB.set(hash32(countB.get(), x.value() * invert)); + }); + + final AtomicInteger countC = new AtomicInteger(0); + inC.bind(x -> { + countC.set(hash32(countC.get(), x)); + }); + + final AtomicInteger countD = new AtomicInteger(0); + inD.bind(x -> { + countD.set(hash32(countD.get(), x ? 1 : 0)); + }); + + int expectedCountAB = 0; + int expectedCountC = 0; + int expectedCountD = 0; + + for (int i = 0; i < 100; i++) { + final boolean invert = i % 3 == 0; + inA.put(new TestData(i, invert)); + + expectedCountAB = hash32(expectedCountAB, i * (invert ? -1 : 1)); + expectedCountC = hash32(expectedCountC, i); + expectedCountD = hash32(expectedCountD, invert ? 1 : 0); + } + + assertEventuallyEquals(expectedCountAB, countA::get, Duration.ofSeconds(1), "A did not receive all data"); + assertEventuallyEquals(expectedCountAB, countB::get, Duration.ofSeconds(1), "B did not receive all data"); + assertEventuallyEquals(expectedCountC, countC::get, Duration.ofSeconds(1), "C did not receive all data"); + assertEventuallyEquals(expectedCountD, countD::get, Duration.ofSeconds(1), "D did not receive all data"); + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/IssHandler.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/IssHandler.java index fd583dc7ab03..fc1a97def14b 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/IssHandler.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/IssHandler.java @@ -147,7 +147,6 @@ public void stateHashValidityObserver( } } - // TODO test /** * Record the latest ISS round in the scratchpad. Does nothing if this is not the latest ISS that has been * observed. diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/EventSignatureValidationScheduler.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/EventSignatureValidationScheduler.java new file mode 100644 index 000000000000..61e358325d08 --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/EventSignatureValidationScheduler.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.platform.wiring; + +import com.swirlds.common.wiring.InputWire; +import com.swirlds.common.wiring.OutputWire; +import com.swirlds.common.wiring.TaskScheduler; +import com.swirlds.common.wiring.WiringModel; +import com.swirlds.platform.event.GossipEvent; +import com.swirlds.platform.event.validation.EventValidator; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Wiring for the event signature validator. + */ +public class EventSignatureValidationScheduler { + + private final TaskScheduler taskScheduler; + + private final InputWire eventInput; + private final InputWire minimumGenerationNonAncientInput; + + /** + * Constructor. + * + * @param model the wiring model + */ + public EventSignatureValidationScheduler(@NonNull final WiringModel model) { + taskScheduler = model.schedulerBuilder("eventSignatureValidator") + .withConcurrency(false) + .withUnhandledTaskCapacity(500) + .withFlushingEnabled(true) + .withMetricsBuilder(model.metricsBuilder().withUnhandledTaskMetricEnabled(true)) + .build() + .cast(); + + eventInput = taskScheduler.buildInputWire("unvalidated events"); + minimumGenerationNonAncientInput = taskScheduler.buildInputWire("minimum generation non ancient"); + } + + /** + * Passes events to the signature validator. + * + * @return the event input channel + */ + @NonNull + public InputWire getEventInput() { + return eventInput; + } + + /** + * Passes the minimum generation non ancient to the signature validator. + * + * @return the minimum generation non ancient input channel + */ + @NonNull + public InputWire getMinimumGenerationNonAncientInput() { + return minimumGenerationNonAncientInput; + } + + /** + * Get the output of the signature validator, i.e. a stream of events with valid signatures. + * + * @return the event output channel + */ + @NonNull + public OutputWire getEventOutput() { + return taskScheduler.getOutputWire(); + } + + /** + * Bind an orphan buffer to this wiring. + * + * @param eventValidator the orphan buffer to bind + */ + public void bind(@NonNull final EventValidator eventValidator) { + // Future work: + // - ensure that the signature validator passed in is the new implementation. + // - Bind the input channels to the appropriate functions. + // - Ensure that functions return a value instead of passing it to an internal lambda. + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/OrphanBufferScheduler.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/OrphanBufferScheduler.java new file mode 100644 index 000000000000..b26d16605035 --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/OrphanBufferScheduler.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.platform.wiring; + +import com.swirlds.common.wiring.InputWire; +import com.swirlds.common.wiring.OutputWire; +import com.swirlds.common.wiring.TaskScheduler; +import com.swirlds.common.wiring.WiringModel; +import com.swirlds.platform.event.GossipEvent; +import com.swirlds.platform.event.orphan.OrphanBuffer; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; + +// Future work: it may actually make more sense to colocate the scheduler classes with the implementations. +// This decision can be delayed until we begin migration in earnest. + +/** + * Wiring for the {@link OrphanBuffer}. + */ +public class OrphanBufferScheduler { + + private final InputWire> eventInput; + private final InputWire> minimumGenerationNonAncientInput; + + private final OutputWire eventOutput; + + /** + * Constructor. + * + * @param model the wiring model + */ + public OrphanBufferScheduler(@NonNull final WiringModel model) { + final TaskScheduler> taskScheduler = model.schedulerBuilder("orphanBuffer") + .withConcurrency(false) + .withUnhandledTaskCapacity(500) + .withFlushingEnabled(true) + .withMetricsBuilder(model.metricsBuilder().withUnhandledTaskMetricEnabled(true)) + .build() + .cast(); + + eventInput = taskScheduler.buildInputWire("unordered events"); + minimumGenerationNonAncientInput = taskScheduler.buildInputWire("minimum generation non ancient"); + + eventOutput = taskScheduler.getOutputWire().buildSplitter(); + } + + /** + * Passes events to the orphan buffer. + * + * @return the event input channel + */ + @NonNull + public InputWire> getEventInput() { + return eventInput; + } + + /** + * Passes the minimum generation non ancient to the orphan buffer. + * + * @return the minimum generation non ancient input channel + */ + @NonNull + public InputWire> getMinimumGenerationNonAncientInput() { + return minimumGenerationNonAncientInput; + } + + /** + * Get the output of the orphan buffer, i.e. a stream of events in topological order. + * + * @return the event output channel + */ + @NonNull + public OutputWire getEventOutput() { + return eventOutput; + } + + /** + * Bind an orphan buffer to this wiring. + * + * @param orphanBuffer the orphan buffer to bind + */ + public void bind(@NonNull final OrphanBuffer orphanBuffer) { + // Future work: these handlers currently do not return anything. They need to be refactored so that + // they return a list of events (as opposed to passing them to a handler lambda). + + eventInput.bind(orphanBuffer::handleEvent); + minimumGenerationNonAncientInput.bind(orphanBuffer::setMinimumGenerationNonAncient); + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java new file mode 100644 index 000000000000..83480b0d4306 --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.platform.wiring; + +import com.swirlds.base.time.Time; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.wiring.WiringModel; +import com.swirlds.platform.event.orphan.OrphanBuffer; +import com.swirlds.platform.event.validation.EventValidator; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Encapsulates wiring for {@link com.swirlds.platform.SwirldsPlatform}. + */ +public class PlatformWiring { + + // Note: this class is currently a placeholder; it's not functional in its current form. + // As we migrate the platform into the new framework, we should expand this class. + + private final WiringModel model; + + private final EventSignatureValidationScheduler eventSignatureValidationScheduler; + private final OrphanBufferScheduler orphanBufferScheduler; + private final boolean cyclicalBackpressurePresent; + + /** + * Constructor. + * + * @param platformContext the platform context + * @param time provides wall clock time + */ + public PlatformWiring(@NonNull final PlatformContext platformContext, @NonNull final Time time) { + model = WiringModel.create(platformContext, time); + + orphanBufferScheduler = new OrphanBufferScheduler(model); + eventSignatureValidationScheduler = new EventSignatureValidationScheduler(model); + + wire(); + + // Logs if there is cyclical back pressure. + // Do not throw -- in theory we might survive this, so no need to crash. + cyclicalBackpressurePresent = model.checkForCyclicalBackpressure(); + } + + /** + * Get the wiring model. + * + * @return the wiring model + */ + @NonNull + public WiringModel getModel() { + return model; + } + + /** + * Check if cyclical backpressure is present in the model. + * + * @return true if cyclical backpressure is present, false otherwise + */ + public boolean isCyclicalBackpressurePresent() { + return cyclicalBackpressurePresent; + } + + /** + * Wire the components together. + */ + private void wire() { + eventSignatureValidationScheduler.getEventOutput().solderTo(orphanBufferScheduler.getEventInput()); + // FUTURE WORK: solder all the things! + } + + /** + * Bind an orphan buffer to this wiring. + * + * @param orphanBuffer the orphan buffer to bind + */ + public void bind(@NonNull final EventValidator eventValidator, @NonNull final OrphanBuffer orphanBuffer) { + eventSignatureValidationScheduler.bind(eventValidator); + orphanBufferScheduler.bind(orphanBuffer); + } +} diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/wiring/WiringTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/wiring/WiringTests.java new file mode 100644 index 000000000000..f079a5e6a3cd --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/wiring/WiringTests.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.platform.wiring; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import com.swirlds.base.time.Time; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.test.framework.context.TestPlatformContextBuilder; +import org.junit.jupiter.api.Test; + +class WiringTests { + + @Test + void cyclicalBackpressureTest() { + final PlatformContext context = TestPlatformContextBuilder.create().build(); + final PlatformWiring wiring = new PlatformWiring(context, Time.getCurrent()); + + assertFalse(wiring.isCyclicalBackpressurePresent(), "cyclical back pressure detected"); + } +} diff --git a/platform-sdk/swirlds-unit-tests/common/swirlds-common-test/src/test/java/com/swirlds/common/test/metrics/extensions/FractionalTimerTests.java b/platform-sdk/swirlds-unit-tests/common/swirlds-common-test/src/test/java/com/swirlds/common/test/metrics/extensions/FractionalTimerTests.java index 39bf0d432626..995eee3193a9 100644 --- a/platform-sdk/swirlds-unit-tests/common/swirlds-common-test/src/test/java/com/swirlds/common/test/metrics/extensions/FractionalTimerTests.java +++ b/platform-sdk/swirlds-unit-tests/common/swirlds-common-test/src/test/java/com/swirlds/common/test/metrics/extensions/FractionalTimerTests.java @@ -20,6 +20,8 @@ import com.swirlds.base.test.fixtures.time.FakeTime; import com.swirlds.common.metrics.extensions.FractionalTimer; +import com.swirlds.common.metrics.extensions.NoOpFractionalTimer; +import com.swirlds.common.metrics.extensions.StandardFractionalTimer; import java.time.Duration; import java.time.Instant; import org.junit.jupiter.api.BeforeEach; @@ -34,7 +36,7 @@ class FractionalTimerTests { @BeforeEach void reset() { clock.reset(); - metric = new FractionalTimer(clock); + metric = new StandardFractionalTimer(clock); } /** @@ -158,4 +160,16 @@ void metricNotReset() { metric.getAndReset(), "after the reset the metric should start tracking again, so 0.5 is expected"); } + + @Test + void noOpTest() { + final FractionalTimer timer = NoOpFractionalTimer.getInstance(); + timer.registerMetric(null, null, null, null); + timer.activate(); + timer.activate(1234); + timer.deactivate(1234); + timer.deactivate(); + assertEquals(0, timer.getActiveFraction()); + assertEquals(0, timer.getAndReset()); + } } diff --git a/platform-sdk/swirlds-unit-tests/common/swirlds-test-framework/src/main/java/com/swirlds/test/framework/TestWiringModel.java b/platform-sdk/swirlds-unit-tests/common/swirlds-test-framework/src/main/java/com/swirlds/test/framework/TestWiringModel.java new file mode 100644 index 000000000000..e664c1d7e6ac --- /dev/null +++ b/platform-sdk/swirlds-unit-tests/common/swirlds-test-framework/src/main/java/com/swirlds/test/framework/TestWiringModel.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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.swirlds.test.framework; + +import com.swirlds.base.time.Time; +import com.swirlds.common.wiring.ModelGroup; +import com.swirlds.common.wiring.WiringModel; +import com.swirlds.test.framework.context.TestPlatformContextBuilder; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Set; + +/** + * A simple version of a wiring model for scenarios where the wiring model is not needed. + */ +public class TestWiringModel extends WiringModel { + + private static final TestWiringModel INSTANCE = new TestWiringModel(); + + /** + * Get the singleton instance. + * + * @return the singleton instance + */ + @NonNull + public static TestWiringModel getInstance() { + return INSTANCE; + } + + /** + * Constructor. + */ + private TestWiringModel() { + super(TestPlatformContextBuilder.create().build(), Time.getCurrent()); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean checkForCyclicalBackpressure() { + return false; + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public String generateWiringDiagram(@NonNull final Set modelGroups) { + return "do it yourself"; + } + + /** + * {@inheritDoc} + */ + @Override + public void registerVertex(@NonNull final String vertexName, final boolean insertionIsBlocking) {} + + /** + * {@inheritDoc} + */ + @Override + public void registerEdge( + @NonNull final String originVertex, + @NonNull final String destinationVertex, + @NonNull final String label, + final boolean injection) {} +} diff --git a/platform-sdk/swirlds-unit-tests/common/swirlds-test-framework/src/main/java/module-info.java b/platform-sdk/swirlds-unit-tests/common/swirlds-test-framework/src/main/java/module-info.java index 71f96982f8ce..8f36c04a5445 100644 --- a/platform-sdk/swirlds-unit-tests/common/swirlds-test-framework/src/main/java/module-info.java +++ b/platform-sdk/swirlds-unit-tests/common/swirlds-test-framework/src/main/java/module-info.java @@ -3,6 +3,7 @@ exports com.swirlds.test.framework.context; exports com.swirlds.test.framework.config; + requires com.swirlds.base; requires transitive com.swirlds.common; requires transitive com.swirlds.config.api; requires org.apache.logging.log4j.core; diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/framework/validation/ConsensusRoundValidation.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/framework/validation/ConsensusRoundValidation.java index d935823034da..6e467475f261 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/framework/validation/ConsensusRoundValidation.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/framework/validation/ConsensusRoundValidation.java @@ -133,7 +133,6 @@ private static void assertConsensusEvents(final String description, final EventI */ public static void printGranularEventListComparison( final List events1, final List events2) { - // TODO add this as a debug tool final int maxIndex = Math.min(events1.size(), events2.size()); diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/GossipEventBuilder.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/GossipEventBuilder.java index b9cc4f7330df..39b3ab5f5be3 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/GossipEventBuilder.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/GossipEventBuilder.java @@ -180,7 +180,7 @@ public GossipEvent buildGossipEvent() { ? fakeGeneration - 1 : getOtherParentGossip() != null ? getOtherParentGossip().getGeneration() : -1; final BaseEventHashedData hashedData = new BaseEventHashedData( - new BasicSoftwareVersion(1), // TODO use constant + new BasicSoftwareVersion(1), creatorId, selfParentGen, otherParentGen,