/* * #%L * ACS AEM Commons Bundle * %% * Copyright (C) 2016 Adobe * %% * 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. * #L% */ package com.adobe.acs.commons.workflow.bulk.execution.impl.runners; import com.adobe.acs.commons.fam.ThrottledTaskRunner; import com.adobe.acs.commons.workflow.bulk.execution.BulkWorkflowRunner; import com.adobe.acs.commons.workflow.bulk.execution.model.Config; import com.adobe.acs.commons.workflow.bulk.execution.model.Payload; import com.adobe.acs.commons.workflow.bulk.execution.model.Status; import com.adobe.acs.commons.workflow.bulk.execution.model.Workspace; import com.day.cq.workflow.WorkflowService; import com.day.cq.workflow.WorkflowSession; import com.day.cq.workflow.model.WorkflowModel; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.Service; import org.apache.sling.api.resource.PersistenceException; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ResourceResolverFactory; import org.apache.sling.commons.scheduler.ScheduleOptions; import org.apache.sling.commons.scheduler.Scheduler; import org.apache.sling.event.jobs.JobManager; import org.apache.sling.event.jobs.Queue; import org.apache.sling.event.jobs.Statistics; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.jcr.Session; import java.util.ArrayList; import java.util.List; @Component @Service(value = BulkWorkflowRunner.class) public class AEMTransientWorkflowRunnerImpl extends AbstractAEMWorkflowRunner implements BulkWorkflowRunner { private static final Logger log = LoggerFactory.getLogger(AEMTransientWorkflowRunnerImpl.class); private static final String JOB_QUEUE_NAME = "Granite Workflow Queue"; @Reference private WorkflowService workflowService; @Reference private Scheduler scheduler; @Reference private ResourceResolverFactory resourceResolverFactory; @Reference private ThrottledTaskRunner throttledTaskRunner; @Reference private JobManager jobManager; /** * {@inheritDoc} */ @Override public Runnable getRunnable(final Config config) { return new AEMTransientWorkflowRunnable(config, scheduler, resourceResolverFactory, workflowService, throttledTaskRunner, jobManager); } @Override public ScheduleOptions getOptions(Config config) { ScheduleOptions options = scheduler.NOW(-1, config.getInterval()); options.canRunConcurrently(false); options.onLeaderOnly(true); options.name(config.getWorkspace().getJobName()); return options; } @Override public void complete(Workspace workspace, Payload payload) throws Exception { super.complete(workspace, payload); } @Override public void forceTerminate(Workspace workspace, Payload payload) throws Exception { log.info("Cannot force terminate Transient Workflow for [ {} ]", payload.getPayloadPath()); } /** Runner's Runnable **/ private class AEMTransientWorkflowRunnable implements Runnable { private final ResourceResolverFactory resourceResolverFactory; private final ThrottledTaskRunner throttledTaskRunner; private final WorkflowService workflowService; private final Scheduler scheduler; private final JobManager jobManager; private String configPath ; private String jobName; public AEMTransientWorkflowRunnable(Config config, Scheduler scheduler, ResourceResolverFactory resourceResolverFactory, WorkflowService workflowService, ThrottledTaskRunner throttledTaskRunner, JobManager jobManager) { this.configPath = config.getPath(); this.jobName = config.getWorkspace().getJobName(); this.resourceResolverFactory = resourceResolverFactory; this.workflowService = workflowService; this.throttledTaskRunner = throttledTaskRunner; this.scheduler = scheduler; this.jobManager = jobManager; } public void run() { log.debug("Running Bulk AEM Transient Workflow job [ {} ]", jobName); ResourceResolver serviceResourceResolver = null; Resource configResource = null; Config config = null; Workspace workspace = null; try { serviceResourceResolver = resourceResolverFactory.getServiceResourceResolver(AUTH_INFO); configResource = serviceResourceResolver.getResource(configPath); if (configResource != null) { config = configResource.adaptTo(Config.class); } if (config == null) { log.error("Bulk workflow process resource [ {} ] could not be found. Removing periodic job.", configPath); scheduler.unschedule(jobName); } else { workspace = config.getWorkspace(); if (workspace.isStopped() || workspace.isStopping()) { unscheduleJob(scheduler, jobName, configResource, workspace); stop(workspace); return; } final List<Payload> priorActivePayloads = workspace.getActivePayloads(); final List<Payload> currentActivePayloads = new ArrayList<Payload>(); long capacity = getCapacity(config); log.debug("Transient workflow capacity of [ {} ] for batch size [ {} ]", capacity, config.getBatchSize()); int complete = 0; for (Payload payload : priorActivePayloads) { if (complete++ < capacity) { // Mark the #capacity complete, so we can onboard #capacity mode // Because this is Transient we cant check on actual jobs log.trace("Marked [ {} ] as complete", payload.getPayloadPath()); complete(workspace, payload); } else { break; } } WorkflowSession workflowSession = workflowService.getWorkflowSession(serviceResourceResolver.adaptTo(Session.class)); WorkflowModel workflowModel = workflowSession.getModel(config.getWorkflowModelId()); boolean dirty = false; while (capacity > 0) { if (config.isAutoThrottle()) { throttledTaskRunner.waitForLowCpuAndLowMemory(); } // Bring new payloads into the active workspace Payload payload = onboardNextPayload(workspace); if (payload != null) { log.debug("Onboarding payload w/ Transient WF [ {} ~> {} ]", payload.getPath(), payload.getPayloadPath()); workflowSession.startWorkflow(workflowModel, workflowSession.newWorkflowData("JCR_PATH", payload.getPayloadPath())); payload.setStatus(Status.RUNNING); currentActivePayloads.add(payload); capacity--; dirty = true; } else { // This means there is nothing left to onboard break; } } cleanupActivePayloadGroups(workspace); if (!dirty && currentActivePayloads.size() == 0) { // Check if we are in a completed state for the entire workspace. // We are done! Everything is processed and nothing left to onboard. log.debug("No more payloads found to process. No more work to be done."); complete(workspace); unscheduleJob(scheduler, jobName, configResource, workspace); log.info("Completed Bulk Workflow execution for [ {} ]", config.getPath()); } workspace.commit(); } } catch (Exception e) { log.error("Error processing periodic execution for job [ {} ] for workspace [ {} ]", new String[]{ jobName, workspace.getPath() }, e); unscheduleJob(scheduler, jobName, configResource, workspace); try { stop(workspace); } catch (PersistenceException e1) { log.error("Unable to mark this workspace [ {} ] as stopped.", workspace.getPath(), e1); } } finally { if (serviceResourceResolver != null) { serviceResourceResolver.close(); } } } private long getCapacity(Config config) { final Queue queue = jobManager.getQueue(JOB_QUEUE_NAME); if (queue != null) { final Statistics statistics = queue.getStatistics(); return config.getBatchSize() - statistics.getNumberOfJobs(); } else { log.warn("Could not locate Job Queue named [ {} ] - this often happens on first run when no jobs have been added to the queue.", JOB_QUEUE_NAME); return config.getBatchSize(); } } } }