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

com.swirlds.common.wiring.component.ComponentWiring Maven / Gradle / Ivy

Go to download

Swirlds is a software platform designed to build fully-distributed applications that harness the power of the cloud without servers. Now you can develop applications with fairness in decision making, speed, trust and reliability, at a fraction of the cost of traditional server-based platforms.

There is a newer version: 0.56.6
Show newest version
/*
 * Copyright (C) 2024 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.component;

import static com.swirlds.common.wiring.model.diagram.HyperlinkBuilder.platformCoreHyperlink;

import com.swirlds.common.wiring.component.internal.FilterToBind;
import com.swirlds.common.wiring.component.internal.InputWireToBind;
import com.swirlds.common.wiring.component.internal.TransformerToBind;
import com.swirlds.common.wiring.component.internal.WiringComponentProxy;
import com.swirlds.common.wiring.model.WiringModel;
import com.swirlds.common.wiring.schedulers.TaskScheduler;
import com.swirlds.common.wiring.schedulers.builders.TaskSchedulerConfiguration;
import com.swirlds.common.wiring.schedulers.builders.TaskSchedulerType;
import com.swirlds.common.wiring.transformers.RoutableData;
import com.swirlds.common.wiring.transformers.WireFilter;
import com.swirlds.common.wiring.transformers.WireRouter;
import com.swirlds.common.wiring.transformers.WireTransformer;
import com.swirlds.common.wiring.wires.input.BindableInputWire;
import com.swirlds.common.wiring.wires.input.InputWire;
import com.swirlds.common.wiring.wires.output.OutputWire;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * Builds and manages input/output wires for a component.
 *
 * @param  the type of the component
 * @param     the output type of the component
 */
@SuppressWarnings("unchecked")
public class ComponentWiring {

    private final WiringModel model;
    private final TaskScheduler scheduler;

    private final WiringComponentProxy proxy = new WiringComponentProxy();
    private final COMPONENT_TYPE proxyComponent;

    /**
     * The component that implements the business logic. Will be null until {@link #bind(Object)} is called.
     */
    private COMPONENT_TYPE component;

    /**
     * Input wires that have been created for this component.
     */
    private final Map> inputWires = new HashMap<>();

    /**
     * Input wires that need to be bound.
     */
    private final List> inputsToBind = new ArrayList<>();

    /**
     * Previously created transformers/splitters/filters.
     */
    private final Map> alternateOutputs = new HashMap<>();

    /**
     * Transformers that need to be bound.
     */
    private final List> transformersToBind = new ArrayList<>();

    /**
     * Filters that need to be bound.
     */
    private final List> filtersToBind = new ArrayList<>();

    /**
     * A splitter (if one has been constructed).
     */
    private OutputWire splitterOutput;

    /**
     * A router (if one has been constructed).
     */
    private WireRouter router;

    /**
     * A router that consumes the output of a splitter (if one has been constructed).
     */
    private WireRouter splitRouter;

    /**
     * Create a new component wiring.
     *
     * @param model     the wiring model that will contain the component
     * @param clazz     the interface class of the component
     * @param scheduler the task scheduler that will run the component
     * @deprecated use {@link #ComponentWiring(WiringModel, Class, TaskSchedulerConfiguration)} instead. Once all uses
     * have been updated, this constructor will be removed.
     */
    @Deprecated
    public ComponentWiring(
            @NonNull final WiringModel model,
            @NonNull final Class clazz,
            @NonNull final TaskScheduler scheduler) {

        this.model = Objects.requireNonNull(model);
        this.scheduler = Objects.requireNonNull(scheduler);

        if (!clazz.isInterface()) {
            throw new IllegalArgumentException("Component class " + clazz.getName() + " is not an interface.");
        }

        proxyComponent = (COMPONENT_TYPE) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] {clazz}, proxy);
    }

    /**
     * Create a new component wiring.
     *
     * @param model                  the wiring model that will contain the component
     * @param clazz                  the interface class of the component
     * @param schedulerConfiguration for the task scheduler that will run the component
     */
    public ComponentWiring(
            @NonNull final WiringModel model,
            @NonNull final Class clazz,
            @NonNull final TaskSchedulerConfiguration schedulerConfiguration) {

        this.model = Objects.requireNonNull(model);
        Objects.requireNonNull(schedulerConfiguration);

        final String schedulerName;
        final SchedulerLabel schedulerLabelAnnotation = clazz.getAnnotation(SchedulerLabel.class);
        if (schedulerLabelAnnotation == null) {
            schedulerName = clazz.getSimpleName();
        } else {
            schedulerName = schedulerLabelAnnotation.value();
        }

        this.scheduler = model.schedulerBuilder(schedulerName)
                .configure(schedulerConfiguration)
                // FUTURE WORK: all components not currently in platform core should move there
                .withHyperlink(platformCoreHyperlink(clazz))
                .build()
                .cast();

        if (!clazz.isInterface()) {
            throw new IllegalArgumentException("Component class " + clazz.getName() + " is not an interface.");
        }

        proxyComponent = (COMPONENT_TYPE) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] {clazz}, proxy);
    }

    /**
     * Get the output wire of this component.
     *
     * @return the output wire
     */
    @NonNull
    public OutputWire getOutputWire() {
        return scheduler.getOutputWire();
    }

    /**
     * Get an input wire for this component.
     *
     * @param handler      the component method that will handle the input, e.g. "MyComponent::handleInput". Should be a
     *                     method on the class, not a method on a specific instance.
     * @param  the type of the input
     * @return the input wire
     */
    public  InputWire getInputWire(
            @NonNull final BiFunction handler) {

        Objects.requireNonNull(handler);

        try {
            handler.apply(proxyComponent, null);
        } catch (final NullPointerException e) {
            throw new IllegalStateException(
                    "Component wiring does not support primitive input types or return types. Use a boxed primitive "
                            + "instead.",
                    e);
        }

        return getOrBuildInputWire(proxy.getMostRecentlyInvokedMethod(), handler, null, null, null);
    }

    /**
     * Get an input wire for this component.
     *
     * @param handler      the component method that will handle the input, e.g. "MyComponent::handleInput". Should be a
     *                     method on the class, not a method on a specific instance.
     * @param  the input type
     * @return the input wire
     */
    public  InputWire getInputWire(
            @NonNull final BiConsumer handler) {

        Objects.requireNonNull(handler);

        try {
            handler.accept(proxyComponent, null);
        } catch (final NullPointerException e) {
            throw new IllegalStateException(
                    "Component wiring does not support primitive input types. Use a boxed primitive instead.", e);
        }

        return getOrBuildInputWire(proxy.getMostRecentlyInvokedMethod(), null, handler, null, null);
    }

    /**
     * Get an input wire for this component.
     *
     * @param handler      the component method that will handle the input, e.g. "MyComponent::handleInput". Should be a
     *                     method on the class, not a method on a specific instance.
     * @param  the input type
     * @return the input wire
     */
    @NonNull
    public  InputWire getInputWire(
            @NonNull final Function handler) {
        Objects.requireNonNull(handler);

        try {
            handler.apply(proxyComponent);
        } catch (final NullPointerException e) {
            throw new IllegalStateException(
                    "Component wiring does not support primitive input types. Use a boxed primitive instead.", e);
        }

        return getOrBuildInputWire(proxy.getMostRecentlyInvokedMethod(), null, null, handler, null);
    }

    /**
     * Get an input wire for this component.
     *
     * @param handler      the component method that will handle the input, e.g. "MyComponent::handleInput". Should be a
     *                     method on the class, not a method on a specific instance.
     * @param  the input type
     * @return the input wire
     */
    @NonNull
    public  InputWire getInputWire(@NonNull final Consumer handler) {
        Objects.requireNonNull(handler);

        try {
            handler.accept(proxyComponent);
        } catch (final NullPointerException e) {
            throw new IllegalStateException(
                    "Component wiring does not support primitive input types. Use a boxed primitive instead.", e);
        }

        return getOrBuildInputWire(proxy.getMostRecentlyInvokedMethod(), null, null, null, handler);
    }

    /**
     * Get the output wire of this component, transformed by a function.
     *
     * @param transformation     the function that will transform the output, must be a static method on the component
     * @param  the type of the transformed output
     * @return the transformed output wire
     */
    @NonNull
    public  OutputWire getTransformedOutput(
            @NonNull final BiFunction transformation) {

        return getOrBuildTransformer(transformation, getOutputWire());
    }

    /**
     * Get the output wire of a splitter of this component, transformed by a function. Automatically constructs the
     * splitter if it does not already exist. Intended for use only with components that produce lists of items.
     *
     * @param transformation the function that will transform the output, must be a static method on the component
     * @param       the type of the elements in the list, the base type of this component's output is expected
     *                       to be a list of this type
     */
    public  OutputWire getSplitAndTransformedOutput(
            @NonNull final BiFunction transformation) {
        return getOrBuildTransformer(transformation, getSplitOutput());
    }

    /**
     * Create a filter for the output of this component.
     *
     * @param predicate the filter predicate
     * @return the output wire of the filter
     */
    @NonNull
    public OutputWire getFilteredOutput(
            @NonNull final BiFunction predicate) {
        return getOrBuildFilter(predicate, getOutputWire());
    }

    /**
     * Create a filter for the output of a splitter of this component. Automatically constructs the splitter if it does
     * not already exist. Intended for use only with components that produce lists of items.
     *
     * @param predicate the filter predicate
     * @param  the type of the elements in the list, the base type of this component's output is expected to be
     *                  a list of this type
     * @return the output wire of the filter
     */
    @NonNull
    public  OutputWire getSplitAndFilteredOutput(
            @NonNull final BiFunction predicate) {

        return getOrBuildFilter(predicate, getSplitOutput());
    }

    /**
     * Create a splitter for the output of this component. A splitter converts an output wire that produces lists of
     * items into an output wire that produces individual items. Note that calling this method on a component that does
     * not produce lists will result in a runtime exception.
     *
     * @param  the type of the elements in the list, the base type of this component's output is expected to be
     *                  a list of this type
     * @return the output wire
     */
    @NonNull
    public  OutputWire getSplitOutput() {
        if (splitterOutput == null) {

            // Future work: there is not a clean way to specify the "splitterInputName" label, so as a short
            // term work around we can just call it "data". This is ugly but ok as a temporary place holder.
            // The proper way to fix this is to change the way we assign labels to wires in the diagram.
            // Instead of defining names for input wires, we should instead define names for output wires,
            // and require that any scheduler that has output define the label for its output data.

            splitterOutput = getOutputWire().buildSplitter(scheduler.getName() + "Splitter", "data");
        }
        return (OutputWire) splitterOutput;
    }

    /**
     * Get an output wire that will emit a specific type of routed data. Data routing is when a component has multiple
     * outputs, and different things want to receive a subset of that output. Each output is described by a routing
     * address, which are implemented by enum values.
     * 

* This method should only be used for components which have an output type of * {@link com.swirlds.common.wiring.transformers.RoutableData RoutableData}. Calling this method more than once with * different enum classes will throw. * * @param address an enum value that describes one of the different types of data that can be routed * @param the enum that describes the different types of data handled by this router * @param the type of data that travels over the output wire * @return the output wire */ @NonNull public , DATA_TYPE> OutputWire getRoutedOutput( @NonNull final ROUTER_ENUM address) { final Class clazz = (Class) address.getClass(); return getOrBuildRouter(clazz).getOutput(address); } /** * Get an output wire that will receive a specific type of routed data after being split apart. Data routing is when * a component has multiple outputs, and different things want to receive a subset of that output. Each output is * described by a routing address, which are implemented by enum values. *

* This method should only be used for components which have an output type of * {@link com.swirlds.common.wiring.transformers.RoutableData List<RoutableData>}. Calling this method more * than once with different enum classes will throw. * * @param address an enum value that describes one of the different types of data that can be routed * @param the enum that describes the different types of data handled by this router * @param the type of data that travels over the output wire * @return the output wire */ @NonNull public , DATA_TYPE> OutputWire getSplitAndRoutedOutput( @NonNull final ROUTER_ENUM address) { final Class clazz = (Class) address.getClass(); return getOrBuildSplitRouter(clazz).getOutput(address); } /** * Get the router for this component if one has been built. Build and return a new router if one has not been * built. * * @param routerType the type of the router * @param the type of the router * @return the router */ @NonNull private > WireRouter getOrBuildRouter( @NonNull final Class routerType) { if (splitRouter != null) { throw new IllegalStateException("Only one type of router can be constructed per task scheduler. " + "This task scheduler already has a router that was built using the split output type, " + "so a router cannot be created with the unmodified output type."); } if (router != null) { if (!router.getRouterType().equals(routerType)) { throw new IllegalArgumentException("Only one type of router can be constructed per task scheduler. " + "This task scheduler already has a router of type " + router.getRouterType().getName() + "but an attempt was made to construct a router of type " + routerType.getName()); } } else { router = new WireRouter<>(model, getSchedulerName() + "Router", "data", routerType); getOutputWire().solderTo((InputWire) router.getInput()); } return (WireRouter) router; } /** * Get the router for split data for this component if one has been built. Build and return a new splitter and * router if one has not been built. * * @param routerType the type of the router * @param the type of the router * @return the router */ @NonNull private > WireRouter getOrBuildSplitRouter( @NonNull final Class routerType) { if (router != null) { throw new IllegalStateException("Only one type of router can be constructed per task scheduler. " + "This task scheduler already has a router that was built using the unmodified output type, " + "so a router cannot be created with the split output type."); } if (splitRouter != null) { if (!splitRouter.getRouterType().equals(routerType)) { throw new IllegalArgumentException("Only one type of router can be constructed per task scheduler. " + "This task scheduler already has a router of type " + router.getRouterType().getName() + "but an attempt was made to construct a router of type " + routerType.getName()); } } else { splitRouter = new WireRouter<>(model, getSchedulerName() + "Router", "data", routerType); final OutputWire> splitOutput = getSplitOutput(); final InputWire> routerInput = ((WireRouter) splitRouter).getInput(); splitOutput.solderTo(routerInput); } return (WireRouter) splitRouter; } /** * Create a transformed output wire or return the existing one if it has already been created. * * @param transformation the function that will transform the output, must be a static method on the component * @param transformerSource the source of the data to transform (i.e. the base output wire or the output wire of * the splitter) * @param the type of the elements passed to the transformer * @param the type of the transformed output * @return the transformed output wire */ @NonNull private OutputWire getOrBuildTransformer( @NonNull final BiFunction transformation, @NonNull final OutputWire transformerSource) { Objects.requireNonNull(transformation); try { transformation.apply(proxyComponent, null); } catch (final NullPointerException e) { throw new IllegalStateException( "Component wiring does not support primitive input types or return types. " + "Use a boxed primitive instead.", e); } final Method method = proxy.getMostRecentlyInvokedMethod(); if (!method.isDefault()) { throw new IllegalArgumentException("Method " + method.getName() + " does not have a default."); } if (alternateOutputs.containsKey(method)) { // We've already created this transformer. return (OutputWire) alternateOutputs.get(method); } final String wireLabel; final InputWireLabel inputWireLabel = method.getAnnotation(InputWireLabel.class); if (inputWireLabel == null) { wireLabel = "data to transform"; } else { wireLabel = inputWireLabel.value(); } final String schedulerLabel; final SchedulerLabel schedulerLabelAnnotation = method.getAnnotation(SchedulerLabel.class); if (schedulerLabelAnnotation == null) { schedulerLabel = method.getName(); } else { schedulerLabel = schedulerLabelAnnotation.value(); } final WireTransformer transformer = new WireTransformer<>(model, schedulerLabel, wireLabel); transformerSource.solderTo(transformer.getInputWire()); alternateOutputs.put(method, transformer.getOutputWire()); if (component == null) { // we will bind this later transformersToBind.add((TransformerToBind) new TransformerToBind<>(transformer, transformation)); } else { // bind this now transformer.bind(x -> transformation.apply(component, x)); } return transformer.getOutputWire(); } /** * Create a filtered output wire or return the existing one if it has already been created. * * @param predicate the filter predicate * @param filterSource the source of the data to filter (i.e. the base output wire or the output wire of the * splitter) * @param the type of the elements passed to the filter * @return the output wire of the filter */ private OutputWire getOrBuildFilter( @NonNull final BiFunction predicate, @NonNull final OutputWire filterSource) { Objects.requireNonNull(predicate); try { predicate.apply(proxyComponent, null); } catch (final NullPointerException e) { throw new IllegalStateException( "Component wiring does not support primitive input types or return types. " + "Use a boxed primitive instead.", e); } final Method method = proxy.getMostRecentlyInvokedMethod(); if (!method.isDefault()) { throw new IllegalArgumentException("Method " + method.getName() + " does not have a default."); } if (alternateOutputs.containsKey(method)) { // We've already created this filter. return (OutputWire) alternateOutputs.get(method); } final String wireLabel; final InputWireLabel inputWireLabel = method.getAnnotation(InputWireLabel.class); if (inputWireLabel == null) { wireLabel = "data to filter"; } else { wireLabel = inputWireLabel.value(); } final String schedulerLabel; final SchedulerLabel schedulerLabelAnnotation = method.getAnnotation(SchedulerLabel.class); if (schedulerLabelAnnotation == null) { schedulerLabel = method.getName(); } else { schedulerLabel = schedulerLabelAnnotation.value(); } final WireFilter filter = new WireFilter<>(model, schedulerLabel, wireLabel); filterSource.solderTo(filter.getInputWire()); alternateOutputs.put(method, filter.getOutputWire()); if (component == null) { // we will bind this later filtersToBind.add((FilterToBind) new FilterToBind<>(filter, predicate)); } else { // bind this now filter.bind(x -> predicate.apply(component, x)); } return filter.getOutputWire(); } /** * Get the input wire for a specified method. * * @param method the method that will handle data on the input wire * @param handlerWithReturn the handler for the method if it has a return type * @param handlerWithoutReturn the handler for the method if it does not have a return type * @param handlerWithoutParameter the handler for the method if it does not have a parameter * @param handlerWithoutReturnAndWithoutParameter the handler for the method if it does not have a return type and * does not have a parameter * @param the input type * @return the input wire */ private InputWire getOrBuildInputWire( @NonNull final Method method, @Nullable final BiFunction handlerWithReturn, @Nullable final BiConsumer handlerWithoutReturn, @Nullable final Function handlerWithoutParameter, @Nullable final Consumer handlerWithoutReturnAndWithoutParameter) { if (inputWires.containsKey(method)) { // We've already created this wire return (InputWire) inputWires.get(method); } final String label; final InputWireLabel inputWireLabel = method.getAnnotation(InputWireLabel.class); if (inputWireLabel == null) { label = method.getName(); } else { label = inputWireLabel.value(); } final BindableInputWire inputWire = scheduler.buildInputWire(label); inputWires.put(method, (BindableInputWire) inputWire); if (component == null) { // we will bind this later inputsToBind.add((InputWireToBind) new InputWireToBind<>( inputWire, handlerWithReturn, handlerWithoutReturn, handlerWithoutParameter, handlerWithoutReturnAndWithoutParameter)); } else { // bind this now if (handlerWithReturn != null) { inputWire.bind(x -> handlerWithReturn.apply(component, x)); } else if (handlerWithoutReturn != null) { inputWire.bindConsumer(x -> { handlerWithoutReturn.accept(component, x); }); } else if (handlerWithoutParameter != null) { inputWire.bind(x -> handlerWithoutParameter.apply(component)); } else { assert handlerWithoutReturnAndWithoutParameter != null; inputWire.bindConsumer(x -> { handlerWithoutReturnAndWithoutParameter.accept(component); }); } } return inputWire; } /** * Flush all data in the task scheduler. Blocks until all data currently in flight has been processed. * * @throws UnsupportedOperationException if the scheduler does not support flushing */ public void flush() { scheduler.flush(); } /** * Start squelching the output of this component. * * @throws UnsupportedOperationException if the scheduler does not support squelching * @throws IllegalStateException if the scheduler is already squelching */ public void startSquelching() { scheduler.startSquelching(); } /** * Stop squelching the output of this component. * * @throws UnsupportedOperationException if the scheduler does not support squelching * @throws IllegalStateException if the scheduler is not squelching */ public void stopSquelching() { scheduler.stopSquelching(); } /** * Bind the component to the input wires. * * @param component the component to bind */ public void bind(@NonNull final COMPONENT_TYPE component) { Objects.requireNonNull(component); this.component = component; // Bind input wires for (final InputWireToBind wireToBind : inputsToBind) { if (wireToBind.handlerWithReturn() != null) { final BiFunction handlerWithReturn = (BiFunction) wireToBind.handlerWithReturn(); wireToBind.inputWire().bind(x -> handlerWithReturn.apply(component, x)); } else if (wireToBind.handlerWithoutReturn() != null) { final BiConsumer handlerWithoutReturn = (BiConsumer) wireToBind.handlerWithoutReturn(); wireToBind.inputWire().bindConsumer(x -> { handlerWithoutReturn.accept(component, x); }); } else if (wireToBind.handlerWithoutParameter() != null) { wireToBind .inputWire() .bind(x -> wireToBind.handlerWithoutParameter().apply(component)); } else { assert wireToBind.handlerWithoutReturnAndWithoutParameter() != null; wireToBind.inputWire().bindConsumer(x -> { wireToBind.handlerWithoutReturnAndWithoutParameter().accept(component); }); } } // Bind transformers for (final TransformerToBind transformerToBind : transformersToBind) { final WireTransformer transformer = transformerToBind.transformer(); final BiFunction transformation = transformerToBind.transformation(); transformer.bind(x -> transformation.apply(component, x)); } // Bind filters for (final FilterToBind filterToBind : filtersToBind) { filterToBind.filter().bind(x -> filterToBind.predicate().apply(component, x)); } } /** * Bind to a component. This method is similar to {@link #bind(Object)}, but it allows the component to be created * if and only if we need to bind to it. This method will invoke the supplier if the task scheduler type is anything * other than a {@link com.swirlds.common.wiring.schedulers.builders.TaskSchedulerType#NO_OP NO_OP} scheduler. * * @param componentBuilder builds or supplies the component */ public void bind(@NonNull final Supplier componentBuilder) { Objects.requireNonNull(componentBuilder); if (scheduler.getType() != TaskSchedulerType.NO_OP) { bind(componentBuilder.get()); } } /** * Get the name of the scheduler that is running this component. * * @return the name of the scheduler */ @NonNull public String getSchedulerName() { return scheduler.getName(); } }