Can Hangfire Handle Changes to Scheduled Tasks Without Redeployment
Asked Answered
A

3

6

I have been playing around with Hangfire in a Microsoft MVC application. I have gotten it to compile and schedule fire-and-forget tasks, but I am surprised that I cannot add/remove jobs while the program is running. Is it true that Hangfire cannot dynamically schedule tasks during runtime? Is there a well-known framework that allows one to schedule tasks even after the application has been compiled or deployed without having to change the C# code every time I want to add tasks?

I have also researched Quartz.NET, and it seems to have the same issue.

EDIT:

Windows Task Scheduler can allow tasks to be scheduled with a GUI, and UNIX's cron can have tasks added or removed by editing a file, but I'm looking for some sort of application running on Windows that would allow the user to add or remove tasks after the application has been deployed. I do not want to re-compile the application every time I want to add or remove tasks.

Absher answered 23/5, 2016 at 14:9 Comment(8)
I'm familiar with Quartz.NET, so your last statement raised a flag. I took a quick look at the hangfire docs, and I'm not sure your understanding is correct...Amagasaki
From what I understand, Hangfire is able to schedule tasks, no problem. But only tasks that you have hard-coded in C#. While Hangfire is running, I have not found a way to add tasks dynamically after compile time. Nowhere in the docs have I found that Hangfire can schedule tasks dynamically during run-time.Absher
Your question is ambiguous and confusing. "...schedule tasks dynamically during run-time." This naturally means "adding and removing new single-execution/delayed/scheduled tasks at run-time," which both Quartz and Hangfire will do, are designed to do. You certainly can "add/remove jobs while the program is running." You seem to be asking for something else, a tool that can index and allow you to execute dynamically defined code, or code from a library loaded at runtime. That is an entirely different problem, not one that job-schedulers are designed to address OOTB.Amagasaki
Is there an example you could perhaps point me to that shows this actually being done? I have not found this to be true.Absher
Again, to which "this" are you referring? Scheduleing jobs, or dynamic code compilation/load/execution?Amagasaki
Dynamic code compilation/load/execution.Absher
That's outside my expertise. You should ask a new, refocused question.Amagasaki
@MarcL. This seems a reasonable question to me. At first glance, the Hangfire documentation doesn't seem to mention anything about dynamically adding or removing tasks. Code examples are provided, such as BackgroundJob.Enqueue(() => Console.WriteLine("Fire-and-forget"));, but that looks like code to me. How can one add a new task, change an existing one, or remove one without changing C# source code? It seems that adding a task should be possible through a menu, a JSON file, or similar.Iodism
A
4

As asked, the question seems to rest on a misunderstanding of the meaning of "dynamic...during runtime". The answer is "yes," it can change tasks without redeployment (but that doesn't appear to be what you're really looking for).

Hangfire will add a dashboard UI to your application if you configure it to do so, but it is not an end-to-end task management application itself. It is designed to give your application the ability to schedule work, and have that work completed in a very disconnected way from the point of invocation--it may not even be completed on the same machine.

It is limited to invoking .NET code, but by definition this fulfills your stated requirement to "dynamically schedule tasks during runtime." This can be done in response to any event within your application that you like. Tasks can also be removed, updated and canceled.

(Post-edit) You're correct: any scheduling UI or deserialization of task-file format you'll have to write yourself. If you are looking for a tool that gives you a UI and/or task-file OOTB, you may need to move up to a commercial product like JAMS. (Disclaimer: this may not even itself have the capabilities you require--I don't have direct experience with the product but folks I've worked with have mentioned it in a positive light).

Amagasaki answered 24/5, 2016 at 17:1 Comment(1)
Thank you. I appreciate your response. JAMS actually looks pretty slick. I'll have to read through their documentation to see if it fits my needs.Absher
Q
2

Create an API to schedule jobs dynamically after runtime. Your API can accept input via an HTTP Get/Put/Post/Delete etc, then run an instance of anything within your code upon the API call, using the data that you give it.

For example, say you have a hard coded Task A and Task B in your code and you want to schedule them to run dynamically using different parameters. You can create an API that will run the desired task at the specified time, using the parameters that you choose.

[HttpPost]
public IHttpActionResult Post([FromBody]TaskDto task)
{
    var job = "";
    if(task.TaskName == "TaskA"){
        job = BackgroundJob.Schedule(() => RunTaskA(task.p1,task.p2), task.StartTime);
    }
    if(task.TaskName == "TaskB"){
        job = BackgroundJob.Schedule(() => RunTaskB(task.p1,task.p2), task.StartTime);
    }

    if(!string.IsNullOrWhiteSpace(task.ContinueWith) && !string.IsNullOrWhiteSpace(job)){       
        if(task.ContinueWith == "TaskB"){
            BackgroundJob.ContinueWith(job, () => RunTaskB(task.p3,task.p4));
        }
        if(task.ContinueWith == "TaskA"){
            BackgroundJob.ContinueWith(job, () => RunTaskA(task.p3,task.p4));
        }
    }
    return Ok(job)
}

Then you can call the API using a JSON POST (example using javascript)

// Sending JSON data to start scheduled task via POST
//
var xhr = new XMLHttpRequest();
var url = "https://www.example.com/api/scheduletask";
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-type", "application/json");
xhr.onreadystatechange = function () {
    if (xhr.readyState === 4 && xhr.status === 200) {
        var json = JSON.parse(xhr.responseText);
    }
};
var data = JSON.stringify({"TaskName": "TaskA", "ContinueWith": "TaskB",
"StartTime": "2-26-2018 10:00 PM", "p1": "myParam1", "p2": true, 
"p3": "myParam3", "p4": false});
xhr.send(data);

And for completeness of the example here is the TaskDto class for this example

public class TaskDto
{

    public string TaskName { get; set; }
    public string ContinueWith { get; set; }
    public DateTime StartTime { get; set; }
    public string p1 { get; set; }
    public bool p2 { get; set; }
    public string p3 { get; set; }
    public bool p4 { get; set; }

}
Quern answered 26/2, 2018 at 17:37 Comment(0)
P
0

This Q&A is a bit long in the tooth by now, but the problem is still relevant. I stumbled on a potential solution just now while looking for the same thing. It's found at this gist.

Here's the code, in case the gist goes away:

ActualTimerJob.cs

using System;

namespace CustomGist.Hangfire {
  public class ActualTimerJob : TimerJob<ActualTimerJob> {
    public override JobId => "Actual timer job";
    
    protected override void PerformJobTasks(){
      //do the actual job here with full access to _cancellationToken and _context      
    }
  }
}

ITimerJob.cs

using Hangfire;
using Hangfire.Server;
using System;

namespace CustomGist.Hangfire {
  public interface ITimerJob {
    string JobId { get; }
    void Execute(IJobCancellationToken cancellationToken, PerformContext context);
    void Schedule(string cronExpression);
  }
}

Scheduler.cs

using Hangfire;
using Hangfire.Storage;
using System;

namespace CustomGist.Hangfire {
  public class Scheduler {
    public static void ScheduleRecurringTasks(){
      //Type the class name and CRON expression in, but could read this from json or xml if we wanted to
      //The class name is fully qualified and could be in another assembly.
      string className = "CustomGist.Hangfire.ActualTimerJob";
      string cronExpression = Cron.Daily;
      try {
        var timerJobType = Type.GetType(className);
        var timerJobInstance = (ITimerJob) Activator.CreateInstance(timerJobType ?? throw new InvalidOperationException($"Unable to get type {className}"));
        timerJobInstance.Schedule(cronExpression);
      } catch (Exception e) {
        //Handle exception here, if something fails when dynamically loading the type.
      }
    }
  }
}

TimerJob.cs

using Hangfire;
using Hangfire.Server;
using System;

namespace CustomGist.Hangfire {
  public abstract class TimerJob<T> : ITimerJob where T : ITimerJob {
    public abstract string JobId { get; }
    protected IJobCancellationToken _cancellationToken;
        protected PerformContext _context;
    
    protected abstract void PerformJobTasks();
    
    [DisableConcurrentExecution(0)]
    public void Execute(IJobCancellationToken cancellationToken, PerformContext context) {
      _cancellationToken = cancellationToken;
            _context = context;
      PerformJobTasks();
    }
    public void Schedule(string cronExpression) {
      if (string.IsNullOrWhiteSpace(cronExpression)) {
        RecurringJob.RemoveIfExists(JobId);
      }
      RecurringJob.AddOrUpdate<T>(JobId, x => x.Execute(JobCancellationToken.Null, null), cronExpression, TimeZoneInfo.Local);
    }
  }
}
Pluviometer answered 10/5 at 22:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.