package forklift; import com.google.common.base.Preconditions; import consul.Consul; import forklift.connectors.ActiveMQConnector; import forklift.connectors.ForkliftConnectorI; import forklift.consumer.ConsumerDeploymentEvents; import forklift.decorators.*; import forklift.deployment.Deployment; import forklift.deployment.DeploymentManager; import forklift.deployment.DeploymentWatch; import forklift.deployment.ClassDeployment; import forklift.replay.ReplayES; import forklift.replay.ReplayLogger; import forklift.retry.RetryES; import forklift.retry.RetryHandler; import forklift.stats.StatsCollector; import org.apache.activemq.broker.BrokerService; import org.apache.http.annotation.ThreadSafe; import java.io.File; import java.io.FileNotFoundException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.CountDownLatch; /** * Start Forklift as a server. A running forklift server is responsible for * <pre> * 1. Watching a directory for new deployments * 2. Watching a directory for new properties * 3. Accepting new deployments at runtime via the {@link #registerDeployment(Class[])} method * 4. Optionally running a Broker * 5. Connections to the Broker * 6. Starting and managing the Deployment lifecycles * 7. Retry Strategy * 8. Replay Strategy * </pre> * * @author zdavep */ @ThreadSafe public final class ForkliftServer { private ExecutorService executor = Executors.newSingleThreadExecutor(); private volatile ServerState state = ServerState.LATENT; // Logging private static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ForkliftServer.class); // Consumer deployment interval private static int SLEEP_INTERVAL = 10000; // 10 seconds private static BrokerService broker = null; private final ForkliftOpts opts; private DeploymentWatch consumerWatch; private Forklift forklift; private DeploymentWatch propsWatch; public ForkliftServer(ForkliftOpts options) { this.opts = options; } private ReplayES replayES; private ConsumerDeploymentEvents deploymentEvents; private DeploymentManager classDeployments = new DeploymentManager(); private CountDownLatch runningLatch = new CountDownLatch(1); /** * Attempts to start the forklift server. This call is blocking and will not return until either the server starts successfully or the waitTime reaches 0. * A response of false does not mean the server will stop its attempt to startup. This method may only be called once. * * @param waitTime the maximum time to wait * @param timeUnit the time unit of the waitTime * @return the {@link ServerState state} of the server at the time this method returns * @throws InterruptedException if the current thread is interrupted while waiting for the server to start * @throws IllegalStateException if this method has already been called */ public ServerState startServer(long waitTime, TimeUnit timeUnit) throws InterruptedException { synchronized (this) { Preconditions.checkState(state == ServerState.LATENT); state = ServerState.STARTING; log.info("Forklift server starting"); } executor.execute(this::launch); try { this.runningLatch.await(waitTime, timeUnit); return state; } catch (InterruptedException e) { log.error("Launch Interrupted", e); throw e; } } /** * Stops the ForkliftServer. This call is blocking and will not return until either the server is shutdown or the waitTime reaches 0. * * @param waitTime the maximum time to wait * @param timeUnit the time unit of the waitTime * @return the {@link ServerState state} of the server at the time this method returns * @throws InterruptedException if the current thread is interrupted before the waitTime has ellapsed */ public ServerState stopServer(long waitTime, TimeUnit timeUnit) throws InterruptedException { shutdown(); executor.shutdownNow(); executor.awaitTermination(waitTime, timeUnit); return state; } /** * @return the current {@link ServerState state} of the server */ public ServerState getServerState() { return state; } /** * Registers a collection of classes as new deployment. Classes are scanned for the {@link Queue}, {@link Topic}, * {@link Topics} {@link Service}, {@link CoreService} annotations. * * @param deploymentClasses the classes which make up the deployment */ public synchronized void registerDeployment(Class<?>... deploymentClasses) { Preconditions.checkState(state == ServerState.RUNNING); Deployment deployment = new ClassDeployment(deploymentClasses); if (!classDeployments.isRegistered(deployment)) { classDeployments.register(deployment); deploymentEvents.onDeploy(deployment); } } public Forklift getForklift() { return this.forklift; } /** * Launch a Forklift server instance. */ private void launch() { if (setupBrokerAndForklift()) { deploymentEvents = new ConsumerDeploymentEvents(forklift); consumerWatch = setupConsumerWatch(deploymentEvents); propsWatch = setupPropertyWatch(deploymentEvents); this.replayES = setupESReplayHandling(forklift); RetryES retryES = setupESRetryHandling(forklift); if (setupLifeCycleMonitors(replayES, retryES, forklift)) { try { runEventLoop(propsWatch, consumerWatch); } catch (InterruptedException ignored) { } } } if (state != ServerState.STOPPED) { state = ServerState.ERROR; log.info("Forklift server Error state"); } } private boolean setupBrokerAndForklift() { try { forklift = new Forklift(); final ForkliftConnectorI connector = startAndConnectToBroker(); forklift.start(connector); } catch (Exception e) { log.error("Unable to startup broker and forklift", e); } return forklift.isRunning(); } private void runEventLoop(DeploymentWatch propsWatch, DeploymentWatch consumerWatch) throws InterruptedException { state = ServerState.RUNNING; log.info("Forklift server Running"); runningLatch.countDown(); while (state == ServerState.RUNNING) { log.debug("Scanning for new deployments..."); try { if (propsWatch != null) propsWatch.run(); } catch (Throwable e) { log.error("", e); } try { if (consumerWatch != null) consumerWatch.run(); } catch (Throwable e) { log.error("", e); } synchronized (this) { this.wait(SLEEP_INTERVAL); } } } private void shutdown() { synchronized(this){ if(state != ServerState.RUNNING || state != ServerState.ERROR){ return; } state = ServerState.STOPPING; log.info("Forklift server Stopping"); if (replayES != null) { replayES.shutdown(); } if (consumerWatch != null) { consumerWatch.shutdown(); } if (propsWatch != null) { propsWatch.shutdown(); } classDeployments.getAll().forEach(deploy -> deploymentEvents.onUndeploy(deploy)); if (consumerWatch != null) { consumerWatch.shutdown(); } forklift.shutdown(); if (broker != null) { try { broker.stop(); } catch (Exception ignored) { } } state = ServerState.STOPPED; log.info("Forklift server Stopped"); this.notifyAll(); } } private boolean setupLifeCycleMonitors(ReplayES replayES, RetryES retryES, Forklift forklift) { forklift.getLifeCycle().register(StatsCollector.class); boolean setup = true; // Setup retry handling. if (retryES != null) { forklift.getLifeCycle().register(retryES); } if (opts.getRetryDir() != null) { forklift.getLifeCycle().register(new RetryHandler(forklift, new File(opts.getRetryDir()))); } // Always add replay last so that other plugins can update props. if (replayES != null) forklift.getLifeCycle().register(replayES); if (opts.getReplayDir() != null) { try { forklift.getLifeCycle().register(new ReplayLogger(new File(opts.getReplayDir()))); } catch (FileNotFoundException e) { log.error("Unable to find file for Replay Logger", e); setup = false; } } return setup; } private ReplayES setupESReplayHandling(Forklift forklift) { // Create the replay ES first if it's needed just in case we are utilizing the startup of the embedded es engine. final ReplayES replayES; if (opts.getReplayESHost() == null) replayES = null; else replayES = new ReplayES(!opts.isReplayESServer(), opts.getReplayESHost(), opts.getReplayESPort(), opts.getReplayESCluster(), forklift.getConnector()); return replayES; } private RetryES setupESRetryHandling(Forklift forklift) { RetryES retryES = null; if (opts.getRetryESHost() != null) retryES = new RetryES(forklift, opts.isRetryESSsl(), opts.getRetryESHost(), opts.getRetryESPort(), opts.isRunRetries()); return retryES; } private DeploymentWatch setupConsumerWatch(ConsumerDeploymentEvents deploymentEvents) { final DeploymentWatch deploymentWatch; if (opts.getConsumerDir() != null) { deploymentWatch = new DeploymentWatch(new java.io.File(opts.getConsumerDir()), deploymentEvents); log.info("Scanning for Forklift consumers at " + opts.getConsumerDir()); } else { deploymentWatch = null; } return deploymentWatch; } private DeploymentWatch setupPropertyWatch(ConsumerDeploymentEvents deploymentEvents) { final DeploymentWatch propsWatch; if (opts.getPropsDir() != null) { propsWatch = new DeploymentWatch(new java.io.File(opts.getPropsDir()), deploymentEvents); log.info("Scanning for Properties at " + opts.getPropsDir()); } else { propsWatch = null; } return propsWatch; } private ForkliftConnectorI startAndConnectToBroker() throws Exception { String brokerUrl = opts.getBrokerUrl(); if (brokerUrl.startsWith("consul.") && brokerUrl.length() > "consul.".length()) { log.info("Building failover url using consul"); final Consul c = new Consul("http://" + opts.getConsulHost(), 8500); // Build the connection string. final String serviceName = brokerUrl.split("\\.")[1]; brokerUrl = "failover:(" + c.catalog().service(serviceName).getProviders().stream() .filter(srvc -> !srvc.isCritical()) .map(srvc -> "tcp://" + srvc.getAddress() + ":" + srvc.getPort()) .reduce("", (a, b) -> a + "," + b) + ")"; c.shutdown(); brokerUrl = brokerUrl.replaceAll("failover:\\(,", "failover:("); log.info("url {}", brokerUrl); if (brokerUrl.equals("failover:()")) { log.error("No brokers found"); System.exit(-1); } } else if (brokerUrl.startsWith("embed")) { brokerUrl = "tcp://0.0.0.0:61616"; broker = new BrokerService(); broker.addConnector(brokerUrl); broker.addConnector("stomp://0.0.0.0:61613"); broker.start(); } log.info("Connected to broker on " + brokerUrl); return new ActiveMQConnector(brokerUrl); } }