org.forgerock.openidm.scheduler.impl.TaskScannerJob.java Source code

Java tutorial

Introduction

Here is the source code for org.forgerock.openidm.scheduler.impl.TaskScannerJob.java

Source

/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Portions copyright 2012-2015 ForgeRock AS.
 */

package org.forgerock.openidm.scheduler.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.script.ScriptException;

import org.forgerock.services.context.Context;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.JsonValueException;
import org.forgerock.json.resource.ConnectionFactory;
import org.forgerock.json.resource.PreconditionFailedException;
import org.forgerock.json.resource.QueryRequest;
import org.forgerock.json.resource.QueryResourceHandler;
import org.forgerock.json.resource.Requests;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.openidm.quartz.impl.ExecutionException;
import org.forgerock.openidm.util.ConfigMacroUtil;
import org.forgerock.openidm.util.DateUtil;
import org.forgerock.openidm.util.RequestUtil;
import org.forgerock.script.Script;
import org.forgerock.script.ScriptEntry;
import org.joda.time.DateTime;
import org.joda.time.ReadablePeriod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TaskScannerJob {
    private final static Logger logger = LoggerFactory.getLogger(TaskScannerJob.class);
    private final static DateUtil DATE_UTIL = DateUtil.getDateUtil("UTC");

    private ConnectionFactory connectionFactory;
    private TaskScannerContext taskScannerContext;

    public TaskScannerJob(ConnectionFactory connectionFactory, TaskScannerContext context)
            throws ExecutionException {
        this.connectionFactory = connectionFactory;
        this.taskScannerContext = context;
    }

    /**
     * Starts the task associated with a task scanner event.
     * This method may run synchronously or launch a new thread depending upon the settings in the TaskScannerContext
     * @return identifier associated with this task scan job
     * @throws ExecutionException
     */
    public String startTask() throws ExecutionException {
        int numberOfThreads = taskScannerContext.getNumberOfThreads();
        final ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);

        if (taskScannerContext.getWaitForCompletion()) {
            try {
                performTask(executor);
            } catch (ExecutionException ex) {
                throw ex;
            } finally {
                executor.shutdown();
            }
        } else {
            // Launch a new thread for the whole taskscan process
            taskScannerContext.getContext();
            Runnable command = new Runnable() {
                @Override
                public void run() {
                    try {
                        performTask(executor);
                    } catch (Exception ex) {
                        logger.warn("Taskscanner failed with unexpected exception", ex);
                    } finally {
                        executor.shutdown();
                    }
                }
            };
            new Thread(command).start();
            // Shouldn't need to keep ahold of this, I don't think? Can just start it and let it go
        }
        return taskScannerContext.getTaskScanID();
    }

    /**
     * Performs the task associated with the task scanner event.
     * Runs the query and executes the script across each resulting object.
     *
     * @param executor ExecutorService in which to invoke this task.
     * @throws ExecutionException
     */
    private void performTask(ExecutorService executor) throws ExecutionException {
        taskScannerContext.startJob();
        logger.info("Task {} started from {} with script {}", new Object[] { taskScannerContext.getTaskScanID(),
                taskScannerContext.getInvokerName(), taskScannerContext.getScriptName() });

        JsonValue results;
        taskScannerContext.startQuery();
        try {
            results = fetchAllObjects();
        } catch (ResourceException e1) {
            throw new ExecutionException("Error during query", e1);
        }
        taskScannerContext.endQuery();
        Integer maxRecords = taskScannerContext.getMaxRecords();
        if (maxRecords == null) {
            taskScannerContext.setNumberOfTasksToProcess(results.size());
        } else {
            taskScannerContext.setNumberOfTasksToProcess(Math.min(results.size(), maxRecords));
        }
        logger.debug("TaskScan {} query results: {}", taskScannerContext.getInvokerName(), results.size());
        // TODO jump out early if it's empty?

        // Split and prune the result set according to our max and if we're synchronous or not
        List<JsonValue> resultSets = splitResultsOverThreads(results, taskScannerContext.getNumberOfThreads(),
                maxRecords);
        logger.debug("Split result set into {} units", resultSets.size());

        List<Callable<Object>> todo = new ArrayList<Callable<Object>>();
        for (final JsonValue result : resultSets) {
            Runnable command = new Runnable() {
                @Override
                public void run() {
                    try {
                        performTaskOverSet(result);
                    } catch (Exception ex) {
                        logger.warn("Taskscanner failed with unexpected exception", ex);
                    }
                }
            };
            todo.add(Executors.callable(command));
        }

        try {
            executor.invokeAll(todo);
        } catch (InterruptedException e) {
            // Mark it interrupted
            taskScannerContext.interrupted();
            logger.warn("Task scan '" + taskScannerContext.getTaskScanID() + "' interrupted");
        }
        // Don't mark the job as completed if its been deactivated
        if (!taskScannerContext.isInactive()) {
            taskScannerContext.endJob();
        }

        logger.info("Task '{}' completed. Total time: {}ms. Query time: {}ms. Progress: {}",
                new Object[] { taskScannerContext.getTaskScanID(),
                        taskScannerContext.getStatistics().getJobDuration(),
                        taskScannerContext.getStatistics().getQueryDuration(), taskScannerContext.getProgress() });
    }

    private List<JsonValue> splitResultsOverThreads(JsonValue results, int numberOfThreads, Integer max) {
        List<List<Object>> resultSets = new ArrayList<List<Object>>();
        for (int i = 0; i < numberOfThreads; i++) {
            resultSets.add(new ArrayList<Object>());
        }

        int i = 0;
        for (JsonValue obj : results) {
            if (max != null && i >= max) {
                break;
            }
            resultSets.get(i % numberOfThreads).add(obj.getObject());
            i++;
        }

        List<JsonValue> jsonSets = new ArrayList<JsonValue>();
        for (List<Object> set : resultSets) {
            jsonSets.add(new JsonValue(set));
        }

        return jsonSets;
    }

    private void performTaskOverSet(JsonValue results) throws ExecutionException {
        for (JsonValue input : results) {
            if (taskScannerContext.isCanceled()) {
                logger.info("Task '" + taskScannerContext.getTaskScanID() + "' cancelled. Terminating execution.");
                break; // Jump out quick since we've cancelled the job
            }
            // Check if this object has a STARTED time already
            JsonValue startTime = input.get(taskScannerContext.getStartField());
            String startTimeString = null;
            if (startTime != null && !startTime.isNull()) {
                startTimeString = startTime.asString();
                DateTime startedTime = DATE_UTIL.parseTimestamp(startTimeString);

                // Skip if the startTime + interval has not been passed
                ReadablePeriod period = taskScannerContext.getRecoveryTimeout();
                DateTime expirationDate = startedTime.plus(period);
                if (expirationDate.isAfterNow()) {
                    logger.debug(
                            "Object already started and has not expired. Started at: {}. Timeout: {}. Expires at: {}",
                            new Object[] { DATE_UTIL.formatDateTime(startedTime), period,
                                    DATE_UTIL.formatDateTime(expirationDate) });
                    continue;
                }
            }

            try {
                claimAndExecScript(input, startTimeString);
            } catch (ResourceException e) {
                throw new ExecutionException("Error during claim and execution phase", e);
            }
        }
    }

    /**
     * Flatten a list of parameters and perform a query to fetch all objects from storage.
     *
     * @return JsonValue containing a list of all the retrieved objects
     * @throws ResourceException
     */
    private JsonValue fetchAllObjects() throws ResourceException {
        JsonValue flatParams = flattenJson(taskScannerContext.getScanValue());
        ConfigMacroUtil.expand(flatParams);
        return performQuery(taskScannerContext.getObjectID(), flatParams);
    }

    /**
     * Performs a query on a resource and returns the result set
     * @param resourceID the identifier of the resource to query
     * @param params parameters to supply to the query
     * @return the set of results from the performed query
     * @throws ResourceException
     */
    private JsonValue performQuery(String resourceID, JsonValue params) throws ResourceException {
        final JsonValue queryResults = new JsonValue(new ArrayList<Map<String, Object>>());
        ;

        QueryRequest request = RequestUtil.buildQueryRequestFromParameterMap(resourceID, params.asMap());
        connectionFactory.getConnection().query(taskScannerContext.getContext(), request,
                new QueryResourceHandler() {
                    @Override
                    public boolean handleResource(ResourceResponse resource) {
                        queryResults.add(resource.getContent().getObject());
                        return true;
                    }
                });
        return queryResults;
    }

    /**
     * Performs a read on a resource and returns the result
     * @param resourceID the identifier of the resource to read
     * @return the results from the performed read
     * @throws ResourceException
     */
    private JsonValue performRead(String resourceID) throws ResourceException {
        JsonValue readResults = null;

        readResults = connectionFactory.getConnection()
                .read(taskScannerContext.getContext(), Requests.newReadRequest(resourceID)).getContent();
        return readResults;
    }

    /**
     * Adds an object to a JsonValue and performs an update
     * @param resourceID the resource identifier that the updated value belongs to
     * @param value value to perform the update with
     * @param path JsonPointer to the updated/added field
     * @param obj object to add to the field
     * @return the updated JsonValue
     * @throws ResourceException
     */
    private JsonValue updateValueWithObject(String resourceID, JsonValue value, JsonPointer path, Object obj)
            throws ResourceException {
        ensureJsonPointerExists(path, value);
        value.put(path, obj);
        return performUpdate(resourceID, value);
    }

    /**
     * Performs an update on a given resource with a supplied JsonValue
     * @param resourceID the resource identifier to perform the update on
     * @param value the object to update with
     * @return the updated object
     * @throws ResourceException
     */
    private JsonValue performUpdate(String resourceID, JsonValue value) throws ResourceException {
        String id = value.get("_id").required().asString();
        String fullID = retrieveFullID(resourceID, value);
        String rev = value.get("_rev").required().asString();
        UpdateRequest updateRequest = Requests.newUpdateRequest(fullID, value);
        updateRequest.setRevision(rev);

        connectionFactory.getConnection().update(taskScannerContext.getContext(), updateRequest);
        return retrieveObject(resourceID, id);
    }

    /**
     * Constructs a full object ID from the supplied resourceID and the JsonValue
     * @param resourceID resource ID that the value originates from
     * @param value JsonValue to create the full ID with
     * @return string indicating the full id
     */
    private String retrieveFullID(String resourceID, JsonValue value) {
        String id = value.get("_id").required().asString();
        return retrieveFullID(resourceID, id);
    }

    /**
     * Constructs a full object ID from the supplied resourceID and the objectID
     * @param resourceID resource ID that the object originates from
     * @param objectID ID of some object
     * @return string indicating the full ID
     */
    private String retrieveFullID(String resourceID, String objectID) {
        return resourceID + '/' + objectID;
    }

    /**
     * Fetches an updated copy of some specified object from the given resource
     * @param resourceID the resource identifier to fetch an object from
     * @param value the value to retrieve an updated copy of
     * @return the updated value
     * @throws ResourceException
     */
    private JsonValue retrieveUpdatedObject(String resourceID, JsonValue value)
            throws JsonValueException, ResourceException {
        return retrieveObject(resourceID, value.get("_id").required().asString());
    }

    /**
     * Retrieves a specified object from a resource
     * @param resourceID the resource identifier to fetch the object from
     * @param id the identifier of the object to fetch
     * @return the object retrieved from the resource
     * @throws ResourceException
     */
    private JsonValue retrieveObject(String resourceID, String id) throws ResourceException {
        return performRead(retrieveFullID(resourceID, id));
    }

    private void claimAndExecScript(JsonValue input, String expectedStartDateStr)
            throws ExecutionException, ResourceException {
        String id = input.get("_id").required().asString();
        boolean claimedTask = false;
        boolean retryClaimTask = false;

        JsonPointer startField = taskScannerContext.getStartField();
        JsonPointer completedField = taskScannerContext.getCompletedField();
        String resourceID = taskScannerContext.getObjectID();

        JsonValue _input = input;
        do {
            try {
                retryClaimTask = false;
                _input = updateValueWithObject(resourceID, _input, startField, DATE_UTIL.now());
                _input = updateValueWithObject(resourceID, _input, completedField, null);
                logger.debug("Claimed task and updated StartField: {}", _input);
                claimedTask = true;
            } catch (PreconditionFailedException ex) {
                // If the object changed since we queried, get the latest
                // and check if it's still in a state we want to process the task.
                _input = retrieveObject(resourceID, id);
                String currentStartDateStr = (_input.get(startField) == null) ? null
                        : _input.get(startField).asString();
                String currentCompletedDateStr = (_input.get(completedField) == null) ? null
                        : _input.get(completedField).asString();
                if (currentCompletedDateStr == null
                        && (currentStartDateStr == null || currentStartDateStr.equals(expectedStartDateStr))) {
                    retryClaimTask = true;
                } else {
                    // Someone else managed to update the started field first,
                    // claimed the task. Do not execute it here this run.
                    logger.debug("Task for {} {} was already claimed, ignore.", resourceID, id);
                }
            }
        } while (retryClaimTask && !taskScannerContext.isCanceled());
        if (claimedTask) {
            execScript(_input);
        }
    }

    /**
     * Performs the individual executions of the supplied script
     *
     * Passes <b>"input"</b> and <b>"objectID"</b> to the script.<br>
     *   <b>"objectID"</b> contains the full ID of the supplied object (including resource identifier).
     *      Useful for performing updates.<br>
     *   <b>"input"</b> contains the supplied object
     *
     * @param input value to input to the script
     * @throws ExecutionException
     * @throws ResourceException
     */
    private void execScript(JsonValue input) throws ExecutionException, ResourceException {
        ScriptEntry script = taskScannerContext.getScriptEntry();

        if (script != null) {
            String resourceID = taskScannerContext.getObjectID();
            Context context = taskScannerContext.getContext();

            try {
                Script scope = script.getScript(context);
                scope.put("input", input.getObject());
                scope.put("objectID", retrieveFullID(resourceID, input));

                Object returnedValue = scope.eval();
                JsonValue _input = retrieveUpdatedObject(resourceID, input);
                logger.debug("After script execution: {}", _input);

                if (returnedValue == Boolean.TRUE) {
                    _input = updateValueWithObject(resourceID, _input, taskScannerContext.getCompletedField(),
                            DATE_UTIL.now());
                    taskScannerContext.getStatistics().taskSucceded();
                    logger.debug("Updated CompletedField: {}", _input);
                } else {
                    taskScannerContext.getStatistics().taskFailed();
                }

            } catch (ScriptException se) {
                taskScannerContext.getStatistics().taskFailed();
                String msg = taskScannerContext.getScriptName() + " script invoked by "
                        + taskScannerContext.getInvokerName() + " encountered exception";
                logger.debug(msg, se);
                throw new ExecutionException(msg, se);
            }
        }
    }

    /**
     * Flattens JsonValue into a one-level-deep object
     * @param original original JsonValue object
     * @return flattened JsonValue
     */
    private static JsonValue flattenJson(JsonValue original) {
        return flattenJson("", original);
    }

    /**
     * Flattens JsonValue into a one-level-deep object
     * @param parent name of the parent object (for nested objects)
     * @param original original JsonValue object
     * @return flattened JsonValue
     */
    private static JsonValue flattenJson(String parent, JsonValue original) {
        JsonValue flattened = new JsonValue(new HashMap<String, Object>());
        Iterator<String> iter = original.keys().iterator();
        while (iter.hasNext()) {
            String oKey = iter.next();
            String key = (parent.isEmpty() ? "" : parent + ".") + oKey;

            JsonValue value = original.get(oKey);
            if (value.isMap()) {
                addAllToJson(flattened, flattenJson(key, value));
            } else {
                flattened.put(key, value.getObject());
            }
        }
        return flattened;
    }

    /**
     * Adds all objects from one JsonValue to another (performs a merge).
     * Any values contained in both objects will be overwritten to reflect the values in <b>from</b>
     * <br><br>
     * <i><b>NOTE:</b> this should be a part of JsonValue itself (so we can support merging two JsonValue objects)</i>
     * @param to JsonValue that will have objects added to it
     * @param from JsonValue that will be used as reference for updating
     */
    private static void addAllToJson(JsonValue to, JsonValue from) {
        Iterator<String> iter = from.keys().iterator();
        while (iter.hasNext()) {
            String key = iter.next();
            to.put(key, from.get(key).getObject());
        }
    }

    /**
     * Ensure that some JsonPointer exists within a supplied object so that some object can be placed in that field
     * @param ptr JsonPointer to ensure exists at each level
     * @param obj object to ensure the JsonPointer exists within
     */
    private static void ensureJsonPointerExists(JsonPointer ptr, JsonValue obj) {
        JsonValue refObj = obj;

        for (String p : ptr) {
            if (!refObj.isDefined(p)) {
                refObj.put(p, new JsonValue(new HashMap<String, Object>()));
            }
            refObj = refObj.get(p);
        }
    }
}