MassTransit: MassTransit Quartz integration breaks Quartz jobs

Is this a bug report?

Yes

Can you also reproduce the problem with the latest version?

Yes

Environment

  1. Operating system: Windows 10
  2. Visual Studio version: Rider 2020.3
  3. Dotnet version: 5.0-rc1

Steps to Reproduce

  1. Add Quartz with AspNetCore integration
  2. Add MassTransit and configure message scheduler
  3. Add any Quartz job with injected service (DbContext, Logger, eg)
  4. Start host

Expected Behavior

Quartz jobs creating with MicrosoftDependencyInjectionScopedJobFactory, MassTransit jobs creating with MassTransitJobFactory

Actual Behavior

MassTransit overrides JobFactory and breaks all existing Quartz jobs

// SchedulerBusObserver.PostStart
_scheduler.JobFactory = new MassTransitJobFactory(bus);

Reproducible Demo

services.AddQuartz(options =>
{
        // Store configuration, etc

        var jobKey = new JobKey("RequestSentJob", "Template");
	options.AddJob<RequestSentJob>(job =>
	{
		job.WithIdentity(jobKey)
			.StoreDurably()
			.WithDescription("Periodical request sent");
	});

	var triggerKey = new TriggerKey("RequestSentJobTrigger", "Template");
	options.AddTrigger(trigger =>
	{
		trigger.ForJob(jobKey)
			.WithIdentity(triggerKey)
			.WithCronSchedule("*/3 * * * * ?*");
	});
}).AddQuartzServer(options => options.WaitForJobsToComplete = true);

services.AddMassTransit(options =>
{
	options.AddMessageScheduler(new Uri("queue:masstransit.quartz"));

	options.UsingRabbitMq((context, rabbitmq) =>
	{
                // Host configuration, etc    

		rabbitmq.UseInMemoryScheduler(
			context.GetRequiredService<ISchedulerFactory>(),
			"masstransit.quartz");
        }
}

public class RequestSentJob : IJob
{
	private readonly IBus bus;
	private readonly ILogger<RequestSentJob> logger;

	public RequestSentJob(IBus bus, ILogger<RequestSentJob> logger)
	{
		this.bus = bus;
		this.logger = logger;
	}

	public async Task Execute(IJobExecutionContext context)
	{
		logger.LogInformation("Sending: RequestSent");

		await bus.Publish(new RequestSent
		{
			Text = DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)
		});
	}
}

Thrown exception

Quartz.Core.ErrorLogger[0]
      An error occurred instantiating job to be executed. job= 'Template.RequestSentJob, message=Exception has been thrown by the target of an invocation.'
      Quartz.SchedulerException: Problem instantiating type 'AppAny.Template.RequestSentJob: Exception has been thrown by the target of an invocation.'
       ---> System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
       ---> Quartz.SchedulerException: The job class does not have a supported constructor: AppAny.Template.RequestSentJob
         at MassTransit.QuartzIntegration.MassTransitJobFactory`1.CreateConstructor()
         at MassTransit.QuartzIntegration.MassTransitJobFactory`1..ctor(IBus bus)
         --- End of inner exception stack trace ---
         at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions)
         at System.Reflection.RuntimeConstructorInfo.Invoke(BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
         at System.RuntimeType.CreateInstanceImpl(BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture)
         at System.Activator.CreateInstance(Type type, BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes)
         at System.Activator.CreateInstance(Type type, Object[] args)
         at MassTransit.QuartzIntegration.MassTransitJobFactory.CreateJobFactory(Type type)
         at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
         at MassTransit.QuartzIntegration.MassTransitJobFactory.NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
         at Quartz.Core.JobRunShell.Initialize(QuartzScheduler sched, CancellationToken cancellationToken)
         --- End of inner exception stack trace --- [See nested exception: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an
invocation.
       ---> Quartz.SchedulerException: The job class does not have a supported constructor: AppAny.Template.RequestSentJob
         at MassTransit.QuartzIntegration.MassTransitJobFactory`1.CreateConstructor()
         at MassTransit.QuartzIntegration.MassTransitJobFactory`1..ctor(IBus bus)
         --- End of inner exception stack trace ---
         at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions)
         at System.Reflection.RuntimeConstructorInfo.Invoke(BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
         at System.RuntimeType.CreateInstanceImpl(BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture)
         at System.Activator.CreateInstance(Type type, BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes)
         at System.Activator.CreateInstance(Type type, Object[] args)
         at MassTransit.QuartzIntegration.MassTransitJobFactory.CreateJobFactory(Type type)
         at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
         at MassTransit.QuartzIntegration.MassTransitJobFactory.NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
         at Quartz.Core.JobRunShell.Initialize(QuartzScheduler sched, CancellationToken cancellationToken)]

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 16 (16 by maintainers)

Commits related to this issue

Most upvoted comments

OK some rough edges but showing what I’ve played with. I’ve tweaked Quartz code a bit so this won’t compile yet without Quartz release.

https://github.com/MassTransit/MassTransit/pull/2067

  • new job factory that inherits from Quartz’s one, allow DI to jobs and simplified

https://github.com/MassTransit/Sample-Quartz/pull/1

  • configure Quartz and allow to use same scheduler as DI now understands the Microsoft DI being used
  • jobs and consumers can use DI
  • configuration with Options pattern, adding to existing, file being the starting point

Sorry for the noise on the second one, I was playing around and wanted to see recurring schedule in action with custom job.

Scheduler factory is a kind of a singleton, so it’s a holder of configuration how to create a scheduler. Well actually you can build multiple schedulers by name but usually you have on scheduler per process so having multiple named ones isn’t the most battle-tested use case. But for one service collection it only makes sense to have one such single factory as there’s no support for named singletons.

So far my thinking is to adapt MassTransit’s job factory to either work the same way as scoped/regular DI job factory that Quartz ships with (do service provider lookup) or even add possibility to hook some lifecycle events on Quartz’s side when Quartz produces jobs. It’s not a big deal to add service provider support which would allow jobs being injected with services in the container - or that’s what I think so far.

What I’ve seen so far MassTransit wants to add some metadata to job data map and inject properties. The property injection on MT side probably is a bit more clever/faster and then there’s the support for Uri type (haven’t tested what Quartz feels about that OOTB).

But like Chris mentioned I probably will toy around with the possibilities and try to produce some kind of PR for discussion and refinement. This might involve opening some APIs or allowing variation in Quartz implementation. Would of course love to hear what bugs the most and hinders the ability to use polymorphism or similar tools.