Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

InvalidOperationException: JobStorage.Current property value has not been initialized #1991

Closed
regisoft opened this issue Jan 10, 2022 · 15 comments

Comments

@regisoft
Copy link

Hi
We are on .net6 and Hangfire 1.7.28.
We use the minimal api ... (no more startup.cs only program.cs)
With "UseHangfireServer" our program works. But "UseHangfireServer" is obsolete.
With "AddHangfireServer" we get...

System.InvalidOperationException: JobStorage.Current property value has not been initialized. You must set it before using Hangfire Client or Server API.
   at Hangfire.JobStorage.get_Current()
   at Hangfire.BackgroundJobClient..ctor()
   at Hangfire.BackgroundJob.<>c.<.cctor>b__45_0()
   at System.Lazy`1.CreateValue()
   at System.Lazy`1.get_Value()
   at Hangfire.BackgroundJob.<>c.<.cctor>b__45_1()
   at Hangfire.BackgroundJob.Schedule[T](Expression`1 methodCall, TimeSpan delay)

Any idea?

@akarboush
Copy link

This is because you are accessing the JobStorage before it has been fully initialized. Perhaps you try to add/edit jobs when starting the application.
Either create an IHostedService for adding/editing jobs, which is started after everything is initialized

services.AddHostedService<AddingJobsService>();

or set the static JobStorage manually before accessing it

JobStorage.Current = new SqlServerStorage("connectionString");

@igsoblechero
Copy link

Hi,

I had the same issue as @regisoft. I started digging in the decompiled code from Hangfire and found something: adding the following line of code solved everything:

services.BuildServiceProvider().GetRequiredService<IGlobalConfiguration>();

(This is from the Startup.ConfigureServices method, but of course it would work equally using the ServiceProvider instance that gets injected at the Startup.Configure method.)

This line needs to be run before any Hangfire-related sentence is run (i.e.: before instantiating a RecurrentJobManager or setting up any BackgroundJob). The rationale for this is: when AddHangfireServer is used, the configuration is set but deferred to the next time a IGlobalConfiguration instance needs to be retrieved, as it's done through the dependency injection framework. Adding this sentence retrieving the service (even though it doesn't need to be used later) runs that deferred code, and solves the issue of not having JobStorage.Current initialized.

I'd say this looks like some kind of bug in the side of the Hangfire package, since it doesn't make any sense to have to run this code on our side. If any, I'd at least appreciate to have this added to the documentation, if not fixed. Funnily enough, I have two different projects with ASP.NET Core and Hangfire: the first of them didn't need this sentence but the second one did, and I cannot tell why this is the case.

In any case, manually setting JobStorage.Current as a global variable shouldn't be needed as this should be done through AddHangfireServer, from my point of view. Any chance this is something that can be checked? Thanks!

@regisoft
Copy link
Author

Thanks @akarboush and @igsoblechero for the input.
We really try to add jobs when starting the application.

services.AddHostedService<AddingJobsService>();
... use a background service to create background services is a bit complicated in my opinion.

JobStorage.Current = new SqlServerStorage("connectionString");
... This works. But why should we define the Storage 2 times. Already defined it in AddHangfire().

services.BuildServiceProvider().GetRequiredService<IGlobalConfiguration>();
... This works. But i share @igsoblechero 's view that Hangfire should handle this itself/internal.

@akarboush
Copy link

@igsoblechero solution would work too, but I would advise against it since it is an antipattern and could results in more than one copy of singleton services being created. See

You can suppress the warning in that case. It’s an anti pattern in general. You can do it if you understand what exactly is happening and if you’re ok with it but there’s no support for building the container while trying to build it. What I mean is you have 2 containers and one of them will never be disposed.

Another solution would be to create background jobs in the Configure method in Startup. This method is called after ConfigureServices is called and all IHostedService are up and running. This means that the BackgroundJobServerHostedService of Hangfire is up and running too and the IGlobalConfiguration has already been retrieved. This would be a cleaner solution IMO.

@igsoblechero
Copy link

Maybe a put a worse example than expected, but this is how I finally left my code because I wanted to add a RecurrentJobManager as a singleton but I needed that line before.

Anyway, I had the same issue putting the new RecurrentJobManager() at the end of Startup.ConfigureServices, so no, sadly IGlobalConfiguration is not well configured at that point so I had to had that serviceProvider.GetRequiredService<IGlobalConfiguration>(); sentence added right before.

@igsoblechero
Copy link

Btw, @akarboush, thanks for letting me know that antipattern!

@akarboush
Copy link

@igsoblechero adding RecurringJobManager to the DI container does not contradict my saying. You can still add it as a singleton or whatever as long as you don't create jobs in the Startup.ConfigureServices method.

The creation of the job(s) can be easily done in the Startup.Configure method, since you can retrieve any dependency already registered in the DI container Services injected into Startup

 public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IRecurringJobManager manager)
 {
       manager.AddOrUpdate("id", () => Console.WriteLine(), Cron.Daily);
       .........

@igsoblechero
Copy link

Okay, I have some news from your proposal. Efectively, if I'm using instantiating a IRecurringJobManager from dependency injection it works, but if I manually use the new RecurringJobManager() constructor (which is what it's documented and it's what I was doing up to now) then it's when I receive the error that was mentioned at the beginning of this thread.

@regisoft
Copy link
Author

In my case the exception is thrown on this statement:

BackgroundJob.Schedule<ProcessXXJob>((job) => job.Process(), startDelay);
//.. and also on this...
RecurringJob.AddOrUpdate<SyncJob>((job) => job.ExecuteAsync(null), Cron.Minutely());

We never used XXXJobManager.

@akarboush
Copy link

the BackgroundJob class is just a facade for the IBackgroundJobClient interface and its default implementation – BackgroundJobClient class. doc

and same for IRecurringJobManager/RecurringJob

@mtorromacco
Copy link

I've had the same issue and Sergey Odinokov helped me a lot! #1967

@regisoft
Copy link
Author

As @akarboush and #1967 proposed, i replaced the static BackgroundJob.Schedule... with app.Services.GetService<IBackgroundJobClient>().Schedule... and it seems to work.
Thanks to everybody

@crossr1
Copy link

crossr1 commented Aug 27, 2022

I discovered that if ".UseSqlServerStorage" is not on the first line of the configuration, it will cause this problem.

This is what I originally had:
// Add Hangfire services.
services.AddHangfire(configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseConsole()
.UseJobsLogger()
.UseRecurringJobAdmin(true, typeof(Startup).Assembly)
.UseDefaultActivator()
.UseSqlServerStorage(Configuration.GetConnectionString("HangfireConnection"), new SqlServerStorageOptions
{
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
QueuePollInterval = TimeSpan.Zero,
UseRecommendedIsolationLevel = true,
DisableGlobalLocks = true
}));

This solved the problem:
// Add Hangfire services.
services.AddHangfire(configuration => configuration
.UseSqlServerStorage(Configuration.GetConnectionString("HangfireConnection"), new SqlServerStorageOptions
{
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
QueuePollInterval = TimeSpan.Zero,
UseRecommendedIsolationLevel = true,
DisableGlobalLocks = true
})
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseConsole()
.UseJobsLogger()
.UseRecurringJobAdmin(true, typeof(Startup).Assembly)
.UseDefaultActivator()
);

@migc44
Copy link

migc44 commented Nov 17, 2023

@crossr1 unfotunatly that did not fix my issue but @igsoblechero your solution of adding

services.BuildServiceProvider().GetRequiredService<IGlobalConfiguration>();

to my start up file did help me. I also have two MVC projects and my first one works just fine but the second one requires this line of code to work. Perhaps the fact that both of my Web applications are using the same sql database might have something to do with it. Anyway below is my start up configuration of hangfire

services.AddDbContext<SJRContext>(options =>
          options.UseLazyLoadingProxies().UseSqlServer(Configuration.GetConnectionString("SJRContext"),
              sqlServerOptionsAction: sqlOptions =>
              {
                  sqlOptions.EnableRetryOnFailure(
                      maxRetryCount: 10,
                      maxRetryDelay: System.TimeSpan.FromSeconds(30),
                      errorNumbersToAdd: null);
                  sqlOptions.CommandTimeout((int)TimeSpan.FromMinutes(5).TotalSeconds);
              }));
services.AddHangfire(configuration => configuration
             .UseSqlServerStorage(Configuration.GetConnectionString("SJRContext"), new SqlServerStorageOptions
             {
                 CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
                 SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
                 QueuePollInterval = TimeSpan.Zero,
                 UseRecommendedIsolationLevel = true,
                 DisableGlobalLocks = true
             })
             .WithJobExpirationTimeout(TimeSpan.FromDays(30))
             .SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
             .UseSimpleAssemblyNameTypeSerializer()
             .UseRecommendedSerializerSettings());
services.BuildServiceProvider().GetRequiredService<IGlobalConfiguration>();

Thank you @igsoblechero

@Dhruv-0987
Copy link

Thanks @igsoblechero and @akarboush both your solutions helped a lot but the application I have doesn't have straightforward startup.cs and I decided to force execute that config logic with @igsoblechero fix. Thanks again 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

7 participants