Skip to content

Instantly share code, notes, and snippets.

@jlucansky
Last active July 27, 2020 20:05
Show Gist options
  • Save jlucansky/1c5b8e4befa3b745b4485184811caa2f to your computer and use it in GitHub Desktop.
Save jlucansky/1c5b8e4befa3b745b4485184811caa2f to your computer and use it in GitHub Desktop.
using Quartz;
using Quartz;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Description;
namespace Quartz.Web.Controllers
{
[ExceptionFilter]
[RoutePrefix("scheduler")]
public class SchedulerController : ApiController
{
IScheduler Scheduler => Request.GetOwinContext().Get<IScheduler>("quartz.scheduler");
/// <summary>
/// Returns the name of the scheduler.
/// </summary>
[Route("name"), HttpGet]
public string SchedulerName()
{
return Scheduler.SchedulerName;
}
/// <summary>
/// Returns the instance Id of the scheduler.
/// </summary>
[Route("instance"), HttpGet]
public string SchedulerInstanceId()
{
return Scheduler.SchedulerInstanceId;
}
/// <summary>
/// Reports whether the scheduler is in stand-by mode.
/// </summary>
[Route("in-standby-mode"), HttpGet]
public bool InStandbyMode()
{
return Scheduler.InStandbyMode;
}
/// <summary>
/// Reports whether the scheduler has been Shutdown.
/// </summary>
[Route("is-shutdown"), HttpGet]
public bool IsShutdown()
{
return Scheduler.IsShutdown;
}
/// <summary>
/// Whether the scheduler has been started.
/// </summary>
[Route("is-started"), HttpGet]
public bool IsStarted()
{
return Scheduler.IsStarted;
}
/// <summary>
/// Add (register) the given calendar to the Scheduler.
/// </summary>
/// <param name="calName">Name of the calendar.</param>
/// <param name="replace">if set to <c>true</c> [replace].</param>
/// <param name="updateTriggers">whether or not to update existing triggers that
/// referenced the already existing calendar so that they are 'correct'
/// based on the new trigger.</param>
/// <param name="cancellationToken">The cancellation instruction.</param>
/// <remarks>
/// Očakáva sa Content-Type: multipart/form-data; s položkami 'calendar.type' a 'calendar.data'
/// </remarks>
[Route("calendar"), HttpPut]
public async Task AddCalendar(string calName, bool replace, bool updateTriggers, CancellationToken cancellationToken = default(CancellationToken))
{
var calendar = await GetMultipartData<ICalendar>("calendar", cancellationToken);
await Scheduler.AddCalendar(calName, calendar, replace, updateTriggers, cancellationToken);
}
/// <summary>
/// Add the given job to the Scheduler - with no associated trigger.
/// </summary>
/// <remarks>
/// Očakáva sa Content-Type: multipart/form-data; s položkami 'jobDetail.type' a 'jobDetail.data'.
/// The job must by definition be 'durable', if it is not,
/// SchedulerException will be thrown.
/// </remarks>
/// <response code="204">No Content</response>
/// <response code="500">Jobs added with no trigger must be durable.</response>
[Route("job"), HttpPut]
public async Task AddJob(bool replace, CancellationToken cancellationToken = default(CancellationToken))
{
var jobDetail = await GetMultipartData<IJobDetail>("jobDetail", cancellationToken);
await Scheduler.AddJob(jobDetail, replace, cancellationToken);
}
/// <summary>
/// Add the given job to the Scheduler - with no associated trigger.
/// </summary>
/// <remarks>
/// Očakáva sa Content-Type: multipart/form-data; s položkami 'jobDetail.type' a 'jobDetail.data'.
/// A non-durable job can be stored. Once it is
/// scheduled, it will resume normal non-durable behavior (i.e. be deleted
/// once there are no remaining associated triggers).
/// </remarks>
[Route("job-nondurable"), HttpPut]
public async Task AddJobNonDurable(bool replace, CancellationToken cancellationToken = default(CancellationToken))
{
var jobDetail = await GetMultipartData<IJobDetail>("jobDetail", cancellationToken);
await Scheduler.AddJob(jobDetail, replace, true, cancellationToken);
}
/// <summary>
/// Determine whether a job with the given identifier already
/// exists within the scheduler.
/// </summary>
/// <remarks>
/// Returns true if a Job exists with the given identifier
/// </remarks>
/// <param name="jobKey">the identifier to check for</param>
/// <param name="cancellationToken"></param>
[Route("job-exists"), HttpPost]
public Task<bool> CheckExists([FromBody] JobKey jobKey, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.CheckExists(jobKey, cancellationToken);
}
/// <summary>
/// Determine whether a trigger with the given identifier already
/// exists within the scheduler.
/// </summary>
/// <remarks>
/// Returns true if a Trigger exists with the given identifier
/// </remarks>
/// <param name="triggerKey">the identifier to check for</param>
/// <param name="cancellationToken"></param>
[Route("trigger-exists"), HttpPost]
public Task<bool> CheckExists([FromBody] TriggerKey triggerKey, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.CheckExists(triggerKey, cancellationToken);
}
/// <summary>
/// Clears (deletes!) all scheduling data - all jobs, triggers, calendars.
/// </summary>
[Route("clear"), HttpGet]
public Task Clear(CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.Clear(cancellationToken);
}
/// <summary>
/// Delete the identified calendar from the Scheduler.
/// </summary>
/// <param name="calName">Name of the calendar.</param>
/// <param name="cancellationToken"></param>
/// <remarks>
/// Returns true if the Calendar was found and deleted.
/// </remarks>
[Route("calendar"), HttpDelete]
public Task<bool> DeleteCalendar(string calName, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.DeleteCalendar(calName, cancellationToken);
}
/// <summary>
/// Delete the identified job from the Scheduler - and any
/// associated triggers.
/// </summary>
/// <remarks>
/// Returns true if the Job was found and deleted.
/// </remarks>
[Route("job"), HttpDelete]
public Task<bool> DeleteJob(JobKey jobKey, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.DeleteJob(jobKey, cancellationToken);
}
/// <summary>
/// Delete the identified jobs from the Scheduler - and any
/// associated triggers.
/// </summary>
/// <remarks>
/// <para>Note that while this bulk operation is likely more efficient than
/// invoking delete job several
/// times, it may have the adverse affect of holding data locks for a
/// single long duration of time (rather than lots of small durations
/// of time).</para>
/// Returns true if all of the Jobs were found and deleted, false if
/// one or more were not deleted.
/// </remarks>
[Route("jobs"), HttpDelete]
public Task<bool> DeleteJobs([FromBody] IReadOnlyCollection<JobKey> jobKeys, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.DeleteJobs(jobKeys, cancellationToken);
}
/// <summary>
/// Get the calendar instance with the given name.
/// </summary>
/// <remarks>
/// Vráti sa Content-Type: multipart/form-data; s položkami 'type' a 'data'
/// </remarks>
[Route("calendar"), HttpGet]
[ResponseType(typeof(ICalendar))]
public async Task<HttpResponseMessage> GetCalendar(string calName, CancellationToken cancellationToken = default(CancellationToken))
{
return CreateMultipartResponse(await Scheduler.GetCalendar(calName, cancellationToken));
}
/// <summary>
/// Get the names of all registered calendar.
/// </summary>
[Route("calendar-names"), HttpGet]
public Task<IReadOnlyCollection<string>> GetCalendarNames(CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.GetCalendarNames(cancellationToken);
}
[Route("executing-jobs"), HttpGet]
public async Task<HttpResponseMessage> GetCurrentlyExecutingJobs(CancellationToken cancellationToken = default(CancellationToken))
{
var jobs = await Scheduler.GetCurrentlyExecutingJobs(cancellationToken);
return CreateMultipartResponse(jobs.Select(x => RemoteJobExecutionContext.CopyFrom(x)).ToArray());
}
/// <summary>
/// Get the JobDetail for the job instance with the given key .
/// </summary>
/// <remarks>
/// The returned JobDetail object will be a snap-shot of the actual stored
/// JobDetail. If you wish to modify the JobDetail, you must re-store the JobDetail afterward.
/// Vráti sa Content-Type: multipart/form-data; s položkami 'type' a 'data'
/// </remarks>
[Route("job-detail"), HttpPost]
[ResponseType(typeof(IJobDetail))]
public async Task<HttpResponseMessage> GetJobDetail([FromBody] JobKey jobKey, CancellationToken cancellationToken = default(CancellationToken))
{
return CreateMultipartResponse(await Scheduler.GetJobDetail(jobKey, cancellationToken));
}
/// <summary>
/// Get the names of all known job groups.
/// </summary>
[Route("job-groups"), HttpGet]
public Task<IReadOnlyCollection<string>> GetJobGroupNames(CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.GetJobGroupNames(cancellationToken);
}
/// <summary>
/// Get the keys of all the jobs in the matching groups.
/// </summary>
[Route("job-keys"), HttpPost]
public Task<IReadOnlyCollection<JobKey>> GetJobKeys([FromBody] SerializableGroupMatcher<JobKey> matcher, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.GetJobKeys(matcher, cancellationToken);
}
/// <summary>
/// Get a SchedulerMetaData object describing the settings
/// and capabilities of the scheduler instance.
/// </summary>
/// <remarks>
/// Note that the data returned is an 'instantaneous' snap-shot, and that as
/// soon as it's returned, the meta data values may be different.
/// </remarks>
[Route("metadata"), HttpGet]
public Task<SchedulerMetaData> GetMetaData(CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.GetMetaData(cancellationToken);
}
/// <summary>
/// Get the names of all trigger groups that are paused.
/// </summary>
[Route("paused-trigger-groups"), HttpGet]
public Task<IReadOnlyCollection<string>> GetPausedTriggerGroups(CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.GetPausedTriggerGroups(cancellationToken);
}
/// <summary>
/// Get the trigger instance with the given key.
/// </summary>
/// <remarks>
/// The returned Trigger object will be a snap-shot of the actual stored
/// trigger. If you wish to modify the trigger, you must re-store the
/// trigger afterward (e.g. see 'reschedule-job').
/// Vráti sa Content-Type: multipart/form-data; s položkami 'type' a 'data'
/// </remarks>
[Route("trigger"), HttpPost]
[ResponseType(typeof(global::Quartz.ITrigger))]
public async Task<HttpResponseMessage> GetTrigger([FromBody] TriggerKey triggerKey, CancellationToken cancellationToken = default(CancellationToken))
{
return CreateMultipartResponse(await Scheduler.GetTrigger(triggerKey, cancellationToken));
}
/// <summary>
/// Get the names of all known trigger groups.
/// </summary>
[Route("trigger-groups"), HttpGet]
public Task<IReadOnlyCollection<string>> GetTriggerGroupNames(CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.GetTriggerGroupNames(cancellationToken);
}
/// <summary>
/// Get the names of all the triggers in the given groups.
/// </summary>
[Route("trigger-keys"), HttpPost]
public Task<IReadOnlyCollection<TriggerKey>> GetTriggerKeys([FromBody] SerializableGroupMatcher<TriggerKey> matcher, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.GetTriggerKeys(matcher, cancellationToken);
}
/// <summary>
/// Get all triggers that are associated with the identified job.
/// </summary>
/// <remarks>
/// The returned Trigger objects will be snap-shots of the actual stored
/// triggers. If you wish to modify a trigger, you must re-store the
/// trigger afterward (e.g. see 'reschedule-job').
/// </remarks>
[Route("triggers-of-job"), HttpPost]
[ResponseType(typeof(IReadOnlyCollection<global::Quartz.ITrigger>))]
public async Task<HttpResponseMessage> GetTriggersOfJob([FromBody] JobKey jobKey, CancellationToken cancellationToken = default(CancellationToken))
{
return CreateMultipartResponse(await Scheduler.GetTriggersOfJob(jobKey, cancellationToken));
}
/// <summary>
/// Get the current state of the identified trigger.
/// </summary>
[Route("trigger-state"), HttpPost]
public Task<TriggerState> GetTriggerState([FromBody] TriggerKey triggerKey, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.GetTriggerState(triggerKey, cancellationToken);
}
/// <summary>
/// Request the cancellation, within this Scheduler instance, of all
/// currently executing instances of the identified job.
/// </summary>
/// <remarks>
/// <para>
/// If more than one instance of the identified job is currently executing,
/// the cancellation token will be set on each instance. However, there is a limitation that in the case that
/// 'interrupt' on one instances throws an exception, all
/// remaining instances (that have not yet been interrupted) will not have
/// their Interrupt method called.
/// </para>
/// <para>
/// This method is not cluster aware. That is, it will only interrupt
/// instances of the identified InterruptableJob currently executing in this
/// Scheduler instance, not across the entire cluster.
/// </para>
/// Returns true if at least one instance of the identified job was found and interrupted.
/// </remarks>
[Route("interrupt"), HttpPost]
public Task<bool> Interrupt([FromBody] JobKey jobKey, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.Interrupt(jobKey, cancellationToken);
}
/// <summary>
/// Request the cancellation, within this Scheduler instance, of the
/// identified executing job instance.
/// </summary>
/// <remarks>
/// This method is not cluster aware. That is, it will only interrupt
/// instances of the identified InterruptableJob currently executing in this
/// Scheduler instance, not across the entire cluster.
/// Returns true if the identified job instance was found and interrupted.
/// </remarks>
/// <param name="fireInstanceId">
/// The unique identifier of the job instance to be interrupted
/// </param>
[Route("interrupt"), HttpGet]
public Task<bool> Interrupt(string fireInstanceId, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.Interrupt(fireInstanceId, cancellationToken);
}
/// <summary>
/// Returns true if the given JobGroup is paused
/// </summary>
[Route("is-job-group-paused"), HttpGet]
public Task<bool> IsJobGroupPaused(string groupName, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.IsJobGroupPaused(groupName, cancellationToken);
}
/// <summary>
/// Returns true if the given JobGroup is paused
/// </summary>
[Route("is-trigger-group-paused"), HttpGet]
public Task<bool> IsTriggerGroupPaused(string groupName, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.IsTriggerGroupPaused(groupName, cancellationToken);
}
/// <summary>
/// Pause all triggers
/// </summary>
/// <remarks>
/// Similar to calling 'pause-trigger' on every group, however, after using this method 'resume-all'
/// must be called to clear the scheduler's state of 'remembering' that all
/// new triggers will be paused as they are added.
/// When 'resume-all' is called (to un-pause), trigger misfire
/// instructions WILL be applied.
/// </remarks>
[Route("pause-all"), HttpGet]
public Task PauseAll(CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.PauseAll(cancellationToken);
}
/// <summary>
/// Pause the job with the given key - by pausing all of its current triggers.
/// </summary>
[Route("pause-job"), HttpPost]
public Task PauseJob([FromBody] JobKey jobKey, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.PauseJob(jobKey, cancellationToken);
}
/// <summary>
/// Pause all of the jobs in the
/// matching groups - by pausing all of their triggers.
/// </summary>
/// <remarks>
/// <para>
/// The Scheduler will "remember" that the groups are paused, and impose the
/// pause on any new jobs that are added to any of those groups until it is resumed.
/// </para>
/// <para>NOTE: There is a limitation that only exactly matched groups
/// can be remembered as paused. For example, if there are pre-existing
/// job in groups "aaa" and "bbb" and a matcher is given to pause
/// groups that start with "a" then the group "aaa" will be remembered
/// as paused and any subsequently added jobs in group "aaa" will be paused,
/// however if a job is added to group "axx" it will not be paused,
/// as "axx" wasn't known at the time the "group starts with a" matcher
/// was applied. HOWEVER, if there are pre-existing groups "aaa" and
/// "bbb" and a matcher is given to pause the group "axx" (with a
/// group equals matcher) then no jobs will be paused, but it will be
/// remembered that group "axx" is paused and later when a job is added
/// in that group, it will become paused.</para>
/// </remarks>
[Route("pause-jobs"), HttpPost]
public Task PauseJobs([FromBody] SerializableGroupMatcher<JobKey> matcher, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.PauseJobs(matcher, cancellationToken);
}
/// <summary>
/// Pause the trigger with the given key.
/// </summary>
[Route("pause-trigger"), HttpPost]
public Task PauseTrigger([FromBody] TriggerKey triggerKey, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.PauseTrigger(triggerKey, cancellationToken);
}
/// <summary>
/// Pause all of the triggers in the groups matching.
/// </summary>
/// <remarks>
/// <para>
/// The Scheduler will "remember" all the groups paused, and impose the
/// pause on any new triggers that are added to any of those groups until it is resumed.
/// </para>
/// <para>NOTE: There is a limitation that only exactly matched groups
/// can be remembered as paused. For example, if there are pre-existing
/// triggers in groups "aaa" and "bbb" and a matcher is given to pause
/// groups that start with "a" then the group "aaa" will be remembered as
/// paused and any subsequently added triggers in that group be paused,
/// however if a trigger is added to group "axx" it will not be paused,
/// as "axx" wasn't known at the time the "group starts with a" matcher
/// was applied. HOWEVER, if there are pre-existing groups "aaa" and
/// "bbb" and a matcher is given to pause the group "axx" (with a
/// group equals matcher) then no triggers will be paused, but it will be
/// remembered that group "axx" is paused and later when a trigger is added
/// in that group, it will become paused.</para>
/// </remarks>
[Route("pause-triggers"), HttpPost]
public Task PauseTriggers([FromBody] SerializableGroupMatcher<TriggerKey> matcher, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.PauseTriggers(matcher, cancellationToken);
}
/// <summary>
/// Remove the trigger with the given key, and store the new given one
/// </summary>
/// <remarks>
/// Remove (delete) the trigger with the
/// given key, and store the new given one - which must be associated
/// with the same job (the new trigger must have the job name &amp; group specified) - however,
/// the new trigger need not have the same name as the old trigger.
/// Returns <c>null</c> if a trigger with the given
/// name and group was not found and removed from the store (and the
/// new trigger is therefore not stored), otherwise
/// the first fire time of the newly scheduled trigger.
/// Očakáva sa Content-Type: multipart/form-data; s položkami 'triggerKey.type', 'triggerKey.data', 'newTrigger.type' a 'newTrigger.data'.
/// </remarks>
[Route("reschedule-job"), HttpPost]
public async Task<DateTimeOffset?> RescheduleJob(CancellationToken cancellationToken = default(CancellationToken))
{
var data = await DeserializeMultipart(cancellationToken);
var triggerKey = GetMultipartData<TriggerKey>(data, "triggerKey");
var newTrigger = GetMultipartData<global::Quartz.ITrigger>(data, "newTrigger");
return await Scheduler.RescheduleJob(triggerKey, newTrigger, cancellationToken);
}
/// <summary>
/// Resume (un-pause) all triggers - similar to calling
/// 'resume-triggers' on every group.
/// </summary>
/// <remarks>
/// If any trigger missed one or more fire-times, then the
/// triggers's misfire instruction will be applied.
/// </remarks>
[Route("resume-all"), HttpGet]
public Task ResumeAll(CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.ResumeAll(cancellationToken);
}
/// <summary>
/// Resume (un-pause) the job with
/// the given key.
/// </summary>
/// <remarks>
/// If any of the job'strigger s missed one
/// or more fire-times, then the trigger's misfire
/// instruction will be applied.
/// </remarks>
[Route("resume-job"), HttpPost]
public Task ResumeJob([FromBody] JobKey jobKey, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.ResumeJob(jobKey, cancellationToken);
}
/// <summary>
/// Resume (un-pause) all of the jobs
/// in matching groups.
/// </summary>
/// <remarks>
/// If any of the job s had trigger s that
/// missed one or more fire-times, then the trigger's
/// misfire instruction will be applied.
/// </remarks>
[Route("resume-jobs"), HttpPost]
public Task ResumeJobs([FromBody] SerializableGroupMatcher<JobKey> matcher, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.ResumeJobs(matcher, cancellationToken);
}
/// <summary>
/// Resume (un-pause) the trigger with the given
/// key.
/// </summary>
/// <remarks>
/// If the trigger missed one or more fire-times, then the
/// trigger's misfire instruction will be applied.
/// </remarks>
[Route("resume-trigger"), HttpPost]
public Task ResumeTrigger([FromBody] TriggerKey triggerKey, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.ResumeTrigger(triggerKey, cancellationToken);
}
/// <summary>
/// Resume (un-pause) all of the triggers in matching groups.
/// </summary>
/// <remarks>
/// If any trigger missed one or more fire-times, then the
/// trigger's misfire instruction will be applied.
/// </remarks>
[Route("resume-triggers"), HttpPost]
public Task ResumeTriggers([FromBody] SerializableGroupMatcher<TriggerKey> matcher, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.ResumeTriggers(matcher, cancellationToken);
}
/// <summary>
/// Add the given job to the
/// Scheduler, and associate the given trigger with it.
/// </summary>
/// <remarks>
/// If the given Trigger does not reference any job, then it
/// will be set to reference the Job passed with it into this method.
/// Očakáva sa Content-Type: multipart/form-data; s položkami 'trigger.type', 'trigger.data' a nepovinné: 'jobDetail.type' a 'jobDetail.data'.
/// </remarks>
[Route("schedule-job"), HttpPost]
public async Task<DateTimeOffset> ScheduleJob(CancellationToken cancellationToken = default(CancellationToken))
{
var data = await DeserializeMultipart(cancellationToken);
var trigger = GetMultipartData<global::Quartz.ITrigger>(data, "trigger");
if (data.ContainsKey("jobDetail"))
{
var jobDetail = GetMultipartData<IJobDetail>(data, "jobDetail");
return await Scheduler.ScheduleJob(jobDetail, trigger, cancellationToken);
}
else
{
return await Scheduler.ScheduleJob(trigger, cancellationToken);
}
}
/// <summary>
/// Schedule all of the given jobs with the related set of triggers.
/// </summary>
/// <remarks>
/// If any of the given jobs or triggers already exist (or more
/// specifically, if the keys are not unique) and the replace
/// parameter is not set to true then an exception will be thrown.
/// </remarks>
[Route("schedule-jobs"), HttpPost]
public async Task ScheduleJobs(bool replace, CancellationToken cancellationToken = default(CancellationToken))
{
var triggersAndJobs = await GetMultipartData<object[]>("triggersAndJobs", cancellationToken);
var dict = new Dictionary<IJobDetail, IReadOnlyCollection<global::Quartz.ITrigger>>();
for (int i = 0; i < triggersAndJobs.Length; i++)
{
// polozky su na striedacku: job, triggers, job, triggers...
var key = triggersAndJobs[i];
var value = triggersAndJobs[++i];
dict[(IJobDetail)key] = (IReadOnlyCollection<global::Quartz.ITrigger>)value;
}
await Scheduler.ScheduleJobs(dict, replace, cancellationToken);
}
/// <summary>
/// Halts the scheduler's firing of triggers.
/// </summary>
/// <remarks>
/// Halts the scheduler's firing of triggers,
/// and cleans up all resources associated with the Scheduler. Equivalent to Shutdown(false).
/// The scheduler cannot be re-started.
/// </remarks>
[Route("shutdown-immediately"), HttpGet]
public Task Shutdown(CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.Shutdown(cancellationToken);
}
/// <summary>
/// Halts the scheduler's firing of triggers.
/// </summary>
/// <remarks>
/// Halts the scheduler's firing of triggers,
/// and cleans up all resources associated with the Scheduler.
/// The scheduler cannot be re-started.
/// </remarks>
/// <param name="waitForJobsToComplete">
/// if <c>true</c>, the scheduler will not allow this method
/// to return until all currently executing jobs have completed.
/// </param>
[Route("shutdown"), HttpGet]
public Task Shutdown(bool waitForJobsToComplete, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.Shutdown(waitForJobsToComplete, cancellationToken);
}
/// <summary>
/// Temporarily halts the scheduler's firing of triggers.
/// </summary>
/// <remarks>
/// <para>
/// When 'start' is called (to bring the scheduler out of
/// stand-by mode), trigger misfire instructions will NOT be applied
/// during the execution of the 'start' method - any misfires
/// will be detected immediately afterward
/// </para>
/// <para>
/// The scheduler is not destroyed, and can be re-started at any time.
/// </para>
/// </remarks>
[Route("standby"), HttpGet]
public Task Standby(CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.Standby(cancellationToken);
}
/// <summary>
/// Starts the scheduler's threads that fire triggers.
/// </summary>
/// <remarks>
/// When a scheduler is first created it is in "stand-by" mode, and will not
/// fire triggers. The scheduler can also be put into stand-by mode by
/// calling the 'standby' method.
/// The misfire/recovery process will be started, if it is the initial call
/// to this method on this scheduler instance.
/// </remarks>
[Route("start"), HttpGet]
public Task Start(CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.Start(cancellationToken);
}
/// <summary>
/// Calls 'start' after the indicated delay.
/// </summary>
/// <remarks>
/// This call does not block. This can be useful within applications that
/// have initializers that create the scheduler immediately, before the
/// resources needed by the executing jobs have been fully initialized.
/// </remarks>
/// <param name="delay">TimeSpan Ticks</param>
[Route("start-delayed"), HttpGet]
public Task StartDelayed([FromBody] long delay, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.StartDelayed(TimeSpan.FromTicks(delay), cancellationToken);
}
/// <summary>
/// Trigger the identified job (Execute it now).
/// </summary>
/// <remarks>
/// Očakáva sa Content-Type: multipart/form-data; s položkami 'jobKey.type', 'jobKey.data' a nepovinné: 'jobDataMap.type' a 'jobDataMap.data'.
/// </remarks>
[Route("trigger-job"), HttpPost]
public async Task TriggerJob(CancellationToken cancellationToken = default(CancellationToken))
{
var multipart = await DeserializeMultipart(cancellationToken);
JobKey jobKey = null;
JobDataMap jobDataMap = null;
jobKey = GetMultipartData<JobKey>(multipart, nameof(jobKey));
if (multipart.ContainsKey(nameof(jobDataMap)))
jobDataMap = GetMultipartData<JobDataMap>(multipart, nameof(jobDataMap));
await Scheduler.TriggerJob(jobKey, jobDataMap, cancellationToken);
}
/// <summary>
/// Remove the indicated trigger from the scheduler.
/// </summary>
/// <remarks>
/// If the related job does not have any other triggers, and the job is
/// not durable, then the job will also be deleted.
/// </remarks>
[Route("unschedule-job"), HttpPost]
public Task<bool> UnscheduleJob([FromBody] TriggerKey triggerKey, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.UnscheduleJob(triggerKey);
}
/// <summary>
/// Remove all of the indicated triggers from the scheduler.
/// </summary>
/// <remarks>
/// <para>
/// If the related job does not have any other triggers, and the job is
/// not durable, then the job will also be deleted.
/// </para>
/// Note that while this bulk operation is likely more efficient than
/// invoking 'unschedule-job' several
/// times, it may have the adverse affect of holding data locks for a
/// single long duration of time (rather than lots of small durations
/// of time).
/// </remarks>
[Route("unschedule-jobs"), HttpPost]
public Task<bool> UnscheduleJobs([FromBody] IReadOnlyCollection<TriggerKey> triggerKeys, CancellationToken cancellationToken = default(CancellationToken))
{
return Scheduler.UnscheduleJobs(triggerKeys, cancellationToken);
}
#region Multipart Serialization
private async Task<T> GetMultipartData<T>(string key, CancellationToken cancellationToken)
{
return GetMultipartData<T>(await DeserializeMultipart(cancellationToken), key);
}
private T GetMultipartData<T>(Dictionary<string, Tuple<Type, object>> multipart, string key)
{
if (!multipart.ContainsKey(key))
throw new InvalidOperationException($"Missing content disposition named '{key}.type' and '{key}.data'");
Tuple<Type, object> data = multipart[key];
if (!typeof(T).IsAssignableFrom(data.Item1))
throw new InvalidOperationException($"{typeof(T)} is not assignable from {data.Item1}");
return (T)data.Item2;
}
private async Task<Dictionary<string, Tuple<Type, object>>> DeserializeMultipart(CancellationToken cancellationToken)
{
/*
Expected form-data:
calendar.type = "Quartz.Impl.Calendar.DailyCalendar, Quartz"
calendar.data = "{"$type": ...}"
*/
Dictionary<string, string> typeDict = new Dictionary<string, string>();
Dictionary<string, byte[]> dataDict = new Dictionary<string, byte[]>();
var multipart = (await Request.Content.ReadAsMultipartAsync(cancellationToken));
foreach (var item in multipart.Contents)
{
string contentName = item.Headers.ContentDisposition.Name.Trim('"', '\'');
var contentNameParts = contentName.Split('.');
if (contentNameParts.Length != 2)
throw new InvalidOperationException("Invalid content disposition name.");
string key = contentNameParts.First();
string field = contentNameParts.Last();
if (field.Equals("type", StringComparison.OrdinalIgnoreCase))
typeDict.Add(key, await item.ReadAsStringAsync());
if (field.Equals("data", StringComparison.OrdinalIgnoreCase))
dataDict.Add(key, await item.ReadAsByteArrayAsync());
}
Dictionary<string, Tuple<Type, object>> result = new Dictionary<string, Tuple<Type, object>>(StringComparer.OrdinalIgnoreCase);
foreach (var key in typeDict.Keys)
{
Type type = Type.GetType(typeDict[key], true);
if (!dataDict.ContainsKey(key))
throw new InvalidOperationException($"Missing content disposition named '{key}.data'");
result.Add(key, Tuple.Create(type, SerializationHelper.Deserialize(type, dataDict[key])));
}
return result;
}
HttpResponseMessage CreateMultipartResponse<T>(T data) where T : class
{
if (data == null)
return new HttpResponseMessage(HttpStatusCode.NoContent);
string typeName = SerializationHelper.RemoveAssemblyDetails(data.GetType());
MultipartFormDataContent multiPartContent = new MultipartFormDataContent();
multiPartContent.Add(new ByteArrayContent(Encoding.Default.GetBytes(typeName)), "type");
multiPartContent.Add(new ByteArrayContent(SerializationHelper.Serialize(data)), "data");
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = multiPartContent
};
}
#endregion
}
}
public class SerializableGroupMatcher<TKey>
where TKey : Key<TKey>
{
public string CompareToValue { get; set; }
public string CompareWithOperator { get; set; }
static readonly Dictionary<StringOperator, string> stringOperatorMap = new Dictionary<StringOperator, string>() {
{ StringOperator.Equality, nameof(StringOperator.Equality) },
{ StringOperator.StartsWith, nameof(StringOperator.StartsWith) },
{ StringOperator.EndsWith, nameof(StringOperator.EndsWith) },
{ StringOperator.Contains, nameof(StringOperator.Contains) },
{ StringOperator.Anything, nameof(StringOperator.Anything) },
};
public static implicit operator SerializableGroupMatcher<TKey>(GroupMatcher<TKey> matcher)
{
return new SerializableGroupMatcher<TKey>()
{
CompareToValue = matcher.CompareToValue,
CompareWithOperator = stringOperatorMap[matcher.CompareWithOperator]
};
}
public static implicit operator GroupMatcher<TKey>(SerializableGroupMatcher<TKey> matcher)
{
switch (matcher.CompareWithOperator)
{
case nameof(StringOperator.Anything):
return GroupMatcher<TKey>.AnyGroup();
case nameof(StringOperator.Contains):
return GroupMatcher<TKey>.GroupContains(matcher.CompareToValue);
case nameof(StringOperator.EndsWith):
return GroupMatcher<TKey>.GroupEndsWith(matcher.CompareToValue);
case nameof(StringOperator.Equality):
return GroupMatcher<TKey>.GroupEquals(matcher.CompareToValue);
case nameof(StringOperator.StartsWith):
return GroupMatcher<TKey>.GroupStartsWith(matcher.CompareToValue);
default:
throw new ArgumentException("Invalid Operator: " + matcher.CompareWithOperator);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment