Java tutorial
/** * Licensed to JumpMind Inc under one or more contributor * license agreements. See the NOTICE file distributed * with this work for additional information regarding * copyright ownership. JumpMind Inc licenses this file * to you under the GNU General Public License, version 3.0 (GPLv3) * (the "License"); you may not use this file except in compliance * with the License. * * You should have received a copy of the GNU General Public License, * version 3.0 (GPLv3) along with this library; if not, see * <http://www.gnu.org/licenses/>. * * 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 org.jumpmind.metl.core.runtime; import static org.apache.commons.lang.StringUtils.isBlank; import static org.apache.commons.lang.StringUtils.isNotBlank; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledFuture; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.exception.ExceptionUtils; import org.jumpmind.metl.core.model.Agent; import org.jumpmind.metl.core.model.AgentDeployment; import org.jumpmind.metl.core.model.AgentDeploymentParameter; import org.jumpmind.metl.core.model.AgentStatus; import org.jumpmind.metl.core.model.DeploymentStatus; import org.jumpmind.metl.core.model.EntityRow; import org.jumpmind.metl.core.model.Flow; import org.jumpmind.metl.core.model.FlowParameter; import org.jumpmind.metl.core.model.FlowStep; import org.jumpmind.metl.core.model.Notification; import org.jumpmind.metl.core.model.Resource; import org.jumpmind.metl.core.model.ResourceName; import org.jumpmind.metl.core.model.SettingDefinition; import org.jumpmind.metl.core.model.StartType; import org.jumpmind.metl.core.persist.IConfigurationService; import org.jumpmind.metl.core.persist.IExecutionService; import org.jumpmind.metl.core.runtime.component.IComponentDeploymentListener; import org.jumpmind.metl.core.runtime.component.IComponentRuntimeFactory; import org.jumpmind.metl.core.runtime.component.definition.IComponentDefinitionFactory; import org.jumpmind.metl.core.runtime.component.definition.XMLComponent; import org.jumpmind.metl.core.runtime.flow.FlowRuntime; import org.jumpmind.metl.core.runtime.resource.IResourceFactory; import org.jumpmind.metl.core.runtime.resource.IResourceRuntime; import org.jumpmind.metl.core.runtime.web.IHttpRequestMappingRegistry; import org.jumpmind.metl.core.util.LogUtils; import org.jumpmind.metl.core.util.ThreadUtils; import org.jumpmind.properties.TypedProperties; import org.jumpmind.util.FormatUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.support.CronSequenceGenerator; import org.springframework.scheduling.support.CronTrigger; public class AgentRuntime { final Logger log = LoggerFactory.getLogger(getClass()); Agent agent; boolean started = false; boolean starting = false; boolean stopping = false; Map<AgentDeployment, List<FlowRuntime>> scheduledFlows = Collections.synchronizedMap(new HashMap<>()); Map<AgentDeployment, ScheduledFuture<?>> scheduledDeployments = Collections.synchronizedMap(new HashMap<>()); Map<String, IResourceRuntime> deployedResources = Collections.synchronizedMap(new HashMap<>()); Map<String, String> globalSettings; IConfigurationService configurationService; IComponentRuntimeFactory componentRuntimeFactory; IComponentDefinitionFactory componentDefinitionFactory; IExecutionService executionService; IResourceFactory resourceFactory; ExecutorService flowStepsExecutionThreads; ThreadPoolTaskScheduler flowExecutionScheduler; ScheduledFuture<?> agentRequestHandler; IHttpRequestMappingRegistry httpRequestMappingRegistry; public AgentRuntime(Agent agent, IConfigurationService configurationService, IExecutionService executionService, IComponentRuntimeFactory componentFactory, IComponentDefinitionFactory componentDefinitionFactory, IResourceFactory resourceFactory, IHttpRequestMappingRegistry httpRequestMappingRegistry) { this.agent = agent; this.componentDefinitionFactory = componentDefinitionFactory; this.executionService = executionService; this.configurationService = configurationService; this.componentRuntimeFactory = componentFactory; this.resourceFactory = resourceFactory; this.httpRequestMappingRegistry = httpRequestMappingRegistry; } public boolean cancel(String executionId) { boolean cancelled = false; Collection<List<FlowRuntime>> runtimes = scheduledFlows.values(); for (List<FlowRuntime> list : runtimes) { for (FlowRuntime flowRuntime : list) { if (executionId.equals(flowRuntime.getExecutionId())) { if (flowRuntime.isRunning()) { flowRuntime.cancel(); cancelled = true; // remove? } } } } return cancelled; } public void setAgent(Agent agent) { this.agent = agent; } public synchronized void start() { if (!started && !starting) { starting = true; log.info("Agent '{}' is being started", agent); executionService.markAbandoned(agent.getId()); String agentName = agent.getName().toLowerCase(); if (agentName.startsWith("<")) { agentName = agentName.substring(1); } if (agentName.endsWith(">")) { agentName = agentName.substring(0, agentName.length() - 1); } if (agentName.indexOf(".") > 0) { agentName = agentName.substring(0, agentName.indexOf(".")); } final String namePrefix = LogUtils.normalizeName(agentName); this.flowStepsExecutionThreads = ThreadUtils.createUnboundedThreadPool(namePrefix); this.flowExecutionScheduler = new ThreadPoolTaskScheduler(); this.flowExecutionScheduler.setDaemon(true); this.flowExecutionScheduler.setThreadNamePrefix(namePrefix + "-job-"); /* * Threads are not pre-created. Set this big enough for a typical flow but not too * big since every agent gets their own pool. Additional threads can be obtained if * the entire pool is used. * A common Linux thread limit is 1024 per user. */ this.flowExecutionScheduler.setPoolSize(20); this.flowExecutionScheduler.initialize(); this.globalSettings = configurationService.findGlobalSettingsAsMap(); List<AgentDeployment> deployments = new ArrayList<AgentDeployment>(agent.getAgentDeployments()); for (AgentDeployment deployment : deployments) { deploy(deployment); } agentRequestHandler = this.flowExecutionScheduler.scheduleWithFixedDelay(new AgentRequestHandler(), 10000); agent.setAgentStatus(AgentStatus.RUNNING); configurationService.save(agent); started = true; starting = false; log.info("Agent '{}' has been started", agent); } } public synchronized void stop() { if (started && !stopping) { stopping = true; agentRequestHandler.cancel(true); List<AgentDeployment> deployments = new ArrayList<AgentDeployment>(agent.getAgentDeployments()); for (AgentDeployment deployment : deployments) { stop(deployment); } agent.setAgentStatus(AgentStatus.STOPPED); configurationService.save(agent); started = false; stopping = false; if (flowExecutionScheduler != null) { this.flowExecutionScheduler.destroy(); this.flowExecutionScheduler = null; } if (flowStepsExecutionThreads != null) { this.flowStepsExecutionThreads.shutdownNow(); this.flowStepsExecutionThreads = null; } Collection<IResourceRuntime> resourceCollection = deployedResources.values(); for (IResourceRuntime resource : resourceCollection) { log.info("Stopping the {} resource on the {} agent", resource.getResource().getName(), agent.getName()); resource.stop(); } log.info("Agent '{}' has been stopped", agent); } } public boolean isStarted() { return started; } public synchronized AgentDeployment deploy(Flow flow, Map<String, String> parameters) { AgentDeployment deployment = agent.getAgentDeploymentFor(flow); if (deployment == null) { deployment = new AgentDeployment(flow); deployment.setStatus(DeploymentStatus.REQUEST_ENABLE.name()); deployment.setAgentId(agent.getId()); deployment.setFlow(flow); List<FlowParameter> defaultParameters = flow.getFlowParameters(); for (FlowParameter flowParameter : defaultParameters) { String value = flowParameter.getDefaultValue(); if (parameters != null && parameters.containsKey(flowParameter.getName())) { value = parameters.get(flowParameter.getName()); } deployment.getAgentDeploymentParameters().add(new AgentDeploymentParameter(flowParameter.getName(), value, deployment.getId(), flowParameter.getId())); } agent.getAgentDeployments().remove(deployment); agent.getAgentDeployments().add(deployment); configurationService.save(deployment); List<AgentDeployment> deployments = agent.getAgentDeployments(); deployments.remove(deployment); deployments.add(deployment); deploy(deployment); } else { // lets make sure resources are deployed and up to date deployResources(deployment.getFlow()); } return deployment; } @SuppressWarnings({ "unchecked", "rawtypes" }) public void deployResources(Flow flow) { List<ResourceName> flowResourceNames = configurationService .findResourcesInProject(flow.getProjectVersionId()); for (ResourceName flowResourceName : flowResourceNames) { Resource flowResource = configurationService.findResource(flowResourceName.getId()); IResourceRuntime alreadyDeployed = deployedResources.get(flowResource.getId()); Map<String, SettingDefinition> settings = resourceFactory .getSettingDefinitionsForResourceType(flowResource.getType()); TypedProperties defaultSettings = flowResource.toTypedProperties(settings); TypedProperties overrideSettings = agent.toTypedProperties(flowResource); TypedProperties combined = new TypedProperties(defaultSettings); combined.putAll(overrideSettings); Set<Entry<Object, Object>> entries = combined.entrySet(); for (Entry<Object, Object> entry : entries) { String value = (String) entry.getValue(); if (value != null) { value = FormatUtils.replaceTokens(value, (Map) System.getProperties(), true); entry.setValue(value); } } boolean deploy = true; if (alreadyDeployed != null) { deploy = false; Resource deployedResource = alreadyDeployed.getResource(); TypedProperties alreadyDeployedOverrides = alreadyDeployed.getResourceRuntimeSettings(); // TODO the runtime is already combined. change this TypedProperties alreadyDeployedDefaultSettings = deployedResource.toTypedProperties(settings); TypedProperties alreadyDeployedCombined = new TypedProperties(alreadyDeployedDefaultSettings); alreadyDeployedCombined.putAll(alreadyDeployedOverrides); for (Object key : combined.keySet()) { Object newObj = combined.get(key); Object oldObj = alreadyDeployedCombined.get(key); if (!ObjectUtils.equals(newObj, oldObj)) { deploy = true; break; } } if (deploy) { log.info("Undeploying the {} resource to the {} agent", flowResource.getName(), agent.getName()); alreadyDeployed.stop(); } } if (deploy) { log.info("Deploying the {} resource to the {} agent", flowResource.getName(), agent.getName()); IResourceRuntime resource = resourceFactory.create(flowResource, combined); deployedResources.put(flowResource.getId(), resource); } } } private void deploy(final AgentDeployment deployment) { DeploymentStatus status = deployment.getDeploymentStatus(); if (!status.equals(DeploymentStatus.DISABLED) && !status.equals(DeploymentStatus.REQUEST_DISABLE) && !status.equals(DeploymentStatus.REQUEST_REMOVE)) { try { log.info("Deploying '{}' to '{}'", deployment.getFlow().toString(), agent.getName()); deployResources(deployment.getFlow()); if (scheduledFlows.get(deployment) == null) { scheduledFlows.put(deployment, Collections.synchronizedList(new ArrayList<FlowRuntime>())); } doComponentDeploymentEvent(deployment, (l, f, s, c) -> l.onDeploy(agent, deployment, f, s, c)); if (deployment.asStartType() == StartType.ON_DEPLOY) { scheduleNow(deployment); } else if (deployment.asStartType() == StartType.SCHEDULED_CRON) { String cron = deployment.getStartExpression(); log.info( "Scheduling '{}' on '{}' with a cron expression of '{}' The next run time should be at: {}", new Object[] { deployment.getFlow().toString(), agent.getName(), cron, new CronSequenceGenerator(cron).next(new Date()) }); ScheduledFuture<?> future = this.flowExecutionScheduler.schedule( new FlowRunner(deployment, UUID.randomUUID().toString()), new CronTrigger(cron)); scheduledDeployments.put(deployment, future); } deployment.setStatus(DeploymentStatus.DEPLOYED.name()); deployment.setMessage(""); log.info("Flow '{}' has been deployed", deployment.getFlow().getName()); } catch (Exception e) { log.warn("Failed to start '{}'", deployment.getFlow().getName(), e); deployment.setStatus(DeploymentStatus.ERROR.name()); deployment.setMessage(ExceptionUtils.getRootCauseMessage(e)); } configurationService.save(deployment); } } private void doComponentDeploymentEvent(AgentDeployment deployment, DeployListenerAction method) { Flow flow = deployment.getFlow(); List<FlowStep> steps = flow.getFlowSteps(); for (FlowStep flowStep : steps) { XMLComponent componentDefintion = componentDefinitionFactory .getDefinition(flowStep.getComponent().getType()); if (componentDefintion != null && isNotBlank(componentDefintion.getDeploymentListenerClassName())) { try { IComponentDeploymentListener listener = (IComponentDeploymentListener) Class .forName(componentDefintion.getDeploymentListenerClassName()).newInstance(); if (listener instanceof IHttpRequestMappingRegistryAware) { ((IHttpRequestMappingRegistryAware) listener) .setHttpRequestMappingRegistry(httpRequestMappingRegistry); } method.run(listener, flow, flowStep, componentDefintion); } catch (Exception e) { throw new RuntimeException(e); } } } } public String scheduleNow(AgentDeployment deployment) { return scheduleNow(deployment, null); } public ArrayList<EntityRow> execute(AgentDeployment deployment, Map<String, String> runtimeParameters) throws Exception { log.info("Executing '{}' on '{}' for now", new Object[] { deployment.getName(), agent.getName() }); String executionId = createExecutionId(); if (agent.isAutoRefresh()) { deployment = configurationService.findAgentDeployment(deployment.getId()); deployment.setFlow(configurationService.findFlow(deployment.getFlowId())); } FlowRuntime flowRuntime = new FlowRuntime(deployment, componentRuntimeFactory, componentDefinitionFactory, resourceFactory, flowStepsExecutionThreads, configurationService, executionService); try { flowRuntime.start(executionId, deployedResources, agent, null, globalSettings, runtimeParameters); } catch (Exception ex) { if (ex instanceof RuntimeException) { throw (RuntimeException) ex; } else { throw new RuntimeException(ex); } } finally { flowRuntime.waitForFlowCompletion(); flowRuntime.notifyStepsTheFlowIsComplete(); } List<Throwable> errors = flowRuntime.getAllErrors(); if (errors.size() == 0) { ArrayList<EntityRow> entities = flowRuntime.getEntityResult(); if (entities != null) { return entities; } else { // TODO maybe in the future also check to see if there was a TextResult return null; } } else { for (Throwable throwable : errors) { if (throwable instanceof Exception) { throw (Exception) throwable; } else if (throwable instanceof Error) { throw (Error) throwable; } else { throw new RuntimeException(throwable); } } throw new Exception(flowRuntime.getErrorText(errors)); } } private final String createExecutionId() { return UUID.randomUUID().toString(); } public String scheduleNow(AgentDeployment deployment, Map<String, String> runtimeParameters) { log.info("Scheduling '{}' on '{}' for now", new Object[] { deployment.getName(), agent.getName() }); String executionId = createExecutionId(); this.flowExecutionScheduler.schedule(new FlowRunner(deployment, executionId, runtimeParameters), new Date()); return executionId; } protected void stop(AgentDeployment deployment) { ScheduledFuture<?> future = scheduledDeployments.get(deployment); if (future != null) { future.cancel(true); scheduledDeployments.remove(future); } List<FlowRuntime> flowRuntimes = scheduledFlows.get(deployment); if (flowRuntimes != null) { for (FlowRuntime flowRuntime : flowRuntimes) { if (flowRuntime != null) { try { flowRuntime.cancel(); log.info("Flow '{}' has been undeployed", deployment.getFlow().getName()); } catch (Exception e) { log.warn("Failed to stop '{}'", deployment.getFlow().getName(), e); } } } } } public Collection<IResourceRuntime> getDeployedResources() { return new HashSet<IResourceRuntime>(deployedResources.values()); } public synchronized void undeploy(AgentDeployment deployment) { doComponentDeploymentEvent(deployment, (l, f, s, c) -> l.onUndeploy(agent, deployment, f, s, c)); stop(deployment); configurationService.delete(deployment); agent.getAgentDeployments().remove(deployment); scheduledFlows.remove(deployment); } class FlowRunner implements Runnable { FlowRuntime flowRuntime; String executionId; Map<String, String> runtimeParameters; public FlowRunner(AgentDeployment deployment, String executionId) { this(deployment, executionId, null); } public FlowRunner(AgentDeployment deployment, String executionId, Map<String, String> runtimeParameters) { this.executionId = executionId; this.runtimeParameters = runtimeParameters; if (agent.isAutoRefresh()) { deployment = configurationService.findAgentDeployment(deployment.getId()); deployment.setFlow(configurationService.findFlow(deployment.getFlowId())); } this.flowRuntime = new FlowRuntime(deployment, componentRuntimeFactory, componentDefinitionFactory, resourceFactory, flowStepsExecutionThreads, configurationService, executionService); scheduledFlows.get(deployment).add(flowRuntime); } @Override public void run() { if (isBlank(executionId)) { executionId = createExecutionId(); } AgentDeployment deployment = flowRuntime.getDeployment(); try { log.info("Deployment '{}' is running on the '{}' agent", deployment.getName(), agent.getName()); configurationService.refreshAgentParameters(agent); List<Notification> notifications = configurationService.findNotificationsForDeployment(deployment); flowRuntime.start(executionId, deployedResources, agent, notifications, globalSettings, runtimeParameters); } catch (Exception e) { log.error("Error while waiting for the flow to complete", e); } finally { flowRuntime.waitForFlowCompletion(); flowRuntime.notifyStepsTheFlowIsComplete(); log.info("Scheduled '{}' on '{}' is finished", deployment.getFlow().toString(), agent.getName()); scheduledFlows.get(deployment).remove(flowRuntime); executionId = null; } } } class AgentRequestHandler implements Runnable { @Override public void run() { synchronized (AgentRuntime.this) { configurationService.refresh(agent); for (AgentDeployment deployment : new HashSet<AgentDeployment>(agent.getAgentDeployments())) { DeploymentStatus status = deployment.getDeploymentStatus(); if (status.equals(DeploymentStatus.REQUEST_ENABLE)) { deploy(deployment); } else if (status.equals(DeploymentStatus.REQUEST_REMOVE)) { undeploy(deployment); } else if (status.equals(DeploymentStatus.REQUEST_DISABLE)) { stop(deployment); deployment.setStatus(DeploymentStatus.DISABLED.name()); configurationService.save(deployment); } else { deployResources(deployment.getFlow()); } } if (agent.getStatus().equals(AgentStatus.REQUEST_REFRESH.name())) { log.info("Agent '" + agent.getName() + "' is refreshing settings"); globalSettings = configurationService.findGlobalSettingsAsMap(); agent.setStatus(AgentStatus.RUNNING.name()); configurationService.save(agent); ; } } } } interface DeployListenerAction { public void run(IComponentDeploymentListener listener, Flow flow, FlowStep step, XMLComponent componentDefintion) throws Exception; } }