io.flowly.engine.verticles.Engine.java Source code

Java tutorial

Introduction

Here is the source code for io.flowly.engine.verticles.Engine.java

Source

/*
 * Copyright (c) 2015 The original author or authors.
 *
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Apache License v2.0
 *  which accompanies this distribution.
 *
 *  The Apache License v2.0 is available at
 *  http://opensource.org/licenses/Apache-2.0
 *
 *  You may elect to redistribute this code under this license.
 */

package io.flowly.engine.verticles;

import io.flowly.core.Failure;
import io.flowly.core.data.FlowInstanceMetadata;
import io.flowly.core.data.FlowMetadata;
import io.flowly.core.verticles.ConsumerRegistration;
import io.flowly.core.verticles.VerticleUtils;
import io.flowly.engine.EngineAddresses;
import io.flowly.engine.JsonKeys;
import io.flowly.core.data.FlowInstance;
import io.flowly.core.data.FlowInstanceStep;
import io.flowly.core.codecs.FlowInstanceCodec;
import io.flowly.core.codecs.FlowInstanceMetadataCodec;
import io.flowly.core.codecs.FlowMetadataCodec;
import io.flowly.engine.data.FlowInstanceWrapper;
import io.flowly.engine.data.manager.FlowInstanceReadManager;
import io.flowly.engine.router.Route;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.eventbus.DeliveryOptions;
import io.vertx.core.eventbus.EventBus;
import io.vertx.core.eventbus.Message;
import io.vertx.core.json.JsonObject;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;

import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

/**
 * Verticle that manages all aspects of flow instances.
 *
 * @author <a>Uday Tatiraju</a>
 */
public class Engine extends AbstractVerticle {
    private static final Logger logger = LoggerFactory.getLogger(Engine.class);

    private EventBus eventBus;

    // If configured to true, a flow's start, complete and fail events are broadcasted.
    private boolean publishFlowLifeCycleEvents;

    @Override
    public void start(Future<Void> startFuture) throws Exception {
        eventBus = vertx.eventBus();

        publishFlowLifeCycleEvents = config().getBoolean(JsonKeys.PUBLISH_FLOW_LIFE_CYCLE_EVENTS, false);

        // Register message handlers.
        VerticleUtils.registerHandlers(vertx.eventBus(), logger, createMessageHandlers(), h -> {
            if (h.succeeded()) {
                logger.info("Deployed engine verticle.");
                startFuture.complete();
            } else {
                startFuture.fail(h.cause());
            }
        });
    }

    @Override
    public void stop(Future<Void> stopFuture) throws Exception {
        stopFuture.complete();
        logger.info("Undeployed engine verticle.");
    }

    /**
     * Create message handlers for flow instance requests.
     *
     * @return queue of consumer registrations.
     */
    private Queue<ConsumerRegistration<Object>> createMessageHandlers() {
        Queue<ConsumerRegistration<Object>> registrations = new LinkedList<>();
        registrations.add(new ConsumerRegistration<>(EngineAddresses.START_FLOW_INSTANCE, startInstanceHandler()));

        registrations
                .add(new ConsumerRegistration<>(EngineAddresses.HOP_FLOW_INSTANCE, hopInstanceHandler(), true));
        registrations.add(
                new ConsumerRegistration<>(EngineAddresses.HOP_INTO_FLOW_INSTANCE, hopIntoInstanceHandler(), true));
        registrations
                .add(new ConsumerRegistration<>(EngineAddresses.FAIL_FLOW_INSTANCE, failInstanceHandler(), true));

        registrations.add(new ConsumerRegistration<>(EngineAddresses.AWAIT_USER_INTERACTION,
                awaitUserInteractionHandler(), true));
        registrations.add(new ConsumerRegistration<>(EngineAddresses.START_FLOW_INSTANCE_TASK,
                startUserInteractionHandler()));

        registrations.add(
                new ConsumerRegistration<>(EngineAddresses.START_USER_INTERACTION_VIEW, startViewHandler(), true));
        registrations.add(new ConsumerRegistration<>(EngineAddresses.SAVE_FLOW_INSTANCE_TASK, saveViewHandler()));
        registrations.add(
                new ConsumerRegistration<>(EngineAddresses.COMPLETE_FLOW_INSTANCE_TASK, completeViewHandler()));

        return registrations;
    }

    private Handler<Message<Object>> startInstanceHandler() {
        return message -> {
            // Message comes from clustered event bus.
            FlowMetadata flowMetadata = new FlowMetadata(((JsonObject) message.body()).getMap());
            flowMetadata.validate();

            createInstance(flowMetadata, resultHandler -> {
                FlowInstance instance = resultHandler.result();

                if (instance != null) {
                    Long instanceId = instance.getMetadata().getInstanceId();
                    message.reply(instanceId);

                    if (logger.isInfoEnabled()) {
                        logger.info("Started instance: " + instanceId + ", flow: " + flowMetadata.getFlowId()
                                + ", app: " + flowMetadata.getAppId());
                    }

                    // Begin the flow.
                    startToken(instance);

                    // Broadcast event.
                    broadcastFlowLifecycleEvent(JsonKeys.FLOW_START_EVENT, instance.getMetadata(),
                            publishFlowLifeCycleEvents);
                } else {
                    Failure failure = new Failure(3000, "Unable to start instance for flow: "
                            + flowMetadata.getFlowId() + ", app: " + flowMetadata.getAppId());
                    logger.error(failure.getError());
                    message.fail(failure.getCode(), failure.getMessage());
                }
            });
        };
    }

    private Handler<Message<Object>> hopInstanceHandler() {
        return message -> {
            // Get the next flow object for the flow instance.
            moveToken(new FlowInstance(((JsonObject) message.body()).getMap()));
        };
    }

    private Handler<Message<Object>> hopIntoInstanceHandler() {
        return message -> {
            // Start sub-flow.
            startToken(new FlowInstance(((JsonObject) message.body()).getMap()));
        };
    }

    private Handler<Message<Object>> failInstanceHandler() {
        return message -> {
            FlowInstance instance = new FlowInstance(((JsonObject) message.body()).getMap());
            failInstance(instance, null);
        };
    }

    private Handler<Message<Object>> awaitUserInteractionHandler() {
        return message -> {
            FlowInstance instance = new FlowInstance(((JsonObject) message.body()).getMap());
            FlowInstanceMetadata metadata = instance.getMetadata();

            // TODO: Assign user based on load balancer output.
            saveInstance(instance, FlowInstanceReadManager.STATUS_NEW, true, resultHandler -> {
                if (resultHandler.result()) {
                    assignUserInteraction(metadata);
                } else {
                    Failure failure = new Failure(3001,
                            "Failed to pause flow instance for user interaction: " + metadata);
                    failInstance(instance, failure);
                }
            });
        };
    }

    private void assignUserInteraction(FlowInstanceMetadata metadata) {
        JsonObject args = new JsonObject().put(JsonKeys.SUBJECT_ID, JsonKeys.ADMIN_USER_ID).put(JsonKeys.TASK_ID,
                metadata.getCurrentStep().getFlowObjectInstanceId());

        eventBus.send(EngineAddresses.REPO_ASSIGN_TASK, args, reply -> {
            if ((Boolean) reply.result().body()) {
                broadcastFlowLifecycleEvent(JsonKeys.FLOW_WAIT_UI_EVENT, metadata, true);
                logger.info("Flow instance awaiting user interaction: " + metadata);
            }
        });
    }

    private Handler<Message<Object>> startUserInteractionHandler() {
        return message -> {
            // TODO: Add validation to ensure that status is "New"
            Long flowObjectInstanceId = (Long) message.body();
            getInstance(flowObjectInstanceId, resultHandler -> {
                FlowInstance instance = resultHandler.result();

                // Start interactive service.
                startToken(instance);

                // Update task status.
                updateTask(instance.getMetadata().getCurrentStep().getFlowObjectInstanceId(),
                        FlowInstanceReadManager.STATUS_IN_PROGRESS);
            });
        };
    }

    private void updateTask(Long taskId, String status) {
        JsonObject args = new JsonObject().put(JsonKeys.STATUS, status).put(JsonKeys.TASK_ID, taskId);

        eventBus.send(EngineAddresses.REPO_UPDATE_TASK, args, reply -> {
            // TODO: If unable to update the task, fail the instance?
            if ((Boolean) reply.result().body()) {
                logger.info("Flow instance user interaction assignment status updated: " + taskId);
            }
        });
    }

    private Handler<Message<Object>> startViewHandler() {
        return message -> {
            FlowInstance instance = new FlowInstance(((JsonObject) message.body()).getMap());
            updateView(instance, FlowInstanceReadManager.STATUS_USER_INTERACTING, JsonKeys.FLOW_START_UI_EVENT,
                    false);
        };
    }

    private Handler<Message<Object>> saveViewHandler() {
        // TODO: update signature to support async client-side save.
        return viewHandler(FlowInstanceReadManager.STATUS_USER_INTERACTING, JsonKeys.FLOW_SAVE_UI_EVENT, false);
    }

    private Handler<Message<Object>> completeViewHandler() {
        return viewHandler(FlowInstanceReadManager.STATUS_COMPLETED, JsonKeys.FLOW_COMPLETE_UI_EVENT, true);
    }

    private Handler<Message<Object>> viewHandler(String viewStatus, String broadcastEventType, boolean moveToken) {
        return message -> {
            // TODO: Add validation.
            // TODO: Add viewData to FlowObjectInstance.
            FlowInstance viewInstance = new FlowInstance(((JsonObject) message.body()).getMap());

            Long flowObjectInstanceId = viewInstance.getMetadata().getCurrentStep().getFlowObjectInstanceId();
            getInstance(flowObjectInstanceId, resultHandler -> {
                FlowInstance instance = resultHandler.result();
                instance.setData(viewInstance.getData());
                updateView(instance, viewStatus, broadcastEventType, moveToken);
            });
        };
    }

    private void updateView(FlowInstance instance, String status, String broadcastEventType, boolean moveToken) {
        FlowInstanceMetadata metadata = instance.getMetadata();

        saveInstance(instance, status, true, resultHandler -> {
            if (resultHandler.result()) {
                // Get the next flow object for the flow instance.
                if (moveToken) {
                    moveToken(instance);
                }

                broadcastFlowLifecycleEvent(broadcastEventType, instance.getMetadata(), true);

                // TODO: Send message to socket.io handler.
                logger.info("Flow instance user interaction view updated: " + metadata);
            } else {
                Failure failure = new Failure(3002,
                        "Flow instance user interaction view failure: " + metadata + ", status: " + status);
                failInstance(instance, failure);
            }
        });
    }

    /**
     * Start a given flow instance by creating a start token and running the step (flow object).
     * The flow router is consulted to get the start flow object.
     *
     * @param instance represents an instance of a given flow.
     */
    private void startToken(FlowInstance instance) {
        // TODO: Add validation.
        FlowInstanceMetadata metadata = instance.getMetadata();
        FlowInstanceStep currentStep = metadata.getCurrentStep();

        // See if we are going to start a flow or a sub-flow.
        String flowId = currentStep != null ? currentStep.getSubFlowId() : metadata.getFlowId();

        FlowInstanceMetadata routeMetadata = new FlowInstanceMetadata();
        routeMetadata.setFlowId(flowId);
        routeMetadata.setCurrentStep(new FlowInstanceStep());
        routeMetadata.getCurrentStep().setFlowObjectId("0");

        getNextRoute(routeMetadata, resultHandler -> {
            Route route = resultHandler.result();

            if (validateInstanceRoute(instance, route)) {
                prepareAndRunStep(instance, route.getNext(), 0, true);
            }
        });
    }

    /**
     * End the flow's current token, consult the router, create a new token and run the step (flow object).
     *
     * @param instance represents an instance of a given flow.
     */
    private void moveToken(FlowInstance instance) {
        FlowInstanceMetadata metadata = instance.getMetadata();
        getNextRoute(metadata, resultHandler -> {
            Route route = resultHandler.result();

            if (!validateInstanceRoute(instance, route)) {
                return;
            }

            if (!route.isEnd()) {
                if (route.isSplit()) {
                    List<Route.Next> nextList = route.getNextList();

                    for (int i = 0; i < nextList.size(); i++) {
                        prepareAndRunStep(instance, nextList.get(i), i, false);
                    }
                } else {
                    prepareAndRunStep(instance, route.getNext(), 0, false);
                }
            } else {
                completeInstance(instance);
            }
        });
    }

    private void getNextRoute(FlowInstanceMetadata routeMetadata, Handler<AsyncResult<Route>> resultHandler) {
        Future<Route> future = Future.future();
        future.setHandler(resultHandler);

        DeliveryOptions options = Kernel.DELIVERY_OPTIONS.get(FlowInstanceMetadataCodec.NAME);
        eventBus.send(EngineAddresses.REPO_FLOW_NEXT_ROUTE, routeMetadata, options, reply -> {
            future.complete((Route) reply.result().body());
        });
    }

    /**
     * If flow's persistence is enabled, save current graph and create a new vertex (represents a new token)
     * and run the new step (flow object).
     *
     * @param instance represents an instance of a given flow.
     * @param next the next flow object in the flow on which a token is created.
     * @param stepIndex represents the index in a split or loop.
     * @param isStart indicates if the current token is to be completed (move) or not.
     */
    private void prepareAndRunStep(FlowInstance instance, Route.Next next, int stepIndex, boolean isStart) {
        // Persistence enabled - update instance graph.
        if (instance.getMetadata().getInstanceId() != null) {
            FlowInstanceWrapper wrapper = new FlowInstanceWrapper(instance, isStart, false, null, next);

            eventBus.send(EngineAddresses.REPO_FLOW_CREATE_FLOW_OBJECT_INSTANCE, wrapper, reply -> {
                Long flowObjectInstanceId = (Long) reply.result().body();

                if (flowObjectInstanceId != null) {
                    prepareStep(instance, next, stepIndex, isStart, flowObjectInstanceId);
                    runStep(instance);
                } else {
                    // Houston, we got a problem.
                }
            });
        } else {
            prepareStep(instance, next, stepIndex, isStart, null);
            runStep(instance);
        }
    }

    private void prepareStep(FlowInstance instance, Route.Next next, int stepIndex, boolean isStart,
            Long flowObjectInstanceId) {
        FlowInstanceMetadata metadata = instance.getMetadata();
        FlowInstanceStep currentStep = metadata.getCurrentStep();

        if (currentStep != null) {
            // This is a sub-flow (current step is not null) - set the parent reference.
            if (isStart) {
                updateSubFlowMetaData(metadata, currentStep);
                instance.setData(new JsonObject());
            }
        }

        currentStep = new FlowInstanceStep();
        currentStep.setStepIndex(stepIndex);
        currentStep.setFlowObjectId(next.getFlowObjectId());

        if (flowObjectInstanceId != null) {
            currentStep.setFlowObjectInstanceId(flowObjectInstanceId);
        }

        if (next.getSubFlowId() != null) {
            currentStep.setSubFlowId(next.getSubFlowId());
        }

        metadata.setCurrentStep(currentStep);

        if (logger.isInfoEnabled()) {
            logger.info("Flow instance token " + (isStart ? "started: " : "moved: ") + metadata);
        }
    }

    /**
     * Executes the flow instance step (flow object) by calling the corresponding flow verticle.
     *
     * @param instance represents the instance of a given flow that is passed to the verticle.
     */
    private void runStep(FlowInstance instance) {
        FlowInstanceMetadata metadata = instance.getMetadata();
        String sendAddress = EngineAddresses.getFlowObjectBusAddress(metadata.getAppId(),
                metadata.getCurrentStep().getFlowObjectId());

        vertx.eventBus().send(sendAddress, instance);

        if (logger.isInfoEnabled()) {
            logger.info("Flow instance running: " + instance.getMetadata().getInstanceId() + ", flow: "
                    + metadata.getFlowId() + ", app: " + metadata.getAppId() + ", step: "
                    + metadata.getCurrentStep());
        }
    }

    private void createInstance(FlowMetadata flowMetadata, Handler<AsyncResult<FlowInstance>> resultHandler) {
        Future<FlowInstance> future = Future.future();
        future.setHandler(resultHandler);

        if (flowMetadata.persistenceEnabled()) {
            DeliveryOptions options = Kernel.DELIVERY_OPTIONS.get(FlowMetadataCodec.NAME);
            eventBus.send(EngineAddresses.REPO_FLOW_CREATE_INSTANCE, flowMetadata, options, reply -> {
                future.complete((FlowInstance) reply.result().body());
            });
        } else {
            future.complete(new FlowInstance(null, flowMetadata));
        }
    }

    private void saveInstance(FlowInstance instance, String status, boolean saveMetaData,
            Handler<AsyncResult<Boolean>> resultHandler) {
        Future<Boolean> future = Future.future();
        future.setHandler(resultHandler);

        FlowInstanceWrapper wrapper = new FlowInstanceWrapper(instance, false, saveMetaData, status, null);
        eventBus.send(EngineAddresses.REPO_FLOW_SAVE_INSTANCE, wrapper, reply -> {
            future.complete((Boolean) reply.result().body());
        });
    }

    private void completeInstance(FlowInstance instance, boolean saved) {
        FlowInstanceMetadata metadata = instance.getMetadata();
        String parentFlowObjectId = metadata.getParentFlowObjectId();

        if (saved) {
            logger.info("Flow instance completed: " + metadata);

            // Broadcast event.
            broadcastFlowLifecycleEvent(JsonKeys.FLOW_COMPLETE_EVENT, metadata, publishFlowLifeCycleEvents);

            // This is a sub flow. Now, flow upwards to parent.
            if (parentFlowObjectId != null) {
                getInstance(metadata.getParentFlowObjectInstanceId(), resultHandler -> {
                    FlowInstance parentInstance = resultHandler.result();

                    if (parentInstance != null) {
                        parentInstance.setOutputData(instance.getOutputData());

                        String sendAddress = EngineAddresses.getFlowObjectBusAddress(metadata.getAppId(),
                                parentFlowObjectId + EngineAddresses.HOP_OUT_FLOW_INSTANCE);
                        eventBus.send(sendAddress, parentInstance);

                        // Update task status.
                        updateTask(parentInstance.getMetadata().getCurrentStep().getFlowObjectInstanceId(),
                                FlowInstanceReadManager.STATUS_COMPLETED);
                    } else {
                        logger.fatal("Parent instance not found. Unable to flow out from instance: " + metadata);
                    }
                });
            }
        } else {
            Failure failure = new Failure(3003,
                    "Unable to close flow object or instance vertex: " + metadata.getInstanceId() + ", flow: "
                            + metadata.getFlowId() + ", app: " + metadata.getAppId());
            failInstance(instance, failure);
        }
    }

    private void completeInstance(FlowInstance instance) {
        if (instance.getMetadata().getInstanceId() != null) {
            DeliveryOptions options = Kernel.DELIVERY_OPTIONS.get(FlowInstanceCodec.NAME);
            eventBus.send(EngineAddresses.REPO_FLOW_COMPLETE_INSTANCE, instance, options, reply -> {
                completeInstance(instance, (Boolean) reply.result().body());
            });
        } else {
            completeInstance(instance, true);
        }
    }

    private void getInstance(Long flowObjectInstanceId, Handler<AsyncResult<FlowInstance>> resultHandler) {
        Future<FlowInstance> future = Future.future();
        future.setHandler(resultHandler);

        if (flowObjectInstanceId != null) {
            eventBus.send(EngineAddresses.GET_FLOW_INSTANCE_TASK, flowObjectInstanceId, reply -> {
                future.complete((FlowInstance) reply.result().body());
            });
        } else {
            future.complete(null);
        }
    }

    private void failInstance(FlowInstance instance, Failure failure) {
        FlowInstanceMetadata metadata = instance.getMetadata();

        if (metadata.getInstanceId() != null) {
            DeliveryOptions options = Kernel.DELIVERY_OPTIONS.get(FlowInstanceCodec.NAME);
            eventBus.send(EngineAddresses.REPO_FLOW_FAIL_INSTANCE, instance, options, reply -> {
                if ((Boolean) reply.result().body()) {
                    // Broadcast event.
                    broadcastFlowLifecycleEvent(JsonKeys.FLOW_FAIL_EVENT, metadata, publishFlowLifeCycleEvents);
                } else {
                    // Should never happen.
                    Failure fatalFailure = new Failure(3004,
                            "Failed to update instance status to 'failed': " + metadata);
                    logger.fatal(fatalFailure.getError());
                }
            });
        } else {
            // Broadcast event.
            broadcastFlowLifecycleEvent(JsonKeys.FLOW_FAIL_EVENT, metadata, publishFlowLifeCycleEvents);
        }

        if (failure != null) {
            logger.error(failure.getError(), failure.getCause());
        }
    }

    private boolean validateInstanceRoute(FlowInstance instance, Route route) {
        if (!route.isValid()) {
            FlowInstanceMetadata metadata = instance.getMetadata();
            Failure failure = new Failure(3005, "Invalid route for instance: " + metadata);
            failInstance(instance, failure);
        }

        return route.isValid();
    }

    private void broadcastFlowLifecycleEvent(String eventType, FlowInstanceMetadata metadata, boolean enabled) {
        if (!enabled) {
            return;
        }

        JsonObject event = new JsonObject().put(JsonKeys.FLOW_EVENT_TYPE, eventType);
        Long instanceId = metadata.getInstanceId();
        Long parentFlowObjectInstanceId = metadata.getParentFlowObjectInstanceId();

        if (instanceId != null) {
            event.put(JsonKeys.INSTANCE_ID, instanceId);
        }

        if (parentFlowObjectInstanceId != null) {
            event.put(FlowInstanceMetadata._PARENT_FLOW_OBJECT_INSTANCE_ID, parentFlowObjectInstanceId);
        }

        if (metadata.getCurrentStep() != null) {
            event.put(FlowInstanceStep._FLOW_OBJECT_INSTANCE_ID,
                    metadata.getCurrentStep().getFlowObjectInstanceId());
        }

        eventBus.publish(EngineAddresses.FLOW_LIFECYCLE_EVENT, event);
    }

    private void updateSubFlowMetaData(FlowInstanceMetadata metadata, FlowInstanceStep step) {
        metadata.setParentFlowObjectId(step.getFlowObjectId());
        metadata.setFlowId(step.getSubFlowId());

        if (step.getFlowObjectInstanceId() != null) {
            metadata.setParentFlowObjectInstanceId(step.getFlowObjectInstanceId());
        }
    }
}