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

io.snice.testing.runtime.impl.SniceLocalDevRuntime Maven / Gradle / Ivy

package io.snice.testing.runtime.impl;

import io.hektor.actors.fsm.FsmActor;
import io.hektor.actors.fsm.OnStartFunction;
import io.hektor.core.ActorRef;
import io.hektor.core.Hektor;
import io.hektor.core.Props;
import io.snice.networking.common.docker.DockerSupport;
import io.snice.testing.core.CoreDsl;
import io.snice.testing.core.MessageBuilder;
import io.snice.testing.core.Session;
import io.snice.testing.core.action.ActionBuilder;
import io.snice.testing.core.protocol.Protocol;
import io.snice.testing.core.protocol.ProtocolProvider;
import io.snice.testing.core.protocol.ProtocolRegistry;
import io.snice.testing.core.scenario.Scenario;
import io.snice.testing.core.scenario.Simulation;
import io.snice.testing.runtime.SniceRuntime;
import io.snice.testing.runtime.fsm.DefaultScenarioSupervisorCtx;
import io.snice.testing.runtime.fsm.ScenarioSupervisorCtx;
import io.snice.testing.runtime.fsm.ScenarioSupervisorData;
import io.snice.testing.runtime.fsm.ScenarioSupervisorFsm;
import io.snice.testing.runtime.fsm.ScenarioSupervisorMessages;
import io.snice.util.concurrent.SniceThreadFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static io.snice.preconditions.PreConditions.assertArrayNotEmpty;
import static io.snice.preconditions.PreConditions.assertNotNull;
import static java.util.stream.Collectors.toList;

public class SniceLocalDevRuntime implements SniceRuntime {

    private static final Logger logger = LoggerFactory.getLogger(SniceLocalDevRuntime.class);

    // TODO: make it configurable
    private static final int noOfScnSupervisors = 5;

    private final int waitTime;
    private final Hektor hektor;
    private final DockerSupport dockerSupport;
    private List supervisors;

    private final List> runningScenarios = Collections.synchronizedList(new ArrayList<>());

    private final CompletableFuture doneFuture = new CompletableFuture<>();

    private final CountDownLatch firstScenarioScheduledLatch = new CountDownLatch(1);

    SniceLocalDevRuntime(final int waitTime, final Hektor hektor, final DockerSupport dockerSupport) {
        this.waitTime = waitTime;
        this.hektor = hektor;
        this.dockerSupport = dockerSupport;
    }

    @Override
    public CompletionStage sync() {
        return doneFuture;
    }

    private boolean waitForFirstTaskToBeScheduled() {
        try {
            firstScenarioScheduledLatch.await(waitTime, TimeUnit.SECONDS);
            return firstScenarioScheduledLatch.getCount() <= 0;
        } catch (final InterruptedException e) {
            return false;
        }
    }

    private void monitor() {
        logger.info("Starting monitoring system");
        boolean done = false;

        if (waitForFirstTaskToBeScheduled()) {

            while (!done) {
                try {
                    if (areAllScenariosCompleted()) {
                        done = true;
                    } else {
                        sleep(100);
                    }
                } catch (final Throwable t) {
                    // TODO
                    done = true;
                    t.printStackTrace();
                }
            }

            logger.info("All tasks completed, shutting down system");
        } else {
            logger.info("No tasks were ever scheduled, shutting down system");
        }

        // TODO: dont' remember why I need this one here.
        sleep(1000);
        // doneFuture.complete(null);
        hektor.terminate().whenComplete((aVoid, error) -> doneFuture.complete(null));
    }


    private void sleep(final int ms) {
        try {
            Thread.sleep(ms);
        } catch (final InterruptedException e) {
            // ignore
        }
    }

    private boolean areAllScenariosCompleted() {
        return !runningScenarios.stream()
                .map(CompletionStage::toCompletableFuture)
                .filter(f -> !f.isDone())
                .findFirst()
                .isPresent();
    }

    @Override
    public Future start() {

        final var startFuture = new CompletableFuture();

        // TODO: after re-structuring and moving this from the Snice main class, the below comments
        // TODO: will have to be revisited. Overall they kind of apply but even so, take the below as
        // TODO: all suggestions/thinking of how to proceed.

        // TODO: new flow, something like this:
        // 1. Gather all protocol settings from all the Scenarios that we are supposed to execute.
        // 2. Ensure there are no conflicts (not sure what those conflicts would be. Shouldn't really be any)
        // 3. Allocate UUIDs for all scenarios. These UUIDs will be part of any potential "accept" listening points.
        // 4. Configure IpProviders and if any scenario needs it then allocate a unique URL per "accept"
        //    and create a FQDN based on the "accepts" potential additional "path" (only for certain protocols, such as
        //    HTTP based ones).

        // Note: these set of protcools are uniquely configured for the one single Scenario
        // and right now we are mixing concepts. The ScenarioSupervisors are kind of per
        // system but then we run a single scenario etc. Need to separate it all since
        // either it's a single run or a system.
        // protocols.forEach(Protocol::start);

        final var latch = new CountDownLatch(noOfScnSupervisors);
        final var scnSupervisorProps = configureScenarioSupervisor(latch);
        supervisors = IntStream.range(0, noOfScnSupervisors).boxed()
                .map(i -> hektor.actorOf("ScenarioSupervisor-" + i, scnSupervisorProps))
                .collect(toList());

        // TODO: what to do if the supervisors doesn't start?
        try {
            latch.await(100, TimeUnit.MILLISECONDS);
        } catch (final InterruptedException e) {
            throw new RuntimeException("Unable to start the Snice Runtime. Exiting.", e);
        }

        final var threadGroup = SniceThreadFactory.withNamePrefix("main-snice-").withDaemon(true).build();
        Executors.newSingleThreadExecutor(threadGroup).submit(() -> monitor());

        // TODO: This obviously is not how you do it. Need to kick off the above in a different thread etc.
        startFuture.complete(null);
        return startFuture;
    }

    @Override
    public  CompletionStage run(final T simulation) {
        return internalRun(simulation);
    }

    private Simulation createDefaultExecutionPlan(final Scenario scenario, final List protocols, final boolean strictMode) {
        final var plan = new SimpleExecutionPlan();
        final var setup = plan.setUp(scenario).strictMode(strictMode);
        if (protocols != null && !protocols.isEmpty()) {
            setup.protocols(protocols);
        }

        return plan;
    }


    @Override
    public CompletionStage run(final Scenario scenario, final List protocols) {
        return internalRun(createDefaultExecutionPlan(scenario, protocols, true));
    }

    @Override
    public CompletionStage run(final ActionBuilder builder) {
        assertNotNull(builder);
        return internalRun(createDefaultExecutionPlan(CoreDsl.scenario("Default Scenario").execute(builder), List.of(), false));
    }

    @Override
    public CompletionStage run(final MessageBuilder... messages) {
        assertArrayNotEmpty(messages);
        var scenario = CoreDsl.scenario("Default Scenario");
        for (int i = 0; i < messages.length; ++i) {
            scenario = scenario.execute(messages[i]);
        }

        return internalRun(createDefaultExecutionPlan(scenario, List.of(), false));
    }

    /**
     * Create the necessary components representing the environment in which the {@link Scenario}
     * is executing and ensure that all necessary {@link Protocol}s needed by the scenario is actually
     * configured. If we run in strict mode then if a protocol is missing, we will bail out with an error
     * but if non-strict mode then we'll try and create a default protocol to satisfy the requirements of
     * the {@link Scenario}.
     *
     * @return
     */
    private CompletionStage internalRun(final Simulation simulation) {

        final var plan = simulation.plan();

        final var protocolsMap = plan.protocols().stream().collect(Collectors.toMap(Protocol::key, Function.identity()));
        final var requiredProtocols = plan.scenario().protocols();
        final var missingProtocols = requiredProtocols.stream().filter(key -> !protocolsMap.containsKey(key)).collect(toList());

        if (!missingProtocols.isEmpty() && plan.strictMode()) {
            throw new IllegalArgumentException("The following required protocol(s) for running the scenario are missing: " + missingProtocols);
        }

        createDefaultProtocols(missingProtocols).stream().forEach(p -> protocolsMap.put(p.key(), p));

        final var registry = new SimpleProtocolRegistry<>(protocolsMap);

        // TODO: when we are running many scenarios that are using the same underlying protocol stack (such
        //      as an HTTP stack) the Protocol.start() must ensure that the stack isn't started again etc.
        //      Currently, that is not the case.
        protocolsMap.values().forEach(Protocol::start);

        final var envVariables = System.getenv();
        final var session = new Session(plan.name()).environment(envVariables);

        final var future = new CompletableFuture();
        nextSupervisor().tell(new ScenarioSupervisorMessages.Run(plan.scenario(), session, registry, future));
        runningScenarios.add(future);
        firstScenarioScheduledLatch.countDown();
        return future;
    }

    /**
     * When not in strict mode and if there are missing protocols, create default versions of them.
     *
     * @param missingProtocols
     * @return
     */
    private List createDefaultProtocols(final List missingProtocols) {

        // so we don't have to load providers if we don't have to
        if (missingProtocols.isEmpty()) {
            return List.of();
        }

        final var protocolProviders = ProtocolProvider.load();
        return missingProtocols.stream()
                .map(key -> protocolProviders.get(key))
                .map(Optional::ofNullable)
                .filter(Optional::isPresent)
                .map(Optional::get)
                .map(protocolProvider -> protocolProvider.createDefaultProtocol(dockerSupport))
                .collect(Collectors.toUnmodifiableList());
    }

    private ProtocolRegistry extendRegistry(final ProtocolRegistry registry, final List protocols) {
        return ((SimpleProtocolRegistry) registry).extend(protocols);
    }

    private ActorRef nextSupervisor() {
        return supervisors.get(new Random().nextInt(noOfScnSupervisors));
    }

    private  ProtocolRegistry configureProtocolRegistry(final List protocols) {
        final Map map = new HashMap<>();
        protocols.forEach(p -> map.put(p.key(), p));
        return new SimpleProtocolRegistry<>(map);
    }

    private static record SimpleProtocolRegistry(
            Map protocols) implements ProtocolRegistry {

        public SimpleProtocolRegistry extend(final List protocols) {
            final var extendedProtocols = new HashMap();
            this.protocols.forEach((key, value) -> extendedProtocols.put(key, value));
            protocols.forEach(p -> extendedProtocols.put(p.key(), p));
            return new SimpleProtocolRegistry<>(extendedProtocols);
        }

        @Override
        public Optional protocol(final Key key) {
            return Optional.ofNullable(protocols.get(key));
        }
    }

    private static Props configureScenarioSupervisor(final CountDownLatch latch) {

        final OnStartFunction onStart = (actorCtx, ctx, data) -> {
            actorCtx.self().tell(new ScenarioSupervisorMessages.Init());
        };

        return FsmActor.of(ScenarioSupervisorFsm.definition)
                .withContext(ref -> DefaultScenarioSupervisorCtx.of(ref, latch))
                .withData(() -> new ScenarioSupervisorData())
                .withStartFunction(onStart)
                .build();
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy