> 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:
+ *
+ * Unscheduled: the task has not been passed to the scheduler yet (e.g. via {@link InputWire#put(Object)})
+ * 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)
+ * Processed: the corresponding handle method for the task has been called and has returned.
+ *
+ *
+ * @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