Java tutorial
/******************************************************************************* * Copyright (c) 2011, 2013 AGETO Service GmbH and others. * All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html. * * Contributors: * Mike Tschierschke - initial API and implementation * Mike Tschierschke - improvements due working on https://bugs.eclipse.org/bugs/show_bug.cgi?id=346996 *******************************************************************************/ package org.eclipse.gyrex.jobs.internal.manager; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.Callable; import javax.inject.Inject; import org.eclipse.gyrex.cloud.services.locking.IExclusiveLock; import org.eclipse.gyrex.cloud.services.locking.ILockService; import org.eclipse.gyrex.cloud.services.queue.IQueue; import org.eclipse.gyrex.cloud.services.queue.IQueueService; import org.eclipse.gyrex.common.identifiers.IdHelper; import org.eclipse.gyrex.context.IRuntimeContext; import org.eclipse.gyrex.jobs.IJob; import org.eclipse.gyrex.jobs.JobState; import org.eclipse.gyrex.jobs.history.IJobHistory; import org.eclipse.gyrex.jobs.history.IJobHistoryEntry; import org.eclipse.gyrex.jobs.internal.JobsActivator; import org.eclipse.gyrex.jobs.internal.JobsDebug; import org.eclipse.gyrex.jobs.internal.storage.CloudPreferncesJobHistoryStorage; import org.eclipse.gyrex.jobs.internal.storage.CloudPreferncesJobStorage; import org.eclipse.gyrex.jobs.internal.util.ContextHashUtil; import org.eclipse.gyrex.jobs.internal.worker.JobInfo; import org.eclipse.gyrex.jobs.manager.IJobManager; import org.eclipse.gyrex.jobs.spi.storage.IJobHistoryStorage; import org.eclipse.gyrex.jobs.spi.storage.JobHistoryEntryStorable; import org.eclipse.gyrex.preferences.ModificationConflictException; import org.eclipse.gyrex.server.settings.SystemSetting; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener; import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent; import org.osgi.service.prefs.BackingStoreException; import org.osgi.service.prefs.Preferences; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.exception.ExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Implementation of a {@link IJobManager} and {@link IJobHistoryManager} that * persists the job data in the {@link Preferences} */ public class JobManagerImpl implements IJobManager { /** * One-time state change trigger. */ private static class StateWatchListener implements IPreferenceChangeListener { private final String jobId; private final JobState state; private final IJobStateWatch stateWatch; public StateWatchListener(final String jobId, final JobState state, final IJobStateWatch stateWatch) { this.jobId = jobId; this.state = state; this.stateWatch = stateWatch; } @Override public void preferenceChange(final PreferenceChangeEvent event) { if (!CloudPreferncesJobStorage.PROPERTY_STATUS.equals(event.getKey())) return; final JobState newState = JobState.toState(event.getNewValue()); if (newState != state) { try { stateWatch.jobStateChanged(jobId); } finally { ((IEclipsePreferences) event.getNode()).removePreferenceChangeListener(this); } } } } private static final Logger LOG = LoggerFactory.getLogger(JobManagerImpl.class); private static final long DEFAULT_MODIFY_LOCK_TIMEOUT = 3000L; private static final long modifyLockTimeout = Long.getLong("gyrex.jobs.modifyLock.timeout", DEFAULT_MODIFY_LOCK_TIMEOUT); private static final int storageMaxNumberOfRetries = SystemSetting .newIntegerSetting("gyrex.jobs.storage.retryAttempts", "The number of retries that should be performed in case a storage operation fails.") .usingDefault(3).create().get(); private static final long storageRetryDelayInMs = Math .max(SystemSetting .newLongSetting("gyrex.jobs.storage.retryDelayInMs", "The time to wait in milli-seconds between retry attempts.") .usingDefault(150L).create().get(), 50L); static final IJobHistory EMPTY_HISTORY = new IJobHistory() { @Override public Collection<IJobHistoryEntry> getEntries() { return Collections.emptyList(); } @Override public String toString() { return "No history available."; }; }; public static IExclusiveLock acquireLock(final JobImpl job) { final String lockId = "gyrex.jobs.modify.".concat(job.getStorageKey()); if (JobsDebug.jobLocks) { LOG.debug("Requesting lock {} for job {}", new Object[] { lockId, job.getId(), new Exception("Call Stack") }); } try { return JobsActivator.getInstance().getService(ILockService.class).acquireExclusiveLock(lockId, null, modifyLockTimeout); } catch (final Exception e) { throw new IllegalStateException(String.format("Unable to get job modify lock. %s", e.getMessage()), e); } } public static void cancel(JobImpl job, final String trigger) { IExclusiveLock jobLock = null; final String jobId = job.getId(); try { // get job modification lock jobLock = acquireLock(job); // re-read job status (inside lock) job = getJob(jobId, job.getStorageKey()); if (null == job) throw new IllegalStateException(String.format("Job %s does not exist!", jobId)); // re-check job status (inside lock) if ((job.getState() != JobState.WAITING) && (job.getState() != JobState.RUNNING)) // no-op return; try { // set state setJobState(job, JobState.ABORTING, jobLock); // add cancellation note setJobCancelled(job, System.currentTimeMillis(), null != trigger ? trigger : findCaller(), jobLock); } catch (final BackingStoreException e) { throw new IllegalStateException(String.format("Error canceling job %s. %s", jobId, e.getMessage()), e); } } finally { releaseLock(jobLock, jobId); } } private static String findCaller() { final StackTraceElement[] trace = Thread.currentThread().getStackTrace(); if (trace.length == 0) return StringUtils.EMPTY; // find first _none_ jobs API call for (int i = 0; i < trace.length; i++) { if (StringUtils.startsWith(trace[i].getClassName(), JobManagerImpl.class.getName())) { continue; } return trace[i].toString(); } return StringUtils.EMPTY; } public static JobImpl getJob(final String jobId, final String storageKey) throws IllegalStateException { try { // don't create node if it doesn't exist if (!CloudPreferncesJobStorage.getJobsNode().nodeExists(storageKey)) return null; // read job return CloudPreferncesJobStorage.readJob(jobId, CloudPreferncesJobStorage.getJobsNode().node(storageKey)); } catch (final Exception e) { throw new IllegalStateException(String.format("Error reading job data. %s", e.getMessage()), e); } } private static void releaseLock(final IExclusiveLock jobLock, final String jobId) { if (null != jobLock) { if (JobsDebug.jobLocks) { LOG.debug("Releasing lock {} for job {}", new Object[] { jobLock.getId(), jobId }); } try { jobLock.release(); } catch (final Exception e) { // ignore } } } /** * records cancellation time and trigger */ private static void setJobCancelled(final JobImpl job, final long timestamp, final String trigger, final IExclusiveLock lock) throws BackingStoreException { if ((null == lock) || !lock.isValid()) throw new IllegalStateException( String.format("Unable to update job %s due to missing or lost job lock!", job.getId())); if (!CloudPreferncesJobStorage.getJobsNode().nodeExists(job.getStorageKey())) // don't update if removed return; // update job node final Preferences jobNode = CloudPreferncesJobStorage.getJobsNode().node(job.getStorageKey()); jobNode.putLong(CloudPreferncesJobStorage.PROPERTY_LAST_CANCELLED, timestamp); jobNode.put(CloudPreferncesJobStorage.PROPERTY_LAST_CANCELLED_TRIGGER, trigger); jobNode.flush(); } private static void setJobState(final JobImpl job, final JobState state, final IExclusiveLock lock) throws BackingStoreException { if (null == state) throw new IllegalArgumentException("job state must not be null"); if ((null == lock) || !lock.isValid()) throw new IllegalStateException( String.format("Unable to update job state of job %s to %s due to missing or lost job lock!", job.getId(), state.toString())); if (!CloudPreferncesJobStorage.getJobsNode().nodeExists(job.getStorageKey())) // don't update if removed return; final Preferences jobNode = CloudPreferncesJobStorage.getJobsNode().node(job.getStorageKey()); if (StringUtils.equals(jobNode.get(CloudPreferncesJobStorage.PROPERTY_STATUS, null), state.name())) // don't update if not different return; // update job node // (note, this might trigger any watches immediately) jobNode.put(CloudPreferncesJobStorage.PROPERTY_STATUS, state.name()); jobNode.flush(); // update job job.setStatus(state); } private final IRuntimeContext context; private final ContextHashUtil contextHash; private final CloudPreferncesJobHistoryStorage cloudHistoryStore; /** * Creates a new instance. */ @Inject public JobManagerImpl(final IRuntimeContext context) { this.context = context; contextHash = new ContextHashUtil(context); cloudHistoryStore = new CloudPreferncesJobHistoryStorage(context); } @Override public void cancelJob(final String jobId, final String trigger) throws IllegalStateException { if (!IdHelper.isValidId(jobId)) throw new IllegalArgumentException(String.format("Invalid id '%s'", jobId)); final JobImpl job = getJob(jobId); if (null == job) throw new IllegalStateException(String.format("Job %s does not exist!", jobId)); // check job status if ((job.getState() != JobState.WAITING) && (job.getState() != JobState.RUNNING)) // no-op return; cancel(job, trigger); } @Override public JobImpl createJob(final String jobTypeId, final String jobId, final Map<String, String> parameter) { if (!IdHelper.isValidId(jobId)) throw new IllegalArgumentException(String.format("Invalid id '%s'", jobId)); if (!IdHelper.isValidId(jobTypeId)) throw new IllegalArgumentException(String.format("Invalid type id '%s'", jobTypeId)); IExclusiveLock jobLock = null; try { final String internalId = toInternalId(jobId); if (CloudPreferncesJobStorage.getJobsNode().nodeExists(internalId)) throw new IllegalStateException(String.format("Job '%s' is already stored", jobId)); // create preference node final Preferences node = CloudPreferncesJobStorage.getJobsNode().node(internalId); node.put(CloudPreferncesJobStorage.PROPERTY_TYPE, jobTypeId); node.flush(); // read job JobImpl job = CloudPreferncesJobStorage.readJob(jobId, node); // acquire lock jobLock = acquireLock(job); // make sure node is in sync syncJobNode(jobId); // re-read job status (inside lock) job = getJob(jobId); if (null == job) throw new IllegalStateException(String.format("Job %s does not exist!", jobId)); // update job parameter setJobParameter(job, parameter, jobLock); // set initial state setJobState(job, JobState.NONE, jobLock); // trigger possible clean-up CloudPreferncesJobStorage.mayTriggerCleanup(); // re-read job (this time with parameter) return CloudPreferncesJobStorage.readJob(jobId, node); } catch (final Exception e) { throw new IllegalStateException(String.format("Error creating job data. %s", e.getMessage()), e); } finally { releaseLock(jobLock, jobId); } } private void doQueueJob(final String jobTypeId, final String jobId, Map<String, String> parameter, final String queueId, final String trigger, final String scheduleInfo) { JobImpl job = getJob(jobId); // if no job type is given, we are not allowed to create a job if ((null == job) && (jobTypeId == null)) throw new IllegalStateException(String.format("Job %s does not exist!", jobId)); // verify the queue exists final IQueue queue = getOrCreateQueue(StringUtils.isNotBlank(queueId) ? queueId : DEFAULT_QUEUE); if (null == queue) throw new IllegalStateException(String.format("Queue %s does not exist!", queueId)); IExclusiveLock jobLock = null; try { // create job if necessary if (job == null) { try { final String internalId = toInternalId(jobId); if (CloudPreferncesJobStorage.getJobsNode().nodeExists(internalId)) throw new IllegalStateException(String.format("Job '%s' is already stored", jobId)); // create preference node final Preferences node = CloudPreferncesJobStorage.getJobsNode().node(internalId); node.put(CloudPreferncesJobStorage.PROPERTY_TYPE, jobTypeId); node.flush(); // read job job = CloudPreferncesJobStorage.readJob(jobId, node); } catch (final BackingStoreException e) { throw new IllegalStateException(String.format("Error creating job data. %s", e.getMessage()), e); } } // acquire lock jobLock = acquireLock(job); // refresh job info syncJobNode(jobId); // re-read job status (inside lock) job = getJob(jobId); if (null == job) throw new IllegalStateException(String.format("Job %s does not exist!", jobId)); // check the job state (inside lock) but ignore stuck jobs if (job.getState() != JobState.NONE) { if (!isStuck(job)) throw new IllegalStateException(String.format( "Job %s cannot be queued because of a job state conflict (expected %s, got %s)!", jobId, JobState.NONE.toString(), job.getState().toString())); else { LOG.warn( "Job {} is in state {}. However, it's assumed stuck. The queue request will reset the state!", job.getId(), job.getState()); } } try { // determine job parameter to use if (null != parameter) { // update job definition with parameter setJobParameter(job, parameter, jobLock); } else { // use parameter from job definition parameter = job.getParameter(); } // set job state setJobState(job, JobState.WAITING, jobLock); // collect queue info final String queueTrigger = null != trigger ? trigger : findCaller(); final long queueTimestamp = System.currentTimeMillis(); // add to queue queue.sendMessage(JobInfo.asMessage(new JobInfo(job.getTypeId(), jobId, context.getContextPath(), parameter, queueTrigger, queueTimestamp, scheduleInfo, job.getLastSuccessfulStart()))); // set queued time try { setJobQueued(job, queueTimestamp, queueTrigger, jobLock); } catch (final Exception e) { // we must not fail at this point, the job has been queued already LOG.warn("Unable to set job queue time for job {}: {}", jobId, ExceptionUtils.getRootCauseMessage(e)); } } catch (final Exception e) { // try to reset the job state try { setJobState(job, JobState.NONE, jobLock); } catch (final Exception resetException) { LOG.error("Unable to reset job state for job {}: {}", jobId, ExceptionUtils.getRootCauseMessage(e)); } throw new IllegalStateException(String.format("Error queuing job %s. %s", jobId, e.getMessage()), e); } } finally { releaseLock(jobLock, jobId); } // trigger possible clean-up CloudPreferncesJobStorage.mayTriggerCleanup(); } private <T> T executeWithRetry(final Callable<T> c) throws Exception { int attempt = 0; while (true) { try { return c.call(); } catch (final ModificationConflictException e) { // try up to max retries if (++attempt > storageMaxNumberOfRetries) throw e; // wait a bit Thread.sleep(storageRetryDelayInMs); } } } @Override public IJobHistory getHistory(final String jobId) throws IllegalStateException { final JobImpl job = getJob(jobId); if (null == job) throw new IllegalStateException(String.format("Job '%s' does not exist.", jobId)); final IJobHistoryStorage storage = getJobHistoryStore(); if (storage == null) return EMPTY_HISTORY; try { final int count = storage.count(jobId); if (count > 0) return new StorageBackedJobHistory(jobId, storage); } catch (final Exception e) { throw new IllegalStateException("Error reading history", e); } return EMPTY_HISTORY; } @Override public JobImpl getJob(final String jobId) { if (!IdHelper.isValidId(jobId)) throw new IllegalArgumentException(String.format("Invalid id '%s'", jobId)); return getJob(jobId, toInternalId(jobId)); } public IJobHistoryStorage getJobHistoryStore() { try { final IJobHistoryStorage historyStorage = context.get(IJobHistoryStorage.class); if (historyStorage != null) return historyStorage; } catch (Exception | LinkageError | AssertionError e) { LOG.error( "Error accessing job history storage provider (context {}). Fallback to cloud based store. {}", context.getContextPath(), ExceptionUtils.getRootCauseMessage(e), e); } // fallback to cloud history store for backwards compatibility return cloudHistoryStore; } @Override public Collection<String> getJobs() { try { final String[] storageIds = CloudPreferncesJobStorage.getJobsNode().childrenNames(); final List<String> jobIds = new ArrayList<String>(storageIds.length); for (final String internalId : storageIds) { if (contextHash.isInternalId(internalId)) { jobIds.add(toExternalId(internalId)); } } return Collections.unmodifiableCollection(jobIds); } catch (final BackingStoreException e) { throw new IllegalStateException(String.format("Error reading job data. %s", e.getMessage()), e); } } @Override public Collection<String> getJobsByState(final JobState state) { if (null == state) throw new IllegalArgumentException("Status must not be null"); try { final String[] storageIds = CloudPreferncesJobStorage.getJobsNode().childrenNames(); final List<String> jobIds = new ArrayList<String>(storageIds.length); for (final String internalId : storageIds) { if (contextHash.isInternalId(internalId) && StringUtils.equals(CloudPreferncesJobStorage.getJobsNode().node(internalId) .get(CloudPreferncesJobStorage.PROPERTY_STATUS, null), state.name())) { jobIds.add(toExternalId(internalId)); } } return Collections.unmodifiableCollection(jobIds); } catch (final BackingStoreException e) { throw new IllegalStateException(String.format("Error reading job data. %s", e.getMessage()), e); } } private IQueue getOrCreateQueue(final String queueId) { final IQueueService queueService = JobsActivator.getInstance().getQueueService(); IQueue queue = queueService.getQueue(queueId, null); if (queue == null) { queue = queueService.createQueue(queueId, null); } return queue; } public boolean isStuck(final JobImpl job) { // just check but don't log a warning return JobHungDetectionHelper.isStuck(toInternalId(job.getId()), job, false); } @Override public void queueJob(final String jobId, final Map<String, String> parameter, final String queueId, final String trigger) throws IllegalArgumentException, IllegalStateException { if (!IdHelper.isValidId(jobId)) throw new IllegalArgumentException(String.format("Invalid id '%s'", jobId)); doQueueJob(null, jobId, parameter, queueId, trigger, null); } @Override public void queueJob(final String jobTypeId, final String jobId, final Map<String, String> parameter, final String queueId, final String trigger) throws IllegalArgumentException, IllegalStateException { if (!IdHelper.isValidId(jobTypeId)) throw new IllegalArgumentException(String.format("Invalid job type id '%s'", jobTypeId)); if (!IdHelper.isValidId(jobId)) throw new IllegalArgumentException(String.format("Invalid id '%s'", jobId)); doQueueJob(jobTypeId, jobId, parameter, queueId, trigger, null); } /** * @noreference This method is not intended to be referenced by clients. */ public void queueJob(final String jobTypeId, final String jobId, final Map<String, String> parameter, final String queueId, final String trigger, final String scheduleInfo) throws IllegalArgumentException, IllegalStateException { if (!IdHelper.isValidId(jobTypeId)) throw new IllegalArgumentException(String.format("Invalid job type id '%s'", jobTypeId)); if (!IdHelper.isValidId(jobId)) throw new IllegalArgumentException(String.format("Invalid id '%s'", jobId)); doQueueJob(jobTypeId, jobId, parameter, queueId, trigger, scheduleInfo); } @Override public void queueJob(final String jobId, final String queueId, final String trigger) { if (!IdHelper.isValidId(jobId)) throw new IllegalArgumentException(String.format("Invalid id '%s'", jobId)); doQueueJob(null, jobId, null, queueId, trigger, null); } @Override public void removeJob(final String jobId) { if (!IdHelper.isValidId(jobId)) throw new IllegalArgumentException(String.format("Invalid id '%s'", jobId)); final JobImpl job = getJob(jobId); if (null == job) return; // check the job state if (job.getState() != JobState.NONE) throw new IllegalStateException( String.format("Job %s cannot be removed because of a job state conflict (expected %s, got %s)!", jobId, JobState.NONE.toString(), job.getState().toString())); IExclusiveLock jobLock = null; try { // get job modification lock jobLock = acquireLock(job); // remove from preferences final String internalId = toInternalId(jobId); final IEclipsePreferences jobsNode = CloudPreferncesJobStorage.getJobsNode(); executeWithRetry(new Callable<Object>() { @Override public Object call() throws Exception { if (jobsNode.nodeExists(internalId)) { jobsNode.node(internalId).removeNode(); jobsNode.flush(); } return null; } }); } catch (final Exception e) { throw new IllegalStateException(String.format("Error removing job %s. %s", jobId, e.getMessage()), e); } finally { releaseLock(jobLock, jobId); } } /** * Marks a job active in the system. * <p> * This must be called by the worker engine to indicate that a job left the * queue and is now scheduled locally and/or running. * </p> * * @param jobId * @noreference This method is not intended to be referenced by clients. */ public void setActive(final String jobId) throws Exception { if (JobsDebug.debug) { LOG.debug("Marking job {} active.", jobId); } JobHungDetectionHelper.setActive(toInternalId(jobId)); final Preferences jobNode = CloudPreferncesJobStorage.getJobsNode().node(toInternalId(jobId)); executeWithRetry(new Callable<Object>() { @Override public Object call() throws Exception { jobNode.sync(); jobNode.putBoolean(CloudPreferncesJobStorage.PROPERTY_ACTIVE, true); jobNode.flush(); return null; } }); } /** * Marks a job inactive in the system. * <p> * This must be called by the worker engine to indicate that a job finished * running locally (either successful or canceled). * </p> * * @param jobId * @noreference This method is not intended to be referenced by clients. */ public void setInactive(final String jobId) throws Exception { if (JobsDebug.debug) { LOG.debug("Marking job {} inactive.", jobId); } JobHungDetectionHelper.setInactive(toInternalId(jobId)); final Preferences jobNode = CloudPreferncesJobStorage.getJobsNode().node(toInternalId(jobId)); executeWithRetry(new Callable<Object>() { @Override public Object call() throws Exception { jobNode.sync(); jobNode.remove(CloudPreferncesJobStorage.PROPERTY_ACTIVE); jobNode.flush(); return null; } }); } private void setJobParameter(final JobImpl job, final Map<String, String> parameter, final IExclusiveLock lock) throws BackingStoreException { if ((null == lock) || !lock.isValid()) throw new IllegalStateException(String .format("Unable to update parameter of job %s due to missing or lost job lock!", job.getId())); final String internalId = toInternalId(job.getId()); if (!CloudPreferncesJobStorage.getJobsNode().nodeExists(internalId)) // don't update if removed return; final Preferences jobNode = CloudPreferncesJobStorage.getJobsNode().node(internalId); if ((null != parameter) && !parameter.isEmpty()) { final Preferences paramNode = jobNode.node(CloudPreferncesJobStorage.NODE_PARAMETER); // remove obsolete keys for (final String key : paramNode.keys()) { if (StringUtils.isBlank(parameter.get(key))) { paramNode.remove(key); } } // add updated/new parameter for (final Entry<String, String> entry : parameter.entrySet()) { if (StringUtils.isNotBlank(entry.getValue())) { paramNode.put(entry.getKey(), entry.getValue()); } } } else { if (jobNode.nodeExists(CloudPreferncesJobStorage.NODE_PARAMETER)) { jobNode.node(CloudPreferncesJobStorage.NODE_PARAMETER).removeNode(); } } // flush jobNode.flush(); // update job (create a copy to prevent modifications from outside) job.setParameter(parameter != null ? new HashMap<String, String>(parameter) : null); } @Override public void setJobParameter(final String jobId, final Map<String, String> parameter) throws IllegalStateException, IllegalArgumentException { if (!IdHelper.isValidId(jobId)) throw new IllegalArgumentException(String.format("Invalid id '%s'", jobId)); if (null == parameter) throw new IllegalArgumentException("parameter must not be null"); JobImpl job = getJob(jobId); if (null == job) throw new IllegalStateException(String.format("Job %s does not exist!", jobId)); // check the job state (but ignore stuck jobs) if ((job.getState() != JobState.NONE) && !isStuck(job)) throw new IllegalStateException( String.format("Job %s cannot be updated because of a job state conflict (expected %s, got %s)!", jobId, JobState.NONE.toString(), job.getState().toString())); IExclusiveLock jobLock = null; try { // acquire lock jobLock = acquireLock(job); // re-read job status (inside lock) job = getJob(jobId); if (null == job) throw new IllegalStateException(String.format("Job %s does not exist!", jobId)); // re-check the job state (inside lock) but ignore stuck jobs if (job.getState() != JobState.NONE) { if (!isStuck(job)) throw new IllegalStateException(String.format( "Job %s cannot be updated because of a job state conflict (expected %s, got %s)!", jobId, JobState.NONE.toString(), job.getState().toString())); else { LOG.warn( "Job {} is in state {}. However, it's assumed stuck. The updated will reset the state!", job.getId(), job.getState()); } } // update job parameter setJobParameter(job, parameter, jobLock); // reset state setJobState(job, JobState.NONE, jobLock); } catch (final BackingStoreException e) { throw new IllegalStateException(String.format("Error updating job parameter. %s", e.getMessage()), e); } finally { releaseLock(jobLock, jobId); } } /** * records queuing time and trigger. */ private void setJobQueued(final IJob job, final long timestamp, final String trigger, final IExclusiveLock lock) throws BackingStoreException { if ((null == lock) || !lock.isValid()) throw new IllegalStateException( String.format("Unable to update job %s due to missing or lost job lock!", job.getId())); final String internalId = toInternalId(job.getId()); if (!CloudPreferncesJobStorage.getJobsNode().nodeExists(internalId)) // don't update if removed return; // update job node final Preferences jobNode = CloudPreferncesJobStorage.getJobsNode().node(internalId); jobNode.putLong(CloudPreferncesJobStorage.PROPERTY_LAST_QUEUED, timestamp); jobNode.put(CloudPreferncesJobStorage.PROPERTY_LAST_QUEUED_TRIGGER, trigger); jobNode.flush(); } private void setJobResult(final JobImpl job, final Map<String, String> parameter, final IStatus result, final long resultTimestamp, final long startTimestamp, final String queueTrigger, final long queueTimestamp, final IExclusiveLock lock) throws BackingStoreException { if ((null == lock) || !lock.isValid()) throw new IllegalStateException(String .format("Unable to update job result of job %s due to missing or lost job lock!", job.getId())); if (null == result) throw new IllegalStateException( String.format("Unable to update job result of job %s due to missing result!", job.getId())); final String internalId = toInternalId(job.getId()); if (!CloudPreferncesJobStorage.getJobsNode().nodeExists(internalId)) // don't update if removed return; // update job node final Preferences jobNode = CloudPreferncesJobStorage.getJobsNode().node(internalId); jobNode.putLong(CloudPreferncesJobStorage.PROPERTY_LAST_RESULT, resultTimestamp); jobNode.put(CloudPreferncesJobStorage.PROPERTY_LAST_RESULT_MESSAGE, StringUtils.left(CloudPreferncesJobHistoryStorage.getFormattedMessage(result, 0), CloudPreferncesJobHistoryStorage.MAX_RESULT_MESSAGE_SIZE)); jobNode.putInt(CloudPreferncesJobStorage.PROPERTY_LAST_RESULT_SEVERITY, result.getSeverity()); if (!result.matches(IStatus.CANCEL | IStatus.ERROR)) { // every run that does not result in ERROR or CANCEL is considered successful jobNode.putLong(CloudPreferncesJobStorage.PROPERTY_LAST_SUCCESSFUL_FINISH, resultTimestamp); jobNode.putLong(CloudPreferncesJobStorage.PROPERTY_LAST_SUCCESSFUL_START, startTimestamp); } jobNode.flush(); // save history final IJobHistoryStorage storage = getJobHistoryStore(); if (storage != null) { final JobHistoryEntryStorable storable = new JobHistoryEntryStorable(); storable.setResult(result); storable.setTimestamp(resultTimestamp); storable.setParameter(parameter); storable.setQueuedTrigger(queueTrigger); // only pass cancellation trigger to history if it makes sense // (is there a more reliable way to pass the cancel trigger to the history?) if ((job.getLastCancelled() > job.getLastQueued()) && (job.getLastQueued() < resultTimestamp)) { storable.setCancelledTrigger(job.getLastCancelledTrigger()); } try { storage.add(job.getId(), storable); } catch (final Exception e) { LOG.error("Error persisting job history for job '{}' (context {}). {}", job.getId(), context.getContextPath(), ExceptionUtils.getRootCauseMessage(e), e); } } } private void setJobStartTime(final IJob job, final long startTimestamp, final IExclusiveLock lock) throws BackingStoreException { if ((null == lock) || !lock.isValid()) throw new IllegalStateException( String.format("Unable to update job %s due to missing or lost job lock!", job.getId())); final String internalId = toInternalId(job.getId()); if (!CloudPreferncesJobStorage.getJobsNode().nodeExists(internalId)) // don't update if removed return; // update job node final Preferences jobNode = CloudPreferncesJobStorage.getJobsNode().node(internalId); jobNode.putLong(CloudPreferncesJobStorage.PROPERTY_LAST_START, startTimestamp); jobNode.flush(); } /** * Updates the state in an atomic way. * <p> * When the job state was changes successfully a watch will be registered * which will be informed in case of the next state change * </p> * * @param jobId * the job id * @param expected * the expected state * @param state * the new state * @param stateWatch * a watch to add * @param stateTimestamp * a timestamp to associate with the state change * @return <code>true</code> if and only if the job state was changed to the * given state successfully * @throws IllegalArgumentException * if any of the arguments is invalid * @throws IllegalStateException * if the job does not exist or an error occured updating the * job */ public boolean setJobState(final String jobId, final JobState expected, final JobState state, final IJobStateWatch stateWatch, final long stateTimestamp) throws IllegalArgumentException, IllegalStateException { if (!IdHelper.isValidId(jobId)) throw new IllegalArgumentException(String.format("Invalid id '%s'", jobId)); if (null == state) throw new IllegalArgumentException("Job state must not be null"); JobImpl job = getJob(jobId); if (null == job) throw new IllegalStateException(String.format("Job %s does not exist!", jobId)); IExclusiveLock jobLock = null; try { // get job modification lock jobLock = acquireLock(job); // make sure node is in sync syncJobNode(jobId); // re-read job status (inside lock) job = getJob(jobId); if (null == job) throw new IllegalStateException(String.format("Job %s does not exist!", jobId)); // check job status (inside lock) but ignore for stuck jobs if ((null != expected) && (job.getState() != expected)) { if (!isStuck(job)) // no-op return false; else { LOG.warn( "Job {} is in state {} which doesn't match the expected state {}. However, it's assumed stuck. Thus, the status will be reset to {}!", new Object[] { job.getId(), job.getState(), expected, state }); } } try { // set state setJobState(job, state, jobLock); // update last run time if new state is RUNNING if (state == JobState.RUNNING) { // set start time setJobStartTime(job, stateTimestamp, jobLock); } // add watch if (null != stateWatch) { ((IEclipsePreferences) CloudPreferncesJobStorage.getJobsNode().node(toInternalId(jobId))) .addPreferenceChangeListener(new StateWatchListener(jobId, state, stateWatch)); } // report update success return true; } catch (final BackingStoreException e) { throw new IllegalStateException(String.format("Error updating state of job %s to %s. %s", jobId, state.name(), e.getMessage()), e); } } finally { releaseLock(jobLock, jobId); } } public void setResult(final String jobId, final Map<String, String> parameter, final IStatus result, final long resultTimestamp, final long startTimestamp, final String queueTrigger, final long queueTimestamp) { if (!IdHelper.isValidId(jobId)) throw new IllegalArgumentException(String.format("Invalid id '%s'", jobId)); JobImpl job = getJob(jobId); if (null == job) throw new IllegalStateException(String.format("Job %s does not exist!", jobId)); IExclusiveLock jobLock = null; try { // get job modification lock jobLock = acquireLock(job); // refresh job info syncJobNode(jobId); try { // make sure node is in sync syncJobNode(jobId); // re-read job status (inside lock) job = getJob(jobId); if (null == job) throw new IllegalStateException(String.format("Job %s does not exist!", jobId)); // set state setJobState(job, JobState.NONE, jobLock); // set result setJobResult(job, parameter, result, resultTimestamp, startTimestamp, queueTrigger, queueTimestamp, jobLock); } catch (final BackingStoreException e) { throw new IllegalStateException( String.format("Error setting result of job %s. %s", jobId, e.getMessage()), e); } } finally { releaseLock(jobLock, jobId); } } private void syncJobNode(final String jobId) { try { executeWithRetry(new Callable<Object>() { @Override public Object call() throws Exception { CloudPreferncesJobStorage.getJobsNode().node(toInternalId(jobId)).sync(); return null; } }); } catch (final Exception e) { LOG.warn("Exception refreshing job {}. Available job data might be stale.", toInternalId(jobId), e); } } private String toExternalId(final String internalId) { return contextHash.toExternalId(internalId); } private String toInternalId(final String id) { return contextHash.toInternalId(id); } }