/*
* #%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.WorkflowException;
import com.day.cq.workflow.WorkflowService;
import com.day.cq.workflow.WorkflowSession;
import com.day.cq.workflow.exec.Workflow;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
@Component
@Service
public class AEMWorkflowRunnerImpl extends AbstractAEMWorkflowRunner implements BulkWorkflowRunner {
private static final Logger log = LoggerFactory.getLogger(AEMWorkflowRunnerImpl.class);
@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 AEMWorkflowRunnable(config, scheduler, resourceResolverFactory, workflowService, throttledTaskRunner);
}
@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);
if (workspace.getConfig().isPurgeWorkflow()) {
try {
purge(payload);
} catch (WorkflowException e) {
throw new Exception(e);
}
}
}
@Override
public void forceTerminate(Workspace workspace, Payload payload) throws Exception {
final WorkflowSession workflowSession =
workflowService.getWorkflowSession(payload.getResourceResolver().adaptTo(Session.class));
Workflow workflow = null;
fail(workspace, payload);
try {
workflow = payload.getWorkflow();
if (workflow != null) {
if (workflow.isActive()) {
workflowSession.terminateWorkflow(workflow);
log.info("Force Terminated workflow [ {} ]", workflow.getId());
payload.setStatus(Status.FORCE_TERMINATED);
if (workspace.getConfig().isPurgeWorkflow()) {
purge(payload);
}
} else {
log.warn("Trying to force terminate an inactive workflow [ {} ]", workflow.getId());
}
} else {
payload.setStatus(Status.FORCE_TERMINATED);
}
} catch (WorkflowException e) {
throw new Exception(e);
}
}
private void purge(Payload payload) throws PersistenceException, WorkflowException {
Workflow workflow = payload.getWorkflow();
if (workflow != null) {
ResourceResolver resourceResolver = payload.getResourceResolver();
final Resource resource = resourceResolver.getResource(workflow.getId());
if (resource != null) {
try {
String path = resource.getPath();
resource.adaptTo(Node.class).remove();
log.info("Purging working instance [ {} ]", path);
} catch (RepositoryException e) {
throw new PersistenceException("Unable to purge workflow instance node.", e);
}
} else {
log.warn("Could not find workflow instance at [ {} ] to purge.", workflow.getId());
}
}
}
/** Runner's Runnable **/
protected class AEMWorkflowRunnable implements Runnable {
private final ResourceResolverFactory resourceResolverFactory;
private final ThrottledTaskRunner throttledTaskRunner;
private final WorkflowService workflowService;
private final Scheduler scheduler;
private String configPath ;
private String jobName;
public AEMWorkflowRunnable(Config config,
Scheduler scheduler,
ResourceResolverFactory resourceResolverFactory,
WorkflowService workflowService,
ThrottledTaskRunner throttledTaskRunner) {
this.configPath = config.getPath();
this.jobName = config.getWorkspace().getJobName();
this.resourceResolverFactory = resourceResolverFactory;
this.workflowService = workflowService;
this.throttledTaskRunner = throttledTaskRunner;
this.scheduler = scheduler;
}
public void run() {
log.debug("Running Bulk AEM Workflow job [ {} ]", jobName);
ResourceResolver adminResourceResolver = null;
Resource configResource = null;
Config config = null;
Workspace workspace = null;
try {
adminResourceResolver = resourceResolverFactory.getServiceResourceResolver(AUTH_INFO);
configResource = adminResourceResolver.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>();
for (Payload payload : priorActivePayloads) {
log.debug("Checking status of payload [ {} ~> {} ]", payload.getPath(), payload.getPayloadPath());
Workflow workflow;
try {
workflow = payload.getWorkflow();
// First check if workflow is complete (aka not active)
if (workflow == null) {
forceTerminate(workspace, payload);
log.warn("Force terminated payload [ {} ] when running under a non-transient Workflow, as workflow is null.", payload.getPath());
} else {
if (!workflow.isActive()) {
// Workflow has ended, so mark payload as complete
payload.updateWith(workflow);
complete(workspace, payload);
} else {
// If active, check that the workflow has not expired
Calendar expiresAt = Calendar.getInstance();
expiresAt.setTime(workflow.getTimeStarted());
expiresAt.add(Calendar.SECOND, config.getTimeout());
if (!Calendar.getInstance().before(expiresAt)) {
payload.updateWith(workflow);
forceTerminate(workspace, payload);
log.warn("Force terminated payload [ {} ~> {} ] as processing time has expired.", payload.getPath(), payload.getPayloadPath());
} else {
// Finally, if active and not expired, update status and let the workflow continue
payload.updateWith(workflow);
currentActivePayloads.add(payload);
}
}
}
} catch (WorkflowException e) {
// Logged in Payload class
forceTerminate(workspace, payload);
} catch (Exception e) {
log.error("Error while processing payload [ {} ~> {} ]", payload.getPath(), payload.getPayloadPath());
forceTerminate(workspace, payload);
}
}
int capacity = config.getBatchSize() - currentActivePayloads.size();
WorkflowSession workflowSession =
workflowService.getWorkflowSession(adminResourceResolver.adaptTo(Session.class));
WorkflowModel workflowModel = workflowSession.getModel(config.getWorkflowModelId());
boolean dirty = false;
while (capacity > 0) {
// Bring new payloads into the active workspace
Payload payload = onboardNextPayload(workspace);
if (payload != null) {
log.trace("Onboarding payload [ {} ~> {} ]", payload.getPath(), payload.getPayloadPath());
Workflow workflow = workflowSession.startWorkflow(workflowModel,
workflowSession.newWorkflowData("JCR_PATH", payload.getPayloadPath()));
if (workflow != null && workflow.getId() != null) {
// If the workflow and workflowId are not null, then there is something to update the payload w so do that.
payload.updateWith(workflow);
currentActivePayloads.add(payload);
capacity--;
dirty = true;
} else {
log.warn("The workflow or workflow ID is null, so something strange happened to it.");
fail(workspace, payload);
dirty = true;
}
} else {
// This means there is nothing
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 (adminResourceResolver != null) {
adminResourceResolver.close();
}
}
}
}
}