All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.liferay.portal.scheduler.quartz.internal.QuartzSchedulerEngine Maven / Gradle / Ivy

/**
 * SPDX-FileCopyrightText: (c) 2000 Liferay, Inc. https://liferay.com
 * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06
 */

package com.liferay.portal.scheduler.quartz.internal;

import com.liferay.petra.string.StringBundler;
import com.liferay.portal.kernel.dao.db.DBManagerUtil;
import com.liferay.portal.kernel.dao.db.DBType;
import com.liferay.portal.kernel.json.JSONFactory;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.messaging.Message;
import com.liferay.portal.kernel.messaging.MessageBus;
import com.liferay.portal.kernel.model.Release;
import com.liferay.portal.kernel.module.service.Snapshot;
import com.liferay.portal.kernel.scheduler.JobState;
import com.liferay.portal.kernel.scheduler.JobStateSerializeUtil;
import com.liferay.portal.kernel.scheduler.SchedulerEngine;
import com.liferay.portal.kernel.scheduler.SchedulerEngineAuditor;
import com.liferay.portal.kernel.scheduler.SchedulerEngineHelper;
import com.liferay.portal.kernel.scheduler.SchedulerException;
import com.liferay.portal.kernel.scheduler.StorageType;
import com.liferay.portal.kernel.scheduler.TriggerState;
import com.liferay.portal.kernel.scheduler.messaging.SchedulerResponse;
import com.liferay.portal.kernel.util.GetterUtil;
import com.liferay.portal.kernel.util.PortalRunMode;
import com.liferay.portal.kernel.util.Props;
import com.liferay.portal.kernel.util.PropsKeys;
import com.liferay.portal.kernel.util.ServerDetector;
import com.liferay.portal.kernel.util.Validator;
import com.liferay.portal.scheduler.quartz.internal.job.MessageSenderJob;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;

import org.quartz.Calendar;
import org.quartz.JobBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.JobPersistenceException;
import org.quartz.ListenerManager;
import org.quartz.ObjectAlreadyExistsException;
import org.quartz.Scheduler;
import org.quartz.SchedulerContext;
import org.quartz.Trigger;
import org.quartz.TriggerKey;
import org.quartz.TriggerUtils;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.jdbcjobstore.UpdateLockRowSemaphore;
import org.quartz.impl.matchers.GroupMatcher;
import org.quartz.listeners.SchedulerListenerSupport;
import org.quartz.spi.OperableTrigger;

/**
 * @author Michael C. Han
 * @author Bruno Farache
 * @author Shuyang Zhou
 * @author Wesley Gong
 * @author Tina Tian
 * @author Edward C. Han
 */
@Component(
	enabled = false, property = "scheduler.engine.proxy=false",
	service = SchedulerEngine.class
)
public class QuartzSchedulerEngine implements SchedulerEngine {

	@Override
	public void delete(String groupName, StorageType storageType)
		throws SchedulerException {

		try {
			Scheduler scheduler = _getScheduler(storageType);

			groupName = _fixMaxLength(
				groupName, _groupNameMaxLength, storageType);

			Set jobKeys = scheduler.getJobKeys(
				GroupMatcher.jobGroupEquals(groupName));

			for (JobKey jobKey : jobKeys) {
				scheduler.deleteJob(jobKey);
			}
		}
		catch (Exception exception) {
			throw new SchedulerException(
				"Unable to delete jobs in group " + groupName, exception);
		}
	}

	@Override
	public void delete(
			String jobName, String groupName, StorageType storageType)
		throws SchedulerException {

		try {
			Scheduler scheduler = _getScheduler(storageType);

			jobName = _fixMaxLength(jobName, _jobNameMaxLength, storageType);
			groupName = _fixMaxLength(
				groupName, _groupNameMaxLength, storageType);

			JobKey jobKey = new JobKey(jobName, groupName);

			scheduler.deleteJob(jobKey);
		}
		catch (Exception exception) {
			throw new SchedulerException(
				StringBundler.concat(
					"Unable to delete job {jobName=", jobName, ", groupName=",
					groupName, "}"),
				exception);
		}
	}

	public int getDescriptionMaxLength() {
		return _descriptionMaxLength;
	}

	public int getGroupNameMaxLength() {
		return _groupNameMaxLength;
	}

	public int getJobNameMaxLength() {
		return _jobNameMaxLength;
	}

	@Override
	public SchedulerResponse getScheduledJob(
			String jobName, String groupName, StorageType storageType)
		throws SchedulerException {

		try {
			Scheduler scheduler = _getScheduler(storageType);

			jobName = _fixMaxLength(jobName, _jobNameMaxLength, storageType);
			groupName = _fixMaxLength(
				groupName, _groupNameMaxLength, storageType);

			JobKey jobKey = new JobKey(jobName, groupName);

			return getScheduledJob(scheduler, jobKey);
		}
		catch (Exception exception) {
			throw new SchedulerException(
				StringBundler.concat(
					"Unable to get job {jobName=", jobName, ", groupName=",
					groupName, "}"),
				exception);
		}
	}

	@Override
	public List getScheduledJobs()
		throws SchedulerException {

		try {
			List groupNames = _persistedScheduler.getJobGroupNames();

			List schedulerResponses = new ArrayList<>();

			for (String groupName : groupNames) {
				schedulerResponses.addAll(
					getScheduledJobs(_persistedScheduler, groupName, null));
			}

			groupNames = _memoryScheduler.getJobGroupNames();

			for (String groupName : groupNames) {
				schedulerResponses.addAll(
					getScheduledJobs(_memoryScheduler, groupName, null));
			}

			return schedulerResponses;
		}
		catch (Exception exception) {
			throw new SchedulerException("Unable to get jobs", exception);
		}
	}

	@Override
	public List getScheduledJobs(StorageType storageType)
		throws SchedulerException {

		try {
			Scheduler scheduler = _getScheduler(storageType);

			List groupNames = scheduler.getJobGroupNames();

			List schedulerResponses = new ArrayList<>();

			for (String groupName : groupNames) {
				schedulerResponses.addAll(
					getScheduledJobs(scheduler, groupName, storageType));
			}

			return schedulerResponses;
		}
		catch (Exception exception) {
			throw new SchedulerException(
				"Unable to get jobs with type " + storageType, exception);
		}
	}

	@Override
	public List getScheduledJobs(
			String groupName, StorageType storageType)
		throws SchedulerException {

		try {
			return getScheduledJobs(
				_getScheduler(storageType), groupName, storageType);
		}
		catch (Exception exception) {
			throw new SchedulerException(
				"Unable to get jobs in group " + groupName, exception);
		}
	}

	@Override
	public void pause(String jobName, String groupName, StorageType storageType)
		throws SchedulerException {

		try {
			Scheduler scheduler = _getScheduler(storageType);

			jobName = _fixMaxLength(jobName, _jobNameMaxLength, storageType);
			groupName = _fixMaxLength(
				groupName, _groupNameMaxLength, storageType);

			JobKey jobKey = new JobKey(jobName, groupName);

			scheduler.pauseJob(jobKey);

			_updateJobState(scheduler, jobKey, TriggerState.PAUSED);
		}
		catch (Exception exception) {
			throw new SchedulerException(
				StringBundler.concat(
					"Unable to pause job {jobName=", jobName, ", groupName=",
					groupName, "}"),
				exception);
		}
	}

	@Override
	public void resume(
			String jobName, String groupName, StorageType storageType)
		throws SchedulerException {

		try {
			Scheduler scheduler = _getScheduler(storageType);

			jobName = _fixMaxLength(jobName, _jobNameMaxLength, storageType);
			groupName = _fixMaxLength(
				groupName, _groupNameMaxLength, storageType);

			JobKey jobKey = new JobKey(jobName, groupName);

			scheduler.resumeJob(jobKey);

			_updateJobState(scheduler, jobKey, TriggerState.NORMAL);
		}
		catch (Exception exception) {
			throw new SchedulerException(
				StringBundler.concat(
					"Unable to resume job {jobName=", jobName, ", groupName=",
					groupName, "}"),
				exception);
		}
	}

	@Override
	public void run(
			long companyId, String jobName, String groupName,
			StorageType storageType)
		throws SchedulerException {

		SchedulerResponse schedulerResponse = getScheduledJob(
			jobName, groupName, storageType);

		Message message = schedulerResponse.getMessage();

		message.put(
			SchedulerEngine.DESTINATION_NAME,
			schedulerResponse.getDestinationName());
		message.put(SchedulerEngine.GROUP_NAME, groupName);
		message.put(SchedulerEngine.JOB_NAME, jobName);
		message.put("companyId", companyId);

		_messageBus.sendMessage(
			schedulerResponse.getDestinationName(), message);
	}

	@Override
	public void schedule(
			com.liferay.portal.kernel.scheduler.Trigger trigger,
			String description, String destination, Message message,
			StorageType storageType)
		throws SchedulerException {

		try {
			Trigger quartzTrigger = (Trigger)trigger.getWrappedTrigger();

			if (quartzTrigger == null) {
				return;
			}

			Scheduler scheduler = _getScheduler(storageType);

			description = _fixMaxLength(
				description, _descriptionMaxLength, storageType);

			message = message.clone();

			message.put(SchedulerEngine.GROUP_NAME, trigger.getGroupName());
			message.put(SchedulerEngine.JOB_NAME, trigger.getJobName());

			schedule(
				scheduler, storageType, quartzTrigger, description, destination,
				message);
		}
		catch (RuntimeException runtimeException) {
			if (PortalRunMode.isTestMode()) {
				StackTraceElement[] stackTraceElements =
					runtimeException.getStackTrace();

				for (StackTraceElement stackTraceElement : stackTraceElements) {
					String className = stackTraceElement.getClassName();

					if (className.contains(ServerDetector.class.getName())) {
						if (_log.isInfoEnabled()) {
							_log.info(runtimeException);
						}

						return;
					}

					throw new SchedulerException(
						"Unable to schedule job", runtimeException);
				}
			}
			else {
				throw new SchedulerException(
					"Unable to schedule job", runtimeException);
			}
		}
		catch (Exception exception) {
			throw new SchedulerException("Unable to schedule job", exception);
		}
	}

	@Override
	public void shutdown() throws SchedulerException {
		try {
			if (!_persistedScheduler.isInStandbyMode()) {
				_persistedScheduler.standby();
			}

			if (!_memoryScheduler.isInStandbyMode()) {
				_memoryScheduler.standby();
			}
		}
		catch (Exception exception) {
			throw new SchedulerException(
				"Unable to shutdown scheduler", exception);
		}
	}

	@Override
	public void start() throws SchedulerException {
		try {
			_persistedScheduler.start();

			_memoryScheduler.start();
		}
		catch (Exception exception) {
			throw new SchedulerException(
				"Unable to start scheduler", exception);
		}
	}

	@Override
	public void validateTrigger(
			com.liferay.portal.kernel.scheduler.Trigger trigger,
			StorageType storageType)
		throws SchedulerException {

		Trigger quartzTrigger = (Trigger)trigger.getWrappedTrigger();

		if (quartzTrigger == null) {
			return;
		}

		Scheduler scheduler = _getScheduler(storageType);

		Calendar calendar = null;

		try {
			calendar = scheduler.getCalendar(quartzTrigger.getCalendarName());
		}
		catch (org.quartz.SchedulerException schedulerException) {
			throw new SchedulerException(
				"Unable to validate trigger \"" + quartzTrigger.getKey() + "\"",
				schedulerException);
		}

		List dates = TriggerUtils.computeFireTimes(
			(OperableTrigger)quartzTrigger, calendar, 1);

		if (!dates.isEmpty()) {
			return;
		}

		throw new SchedulerException(
			"Based on configured schedule, the given trigger \"" +
				quartzTrigger.getKey() + "\" will never fire.");
	}

	@Activate
	protected void activate() {
		_descriptionMaxLength = GetterUtil.getInteger(
			_props.get(PropsKeys.SCHEDULER_DESCRIPTION_MAX_LENGTH), 120);
		_groupNameMaxLength = GetterUtil.getInteger(
			_props.get(PropsKeys.SCHEDULER_GROUP_NAME_MAX_LENGTH), 80);
		_jobNameMaxLength = GetterUtil.getInteger(
			_props.get(PropsKeys.SCHEDULER_JOB_NAME_MAX_LENGTH), 80);

		_schedulerEngineEnabled = GetterUtil.getBoolean(
			_props.get(PropsKeys.SCHEDULER_ENABLED));

		if (!_schedulerEngineEnabled) {
			return;
		}

		try {
			_persistedScheduler = _initializeScheduler(
				"persisted.scheduler.", true);

			_memoryScheduler = _initializeScheduler("memory.scheduler.", false);
		}
		catch (Exception exception) {
			_log.error("Unable to initialize engine", exception);
		}
	}

	@Deactivate
	protected void deactivate() {
		if (!_schedulerEngineEnabled) {
			return;
		}

		try {
			if (!_persistedScheduler.isShutdown()) {
				_persistedScheduler.shutdown(false);
			}

			if (!_memoryScheduler.isShutdown()) {
				_memoryScheduler.shutdown(false);
			}
		}
		catch (Exception exception) {
			if (_log.isWarnEnabled()) {
				_log.warn("Unable to deactivate scheduler", exception);
			}
		}
	}

	protected Message getMessage(JobDataMap jobDataMap) {
		String messageJSON = (String)jobDataMap.get(SchedulerEngine.MESSAGE);

		return (Message)_jsonFactory.deserialize(messageJSON);
	}

	protected SchedulerResponse getScheduledJob(
			Scheduler scheduler, JobKey jobKey)
		throws Exception {

		JobDetail jobDetail = scheduler.getJobDetail(jobKey);

		if (jobDetail == null) {
			return null;
		}

		String jobName = jobKey.getName();
		String groupName = jobKey.getGroup();

		Trigger trigger = scheduler.getTrigger(
			new TriggerKey(jobName, groupName));

		if (trigger == null) {
			if (_log.isDebugEnabled()) {
				_log.debug(
					"Unable to find trigger for job (" + jobKey +
						"), will delete it");
			}

			scheduler.deleteJob(jobKey);

			return null;
		}

		SchedulerResponse schedulerResponse = new SchedulerResponse();

		JobDataMap jobDataMap = jobDetail.getJobDataMap();

		schedulerResponse.setDescription(
			jobDataMap.getString(SchedulerEngine.DESCRIPTION));
		schedulerResponse.setDestinationName(
			jobDataMap.getString(SchedulerEngine.DESTINATION_NAME));

		Message message = getMessage(jobDataMap);

		message.put(SchedulerEngine.JOB_STATE, _getJobState(jobDataMap));

		schedulerResponse.setMessage(message);

		schedulerResponse.setStorageType(
			StorageType.valueOf(
				jobDataMap.getString(SchedulerEngine.STORAGE_TYPE)));

		message.put(SchedulerEngine.END_TIME, trigger.getEndTime());
		message.put(
			SchedulerEngine.FINAL_FIRE_TIME, trigger.getFinalFireTime());
		message.put(SchedulerEngine.NEXT_FIRE_TIME, trigger.getNextFireTime());
		message.put(
			SchedulerEngine.PREVIOUS_FIRE_TIME, trigger.getPreviousFireTime());
		message.put(SchedulerEngine.START_TIME, trigger.getStartTime());

		schedulerResponse.setTrigger(new QuartzTrigger(trigger));

		return schedulerResponse;
	}

	protected List getScheduledJobs(
			Scheduler scheduler, String groupName, StorageType storageType)
		throws Exception {

		groupName = _fixMaxLength(groupName, _groupNameMaxLength, storageType);

		List schedulerResponses = new ArrayList<>();

		Set jobKeys = scheduler.getJobKeys(
			GroupMatcher.jobGroupEquals(groupName));

		for (JobKey jobKey : jobKeys) {
			SchedulerResponse schedulerResponse = getScheduledJob(
				scheduler, jobKey);

			if ((schedulerResponse != null) &&
				((storageType == null) ||
				 (storageType == schedulerResponse.getStorageType()))) {

				schedulerResponses.add(schedulerResponse);
			}
		}

		return schedulerResponses;
	}

	protected void schedule(
			Scheduler scheduler, StorageType storageType, Trigger trigger,
			String description, String destinationName, Message message)
		throws Exception {

		try {
			JobBuilder jobBuilder = JobBuilder.newJob(MessageSenderJob.class);

			jobBuilder.withIdentity(trigger.getJobKey());

			jobBuilder.storeDurably();

			JobDetail jobDetail = jobBuilder.build();

			JobDataMap jobDataMap = jobDetail.getJobDataMap();

			jobDataMap.put(SchedulerEngine.DESCRIPTION, description);
			jobDataMap.put(SchedulerEngine.DESTINATION_NAME, destinationName);
			jobDataMap.put(
				SchedulerEngine.MESSAGE, _jsonFactory.serialize(message));
			jobDataMap.put(
				SchedulerEngine.STORAGE_TYPE, storageType.toString());

			JobState jobState = new JobState(TriggerState.NORMAL);

			jobDataMap.put(
				SchedulerEngine.JOB_STATE,
				JobStateSerializeUtil.serialize(jobState));

			try {
				scheduler.scheduleJob(jobDetail, trigger);
			}
			catch (JobPersistenceException jobPersistenceException) {
				if (_log.isWarnEnabled()) {
					_log.warn(
						"Scheduler job " + trigger.getJobKey() +
							" already exists",
						jobPersistenceException);
				}
			}
		}
		catch (ObjectAlreadyExistsException objectAlreadyExistsException) {
			if (_log.isInfoEnabled()) {
				_log.info(
					"Message is already scheduled",
					objectAlreadyExistsException);
			}
		}
	}

	protected void update(
			Scheduler scheduler,
			com.liferay.portal.kernel.scheduler.Trigger trigger,
			StorageType storageType)
		throws Exception {

		Trigger quartzTrigger = (Trigger)trigger.getWrappedTrigger();

		if (quartzTrigger == null) {
			return;
		}

		TriggerKey triggerKey = quartzTrigger.getKey();

		if (scheduler.getTrigger(triggerKey) != null) {
			scheduler.rescheduleJob(triggerKey, quartzTrigger);
		}
		else {
			JobKey jobKey = quartzTrigger.getJobKey();

			JobDetail jobDetail = scheduler.getJobDetail(jobKey);

			if (jobDetail == null) {
				return;
			}

			synchronized (this) {
				scheduler.deleteJob(jobKey);
				scheduler.scheduleJob(jobDetail, quartzTrigger);
			}

			_updateJobState(scheduler, jobKey, TriggerState.NORMAL);
		}
	}

	private String _fixMaxLength(
		String argument, int maxLength, StorageType storageType) {

		if ((argument == null) || (storageType != StorageType.PERSISTED)) {
			return argument;
		}

		if (argument.length() > maxLength) {
			argument = argument.substring(0, maxLength);
		}

		return argument;
	}

	private JobState _getJobState(JobDataMap jobDataMap) {
		Map jobStateMap = (Map)jobDataMap.get(
			SchedulerEngine.JOB_STATE);

		return JobStateSerializeUtil.deserialize(jobStateMap);
	}

	private Scheduler _getScheduler(StorageType storageType) {
		if (storageType == StorageType.PERSISTED) {
			return _persistedScheduler;
		}

		return _memoryScheduler;
	}

	private Scheduler _initializeScheduler(
			String propertiesPrefix, boolean useQuartzCluster)
		throws Exception {

		StdSchedulerFactory schedulerFactory = new StdSchedulerFactory();

		Properties properties = _props.getProperties(propertiesPrefix, true);

		if (useQuartzCluster) {
			DBType dbType = DBManagerUtil.getDBType();

			if (dbType == DBType.SQLSERVER) {
				String lockHandlerClassName = properties.getProperty(
					"org.quartz.jobStore.lockHandler.class");

				if (Validator.isNull(lockHandlerClassName)) {
					properties.setProperty(
						"org.quartz.jobStore.lockHandler.class",
						UpdateLockRowSemaphore.class.getName());
				}
			}

			if (GetterUtil.getBoolean(
					_props.get(PropsKeys.CLUSTER_LINK_ENABLED))) {

				if (dbType == DBType.HYPERSONIC) {
					_log.error("Unable to cluster scheduler on Hypersonic");
				}
				else {
					properties.put(
						"org.quartz.jobStore.isClustered",
						Boolean.TRUE.toString());
				}
			}
		}

		schedulerFactory.initialize(properties);

		Scheduler scheduler = schedulerFactory.getScheduler();

		SchedulerContext schedulerContext = scheduler.getContext();

		schedulerContext.put("jSONFactory", _jsonFactory);
		schedulerContext.put("messageBus", _messageBus);

		ListenerManager listenerManager = scheduler.getListenerManager();

		listenerManager.addSchedulerListener(
			new SchedulerListenerImpl(scheduler));

		return scheduler;
	}

	private void _updateJobState(
			Scheduler scheduler, JobKey jobKey, TriggerState triggerState)
		throws Exception {

		JobDetail jobDetail = scheduler.getJobDetail(jobKey);

		if (jobDetail == null) {
			return;
		}

		JobDataMap jobDataMap = jobDetail.getJobDataMap();

		JobState jobState = _getJobState(jobDataMap);

		if (triggerState != null) {
			jobState.setTriggerState(triggerState);
		}

		jobDataMap.put(
			SchedulerEngine.JOB_STATE,
			JobStateSerializeUtil.serialize(jobState));

		scheduler.addJob(jobDetail, true);
	}

	private static final Log _log = LogFactoryUtil.getLog(
		QuartzSchedulerEngine.class);

	private static final Snapshot
		_schedulerEngineHelperSnapshot = new Snapshot<>(
			QuartzSchedulerEngine.class, SchedulerEngineHelper.class, null,
			true);

	private int _descriptionMaxLength;
	private int _groupNameMaxLength;
	private int _jobNameMaxLength;

	@Reference
	private JSONFactory _jsonFactory;

	private Scheduler _memoryScheduler;

	@Reference
	private MessageBus _messageBus;

	private Scheduler _persistedScheduler;

	@Reference
	private Props _props;

	@Reference(
		target = "(&(release.bundle.symbolic.name=com.liferay.portal.scheduler.quartz)(release.schema.version>=1.0.2))"
	)
	private Release _release;

	@Reference
	private SchedulerEngineAuditor _schedulerEngineAuditor;

	private volatile boolean _schedulerEngineEnabled;

	private class SchedulerListenerImpl extends SchedulerListenerSupport {

		public void jobPaused(JobKey jobKey) {
			_audit(jobKey, TriggerState.PAUSED);
		}

		public void jobResumed(JobKey jobKey) {
			_audit(jobKey, TriggerState.NORMAL);
		}

		public void jobScheduled(Trigger trigger) {
			_audit(trigger.getJobKey(), TriggerState.NORMAL);
		}

		public void triggerFinalized(Trigger trigger) {
			JobKey jobKey = trigger.getJobKey();

			_audit(jobKey, TriggerState.COMPLETE);

			try {
				JobDetail jobDetail = _scheduler.getJobDetail(jobKey);

				JobDataMap jobDataMap = jobDetail.getJobDataMap();

				SchedulerEngineHelper schedulerEngineHelper =
					_schedulerEngineHelperSnapshot.get();

				schedulerEngineHelper.delete(
					jobKey.getName(), jobKey.getGroup(),
					StorageType.valueOf(
						jobDataMap.getString(SchedulerEngine.STORAGE_TYPE)));
			}
			catch (Exception exception) {
				_log.error("Unable to delete job " + jobKey, exception);
			}
		}

		private SchedulerListenerImpl(Scheduler scheduler) {
			_scheduler = scheduler;
		}

		private void _audit(JobKey jobKey, TriggerState triggerState) {
			try {
				JobDetail jobDetail = _scheduler.getJobDetail(jobKey);

				JobDataMap jobDataMap = jobDetail.getJobDataMap();

				Message message = new Message();

				message.setValues(new HashMap<>(jobDataMap.getWrappedMap()));

				_schedulerEngineAuditor.auditSchedulerJobs(
					message, triggerState);
			}
			catch (Exception exception) {
				_log.error(
					"Unable to send audit message for scheduler job " + jobKey,
					exception);
			}
		}

		private final Scheduler _scheduler;

	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy