Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Error when a Quartz job runs with "Object Reference not set to an instance of an object"

I have a Quartz job that is setup during Application_Start in the Global.asax.cs

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    Logger.log("About to Setup Retry Job");
    JobScheduler.Start();
}

This calls the Start method which then schedules the job.

The job runs every 20 seconds and throws an exception. Here is my Job.

public class RetryTempJob : IJob
{
    public async Task Execute(IJobExecutionContext context)
    {
        try
        {
            Logger.log("Executing Job");
            new ProcessOrder().retryFailedOrders();
            //Logger.log("Done Executing Syspro Job");
            await Console.Error.WriteLineAsync("Done Executing Syspro Job");
        }
        catch (Exception se)
        {
            await Console.Error.WriteLineAsync("" + se.InnerException);
        }
    }
}

An exception is thrown at this line Logger.log("Executing Job");. This is a `Static method that opens a log file and write to it. This method works everywhere else in my site.

Here is the Exception Message: {"Object reference not set to an instance of an object."}

The InnerException is NULL. Here is the stack:

DarwinsShoppingCart.dll!DarwinsShoppingCart.SharedClasses.JobScheduler.RetrySyspro.Execute(Quartz.IJobExecutionContext context) Line 69 C#
    Quartz.dll!Quartz.Core.JobRunShell.Run(System.Threading.CancellationToken cancellationToken)    Unknown
    mscorlib.dll!System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start<Quartz.Core.JobRunShell.<Run>d__9>(ref Quartz.Core.JobRunShell.<Run>d__9 stateMachine)    Unknown
    Quartz.dll!Quartz.Core.JobRunShell.Run(System.Threading.CancellationToken cancellationToken)    Unknown
    Quartz.dll!Quartz.Core.QuartzSchedulerThread.Run.AnonymousMethod__0()   Unknown
    mscorlib.dll!System.Threading.Tasks.Task<System.Threading.Tasks.Task>.InnerInvoke() Unknown
    mscorlib.dll!System.Threading.Tasks.Task.Execute()  Unknown
    mscorlib.dll!System.Threading.Tasks.Task.ExecutionContextCallback(object obj)   Unknown
    mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown
    mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown
    mscorlib.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref System.Threading.Tasks.Task currentTaskSlot)    Unknown
    mscorlib.dll!System.Threading.Tasks.Task.ExecuteEntry(bool bPreventDoubleExecution) Unknown
    mscorlib.dll!System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() Unknown
    mscorlib.dll!System.Threading.ThreadPoolWorkQueue.Dispatch()    Unknown
    mscorlib.dll!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() Unknown

Here is my Logger class code

public static void log(string strLog)
{
    StreamWriter log;
    FileStream fileStream = null;
    DirectoryInfo logDirInfo = null;
    FileInfo logFileInfo;
    string username = Environment.UserName;
    string logFilePath = HttpContext.Current.Server.MapPath("~/log/Log.txt");
    logFileInfo = new FileInfo(logFilePath);
    logDirInfo = new DirectoryInfo(logFileInfo.DirectoryName);
    double fileSize = ConvertBytesToMegabytes(logFileInfo.Length);
    if (fileSize > 30)
    {
        string FileDate = DateTime.Now.ToString().Replace("/", "-").Replace(":", "-");
        string oldfilepath = HttpContext.Current.Server.MapPath("~/log/log-" + FileDate + ".txt");
        File.Move(logFileInfo.FullName, oldfilepath);
    }
    if (!logFileInfo.Exists)
    {
        fileStream = logFileInfo.Create();
    }
    else
    {
        fileStream = new FileStream(logFilePath, FileMode.Append);
    }
    log = new StreamWriter(fileStream);

    log.WriteLine(DateTime.Now.ToString("MM-dd HH:mm:ss") + " " + username + " " + strLog);
    log.Close();
}
like image 433
codeNinja Avatar asked Sep 17 '18 01:09

codeNinja


2 Answers

Given that the logger can be called within a request and is also possible to be invoked outside of a request. If a job is started and there is no request HttpContext will be null

Consider decoupling the logger from implementation concerns like the HttpContext and abstract the process of mapping the path and taking advantage of dependency injections

public interface IPathProvider {
    string MapPath(string path);
}

You should also avoid the static logger code smell as it makes the code difficult to maintain and test in isolation.

public interface ILogger {
    void log(string message);
    //...
}

public class Logger : ILogger {
    private readonly IPathProvider pathProvider;

    public Logger(IPathProvider pathProvider) {
        this.pathProvider = pathProvider;
    }

    public void log(string strLog) {
        FileStream fileStream = null;
        string username = Environment.UserName;
        string logFilePath = pathProvider.MapPath("~/log/Log.txt");
        var logFileInfo = new FileInfo(logFilePath);
        var logDirInfo = new DirectoryInfo(logFileInfo.DirectoryName);
        double fileSize = ConvertBytesToMegabytes(logFileInfo.Length);
        if (fileSize > 30) {
            string FileDate = DateTime.Now.ToString().Replace("/", "-").Replace(":", "-");
            string oldfilepath = pathProvider.MapPath("~/log/log-" + FileDate + ".txt");
            File.Move(logFileInfo.FullName, oldfilepath);
        }
        if (!logFileInfo.Exists) {
            fileStream = logFileInfo.Create();
        } else {
            fileStream = new FileStream(logFilePath, FileMode.Append);
        }
        using(fileStream) {
            using (var log = new StreamWriter(fileStream)) {
                log.WriteLine(DateTime.Now.ToString("MM-dd HH:mm:ss") + " " + username + " " + strLog);
                log.Close();
            }
        }
    }
}

and have the job explicitly depend on the abstraction.

public class RetryTempJob : IJob {
    private readonly ILogger logger;

    public RetryTempJob(ILogger logger) {
        this.logger = logger;
    }

    public async Task Execute(IJobExecutionContext context) {
        try {
            logger.log("Executing Job");
            new ProcessOrder().retryFailedOrders();
            //logger.log("Done Executing Syspro Job");
            await Console.Error.WriteLineAsync("Done Executing Syspro Job");
        } catch (Exception se) {
            await Console.Error.WriteLineAsync("" + se.InnerException);
        }
    }
}

There is more that can be abstracted out here but that is a little outside of the scope of the example.

Now that the design issues have been addressed, we can look at the path provider implementation to take care of the HttpContext situation.

Server.MapPath() requires an HttpContext while HostingEnvironment.MapPath does not.

Reference What is the difference between Server.MapPath and HostingEnvironment.MapPath?

The implementation can try checking the context for null but Server.MapPath() eventually calls HostingEnvironment.MapPath() so it would be better to just use HostingEnvironment.MapPath()

public class PathProvider : IPathProvider {
    public string MapPath(string path) {
        return HostingEnvironment.MapPath(path);
    }
}

What is left now is to configure the scheduler to allow dependency injection and for you to decide which DI framework you want to use.

Create a job factory that inherits from the default Quartz.NET job factory SimpleJobFactory. This new job factory is going to take an IServiceProvider in its constructor, and override the default NewJob() method provided by SimpleJobFactory.

class MyDefaultJobFactory : SimpleJobFactory {
    private readonly IServiceProvider container;

    public MyDefaultJobFactory(IServiceProvider container) {
        this.container = container;
    }

    public override IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) {
        IJobDetail jobDetail = bundle.JobDetail;
        Type jobType = jobDetail.JobType;
        try {
            // this will inject any dependencies that the job requires
            return (IJob) this.container.GetService(jobType); 
        } catch (Exception e) {
            var errorMessage = string.Format("Problem instantiating job '{0}'.", jobType.FullName);
            throw new SchedulerException(errorMessage, e);
        }
    }
}

There are many DI frameworks to choose from, but for this example I an using the .Net Core Dependency Injection Extension, which, because of the modular nature of .Net Core, allows for it to be dropped easily into your project.

Finally, configure the service collection on application start

protected void Application_Start() {
    AreaRegistration.RegisterAllAreas();
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    var services = new ServiceCollection();
    var serviceProvider = ConfigureService(services);
    var logger = serviceProvider.GetService<ILogger>();
    logger.log("About to Setup Retry Job");
    var jobScheduler = serviceProvider.GetRequiredService<IScheduler>();

    //...add jobs as needed
    jobScheduler.ScheduleJob(.....);

    //and start scheduler
    jobScheduler.Start();
}

private IServiceProvider ConfigureService(IServiceCollection services) {
    //Add job dependencies
    services.AddSingleton<ILogger, Logger>();
    services.AddSingleton<IPathProvider, PathProvider>();
    //add scheduler
    services.AddSingleton<IScheduler>(sp => {
        var scheduler = new StdSchedulerFactory().GetScheduler();
        scheduler.JobFactory = new MyDefaultJobFactory(sp);
        return scheduler;
    });

    //...add any other dependencies

    //build and return service provider
    return services.BuildServiceProvider();
}

As I said before you can use any other DI/IoC container you want. The refactored code is now flexible enough for you to swap one for the other by simply wrapping it in a IServiceProvider derived class and giving it to the job factory.

And by cleaning the code from concerns that make the code smell, you can manage it easier.

like image 172
Nkosi Avatar answered Oct 17 '22 14:10

Nkosi


There is no HttpContext when using a Quarz Job. It runs i a separate thread. So on a website with a Quarz Job I use HostingEnvironment

So insead of

HttpContext.Current.Server.MapPath("~/log/Log.txt")

Use

using System.Web.Hosting;

HostingEnvironment.MapPath("~/log/Log.txt");
like image 36
VDWWD Avatar answered Oct 17 '22 16:10

VDWWD