We are trying to use quartz for some of our scheduled tasks such as email and record duplication. It seems to be a perfect fit however, the documentation hasn't really been updated for DI stuff and repo unit of work patterns so we have an issue. We have a separate project in our solution where we want to house our scheduler and different jobs, which will have access to our service layer where each Job calls our service. The issue is, we aren't sure how to get access to the scheduler after it gets instantiated in the job runner project's main file.
Main file:
private static async Task Main(string[] args)
{
LogProvider.SetCurrentLogProvider(new ConsoleLogProvider());
// Grab the Scheduler instance from the Factory
StdSchedulerFactory factory = new StdSchedulerFactory();
IScheduler scheduler = await factory.GetScheduler();
// and start it off
await scheduler.Start();
// and last shut down the scheduler when you are ready to close your program
await scheduler.Shutdown();
}
Typical Job:
using Quartz;
using Compyl.AppLogic.AssessmentService;
namespace JobRunner.Jobs
{
public class AssessmentDuplicationJob : IJob
{
private readonly IAssessmentService _assessmentService;
public AssessmentDuplicationJob(IAssessmentService assessmentService)
{
_assessmentService = assessmentService;
}
public async Task Execute(IJobExecutionContext context)
{
var dataMap = context.MergedJobDataMap;
var id = dataMap.GetInt("id");
var shouldPopulateAnswers = dataMap.GetBoolean("shouldPopulateAnswers");
_assessmentService.DuplicateAssessment(id, shouldPopulateAnswers);
}
}
}
we want to have a class or method in the job runner project that houses the scheduler through DI and a method(s) like
[Inject] IScheduler scheduler
public ScheduleEntity (T entity)
{
var job = createJob(entity)//another method that creates the job and jobdatamap this is easy enough
var trigger = createTrigger(entity)// same as above
scheduler.ScheduleJob(job, trigger)
}
Then we would call the ScheduleEntity method wherever we needed it in our webapp.
Not sure if this is the "correct" way, but what we're doing is setting up the default scheduler in the DI container and then using it in each component we need it by calling the factory's get scheduler method, and then using a static class from our other project to create and schedule jobs, using the scheduler as a parameter.
Related
I have an extension to enqueue my view models pointing to an implementation of an interface IBackgroundJob
this are my extensions methods
private static readonly ActivitySource activitySource = new("MC.Hangfire.Extensions");
public static string Enqueue<T>(this T job, IBackgroundJobClient client)
{
return client.Enqueue<IBackgroundJob<T>>(ps => ps.AddTelemetry(null).EnqueueJob(null, job, JobCancellationToken.Null));
}
public static IBackgroundJob<T> AddTelemetry<T>(this IBackgroundJob<T> job, PerformContext context)
{
using var activity = activitySource.StartActivity($"Start Job {typeof(T).FullName} id {context.BackgroundJob.Id}", ActivityKind.Server);
activity?.SetTag("JobId", context.BackgroundJob.Id);
activity?.SetTag("JobJson", Newtonsoft.Json.JsonConvert.SerializeObject(job));
activity?.SetTag("Job", Newtonsoft.Json.JsonConvert.SerializeObject(context.BackgroundJob.Job));
return job;
}
My problem is that the EnqueueJob method is called, but the AddTelemetry method is not called before, how can I Add the telemetry information before calling all of my jobs, but in the context of the jobs, and of course not adding this code in all of my enqueue methods?
I'm looking for the Hangfire filters, but I think that there should be a way to inject the filter with the DI of the ASP.NET core application
I created this issue on github because I think that the problem with instrumentation is a little deeper in the code
https://github.com/HangfireIO/Hangfire/issues/2017
In an MVC5 web application using Net 4.8, I am trying to do the following:
When launching the application, I would like a scheduler to trigger a job every 2 Minutes.
The job reads and processes messages from a message queue.
I previously used Hangfire for this and it worked quite well.
However, I was told not to use Hangfire for this application. As an alternative, I opted for Quartz.NET here but I am currently having trouble setting up and triggering
the desired action. Following the Quartz.NET documentation, this is my current setup.
public class DummyJob :IJob
{
private readonly ISomeInterface _someInterface;
public DummyJob(ISomeInterface someInterface)
{
_someInterface = someInterface;
}
public async Task Execute(IJobExecutionContext context)
{
await _someInterface.ProcessMessages();
}
}
Next, the job configuration
public class JobScheduler
{
public static void Start()
{
// Run every 2 minutes
const string cron = #"0 0/2 * * * ?";
ISchedulerFactory schedulerFactory = new StdSchedulerFactory();
var scheduler = (IScheduler)schedulerFactory.GetScheduler().Result;
scheduler.Start();
var job = JobBuilder.Create<DummyJob>().Build();
var trigger = TriggerBuilder
.Create()
.WithCronSchedule(cron)
.Build();
scheduler.ScheduleJob(job, trigger);
}
}
I tried starting the job from both Startup.cs and Global.asax. Although the job was created, it did not fire when launching the application.
public void Configuration(IAppBuilder app)
{
[...]
JobScheduler.JobScheduler.Start();
}
What am I missing in terms of setting up QuartzNet? Do I need to put JobScheduler.JobScheduler.Start(); into Global.asax.cs or Startup.cs?
I've implemented the BackgroundQueue as explained here, and as shown:
public ActionResult SomeAction()
{
backgroundQueue.QueueBackgroundWorkItem(async ct =>
{
//Do some work...
});
return Ok();
}
I registered the BackgroundQueue with Autofac as:
builder.RegisterType<BackgroundQueue>()
.As<IBackgroundQueue>()
.SingleInstance();
So far so good. I call my controller action and the task is added to the queue. And there it stays without being executed.
So how do I get the task to execute?
The BackgroundQueue implementation that you took from the documentation is only one part to the solution: The background queue will just keep track of the jobs that you want to be executed.
What you will also need is right below that in the docs: The QueuedHostedService. This is a background service that gets registered with the DI container and is started when the application starts. From then on, it will monitor your BackgroundQueue and work off jobs as they get queued.
A simplified example implementation of this background service, without logging or error handling, could look like this:
public class QueuedHostedService : BackgroundService
{
private readonly IBackgroundQueue _backgroundQueue;
public QueuedHostedService(IBackgroundQueue backgroundQueue)
{
_backgroundQueue = backgroundQueue;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var workItem = await _backgroundQueue.DequeueAsync(stoppingToken);
await workItem(stoppingToken);
}
}
}
All the code samples I've seen so far for Azure WebJobs rely on some kind of trigger (e.g. TimerTrigger or QueueTrigger).
I am looking specifically at WebJobs SDK 3.x, by the way.
So. For a triggerless WebJob (Windows Service-alike one), am I expected to use NoAutomaticTrigger and find a way to kickoff my "main" code manually?
Or should I resort to implementing and registering a class that implements the IHostedService interface?
So far that's the approach I'm taking but it feels more of a hack than a recommended way.
I have not even tried to deploy this code and only ran it on my local machine, so I am afraid that the publishing process will confirm my code is not suitable for Azure WebJobs in its current form.
EntryPoint.cs
This is how the application is being bootstrap when the process is starting.
using Microsoft.Azure.ServiceBus;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace AbcCorp.Jobs
{
public static class Program
{
static async Task Main(string[] args)
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", false)
.Build();
var hostBuilder = new HostBuilder()
.ConfigureWebJobs(builder => { builder.AddAzureStorageCoreServices(); })
.ConfigureServices(serviceCollection =>
{
ConfigureServices(serviceCollection, config);
serviceCollection.AddHostedService<ConsoleApplication>();
});
using (var host = hostBuilder.Build())
await host.RunAsync();
}
private static IServiceCollection ConfigureServices(IServiceCollection services, IConfigurationRoot configuration)
{
services.AddTransient<ConsoleApplication>();
// ... more DI registrations
return services;
}
}
}
ConsoleApplication.cs
This would normally be implemented as a function with a trigger.
The thing is, I want this code to only run once on the process startup.
It will start listening on the service bus events using the regular Microsoft.Azure.ServiceBus SDK package.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using AbcCorp.Internal.Microsoft.Azure.ServiceBus;
using AbcCorp.Api.Messaging;
namespace AbcCorp.Jobs
{
public sealed class ConsoleApplication: IHostedService
{
private readonly IReceiver<SubmissionNotification> _messageReceiver;
private readonly MessageHandler _messageHandler;
public ConsoleApplication(IReceiver<SubmissionNotification> messageReceiver, MessageHandler messageHandler)
{
_messageReceiver = messageReceiver;
_messageHandler = messageHandler;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_messageReceiver.StartListening(_messageHandler.HandleMessage, _messageHandler.HandleException);
return Task.Delay(Timeout.Infinite);
}
public Task StopAsync(CancellationToken cancellationToken)
{
_messageReceiver.Dispose();
return Task.CompletedTask;
}
}
}
So you want a console application to run in a WebJob and listen to messages. You don't really care about WebJob magic like triggers, it's just a place to run your console app. I've done the exact same thing before.
I found the IHostedService abstraction to be very helpful, but I didn't like their SDK. I found it bloated and hard to use. I didn't want to take a large dependency in order use a large array of special magic Azure stuff, when all I wanted to do was run a console application in a WebJob for now, and maybe move it elsewhere later.
So I ended just deleting that dependency, stealing the Shutdown code from the SDK and writing my own Service Host. The result is on my Github Repo azure-webjob-host. Feel free to use it or raid it for ideas. I don't know, maybe if I did it again I'd have another attempt at getting the SDK to work, but I present this is a bit of an alternative to the SDK.
Basically I wrote an IServiceHost not too different from yours (except that StartAsync exited when stuff started instead of just hanging). Then I wrote my own service host, which is basically just a loop:
await _service.StartAsync(cancellationToken);
while (!token.IsCancellationRequested){await Task.Delay(1000);}
await _service.StopAsync(default);
Then I stole the WebJobsShutdownWatcher code from their repo.
Then I created an IServiceHost that started my message handler. (I was using Rabbit, which has nothing to do with triggers or azure stuff)
public class MessagingService : IHostedService, IDisposable
{
public MessagingService(ConnectionSettings connectionSettings,
AppSubscriberSettings subscriberSettings,
MessageHandlerTypeMapping[] messageHandlerTypeMappings,
ILogger<MessagingService> logger)
{
....
}
public async Task StartAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.WhenAll(subscribers.Value.Select(s => s.StartSubscriptionAsync()));
}
public async Task StopAsync(CancellationToken cancellationToken)
{
...
}
public void Dispose()
{
...
}
}
Then I put that all together into something like this:
IHostedService myService = new MyService();
using (var host = new ServiceHostBuilder().HostService(myService))
{
await host.RunAsync(default);
}
I have some workers attached to service bus topics and what we do is the following (ServiceBusClient is a custom Class that contains our Subscription Client):
public override Task StartAsync(CancellationToken cancellationToken)
{
_serviceBusClient.RegisterOnMessageHandlerAndReceiveMessages(MessageReceivedAsync);
_logger.LogDebug($"Started successfully the Import Client. Listening for messages...");
return base.StartAsync(cancellationToken);
}
public void RegisterOnMessageHandlerAndReceiveMessages(Func<Message, CancellationToken, Task> ProcessMessagesAsync)
{
// Configure the message handler options in terms of exception handling, number of concurrent messages to deliver, etc.
var messageHandlerOptions = new MessageHandlerOptions(ExceptionReceivedHandler)
{
// Maximum number of concurrent calls to the callback ProcessMessagesAsync(), set to 1 for simplicity.
// Set it according to how many messages the application wants to process in parallel.
MaxConcurrentCalls = 1,
// Indicates whether MessagePump should automatically complete the messages after returning from User Callback.
// False below indicates the Complete will be handled by the User Callback as in `ProcessMessagesAsync` below.
AutoComplete = false
};
// Register the function that processes messages.
SubscriptionClient.RegisterMessageHandler(ProcessMessagesAsync, messageHandlerOptions);
}
And then you can use DI to instantiate your service bus client and inject on the constructor of your Worker class.
Here i have the initialization of the singleton instance of my custom class Service Bus Client
services.AddSingleton<IServiceBusClient, ServiceBusClient>((p) =>
{
var diagnostics = p.GetService<EventHandling>();
var sbc = new ServiceBusClient(
programOptions.Endpoint,
programOptions.TopicName,
programOptions.Subscriber,
programOptions.SubscriberKey);
sbc.Exception += exception => diagnostics.HandleException(exception);
return sbc;
});
Then on this custom class, i initialize my subscription client
public ServiceBusClient(
string endpoint,
string topicName,
string subscriberName,
string subscriberKey, ReceiveMode mode = ReceiveMode.PeekLock)
{
var connBuilder = new ServiceBusConnectionStringBuilder(endpoint, topicName, subscriberName, subscriberKey);
var connectionString = connBuilder.GetNamespaceConnectionString();
ConnectionString = connectionString;
TopicName = topicName;
SubscriptionName = topicName;
SubscriptionClient = new SubscriptionClient(connectionString, topicName, subscriberName, mode);
}
You can check #george chen's answer from this post How to create service bus trigger webjob?
where instead of creating a receiver and registering a message handler, you can use the in built queue trigger and and write your message handler logic inside it.
I want to use task scheduler to create thread during application start.
I made it thanks to this and this, but something goes wrong and job is not running, of course is initialized before.
My class which is run before start:
[assembly: WebActivatorEx.PreApplicationStartMethod(
typeof(Application.App_Start.TaskScheduler), "Start")]
namespace Application.App_Start
{
public static class TaskScheduler
{
private static readonly IScheduler scheduler = new StdSchedulerFactory().GetScheduler();
private static void CreateTaskToDeleteTmpFiles(Object sender)
{
scheduler.Start();
//Create job which will be add to thread
IJobDetail job = JobBuilder.Create<DeleteTmpJob>()
.WithIdentity("ClearTmpFiles")
.StoreDurably()
.Build();
//Create thread which run the job after specified conditions
ITrigger trigger = TriggerBuilder.Create()
.WithIdentity("ClearTmpFiles")
.StartAt(DateBuilder.FutureDate(1, IntervalUnit.Second))
.Build();
//Add Job and Trigger to scheduler
scheduler.ScheduleJob(job, trigger);
}
}
}
My job class:
public class DeleteTmpJob : IJob
{
private IDocumentStore documentStore;
private IUploaderCollection uploaderCollection;
public DeleteTmpJob(IDocumentStore _documentStore, IUploaderCollection _uploaderCollection)
{
documentStore = _documentStore;
uploaderCollection = _uploaderCollection;
}
public void Execute(IJobExecutionContext context)
{
documentStore.ClearTmpDirectory();
}
}
Job is not running
Anyone can help?
Have you tried using an empty constructor for your job?
"Each (and every) time the scheduler executes the job, it creates a new instance of the class before calling its Execute(..) method. One of the ramifications of this behavior is the fact that jobs must have a no-arguement constructor."
You may need to implement your own JobFactory to allow you to use DI. How you implement it depends on which library you are using.
"When a trigger fires, the JobDetail (instance definition) it is associated to is loaded, and the job class it refers to is instantiated via the JobFactory configured on the Scheduler.The default JobFactory simply calls the default constructor of the job class using Activator.CreateInstance, then attempts to call setter properties on the class that match the names of keys within the JobDataMap. You may want to create your own implementation of JobFactory to accomplish things such as having your application's IoC or DI container produce/initialize the job instance."
source: see here
I had the same problem, when I deleted constructor job worked. First try to call base constructor, if it is still not working try to delete constructor.