We are leaving Russia. We are against aggression and war with Ukraine. It is a tragedy for our nations, it is a nightmare

Another instance of the error "Target method was not found"

TL;DR: Solution is in my 2nd reply

I’m getting this error for a new job I added and I’m struggling to understand what I’m doing wrong.

I have the following project structure:

  • Project A registers the dashboard and also acts as the default queue (which is not used)
  • Project B processes jobs, contains implementation of the job interface
  • Project C just enqueues jobs
  • Library - contains interface for the job and a few other stuff; referenced by all projects

These are the code snippets that I assume are relevant:

// This is from project C
_backgroundJobClient.Enqueue<IMyJob>(x => x.ExecuteAsync(date0, date1, cycle));

// This is the interface in my library
public interface IMyJob
{
      [Queue("worker_queue")]
      [DisplayName("My Job")]
      public Task ExecuteAsync(DateTime t0, DateTime tx, string cycle);
}

// This is the implementation from project B
public class MyJob : IMyJob
{
      public Task ExecuteAsync(DateTime t0, DateTime tx, string cycle)
      {
           // Do stuff
      }
}

// This is how I register the job in project B's startup
services.AddTransient<IMyJob, MyJob>();

My issue is as follows:

  • Assuming there are no errors, the job is enqueued successfully and finishes successfully
  • Job name (“My Job”) is displayed at all times in the dashboard
  • If the job fails, I can successfully requeue the job manually using the requeue button and the job will start processing again without issues
  • If I let the retry timer do its thing, processing doesn’t start and throws the following error
Failed
Can not change the state to 'Enqueued': target method was not found.

System.TypeLoadException
Could not load type 'Library.Jobs.IMyJob' from assembly 'Library, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.

I know most answers suggest deploying the code version to all services, but as far as I can tell I did this:

  • The job can be enqueued by project C, so this one can find the method
  • Project B can process the job, so this one can find the method
  • Project A not only displays the custom name, but I also logged typeof(IMyJob) at startup and it logs the full namespace

Does anyone have any suggestions? I’m really at a loss here.

Project B can process the job, so this one can find the method

This seems like it’s an incorrect statement or you wouldn’t be having an issue.

How are you configuring Hangfire in Project B? The default job activator just uses Activator.CreateInstance(type), which cannot create an instance of an interface.

Retrying the job manually is likely putting it into the default queue which is having Project A process it.

Retrying the job manually is likely putting it into the default queue which is having Project A process it.

The implementation of the MyJob is part of B’s assembly, project A doesn’t have access to any worker-related classes. In addition to this, logs show up on the correct machine (the one running service B).

This is how I’m configuring Hangfire in project B:

services.AddSingleton(new BackgroundJobServerOptions()
{
    ServerName = serverName,
    Queues = queues,
    WorkerCount = workerCount,
    HeartbeatInterval = hangfireSettings.HeartbeatInterval,
    ServerTimeout = hangfireSettings.ServerTimeout,
    ServerCheckInterval = hangfireSettings.ServerCheckInterval
});

services.AddHangfire((provider, config) =>
{
    config
        .UseSerilogLogProvider()
        .UseFilter(new PreserveOriginalQueueAttribute())
        .UsePostgreSqlStorage(hangfireDbConnectionString, postgreSqlStorageOptions);
});

services.AddHangfireServer();

services.AddTransient(typeof(IBackgroundJobClient), typeof(BackgroundJobClient));

After some more debugging, I found the lucky combination.

  1. The version of Hangfire.PostgreSql we were using was two years old, so I bumped it to 1.7.0 (still old, but looked like a good place to start at).
  2. I applied the queue attribute on the interface, not on its method.
// Before
public interface IMyJob
{
     [Queue("worker_queue")]
     public Task ExecuteAsync(...)
}

// After
[Queue("worker_queue")]
public interface IMyJob
{
     public Task ExecuteAsync(...)
}

These two things combined now allow the job to be automatically requeued.