10

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();
}
Nkosi
  • 191,971
  • 29
  • 311
  • 378
codeNinja
  • 1,358
  • 1
  • 19
  • 50
  • 1
    post your `Logger` class code. if you are trying to access `HttpContext` when running under quartz, you are having bad day - i ever had the [same issue](https://stackoverflow.com/q/48848034/4648586). – Bagus Tesa Sep 20 '18 at 00:20
  • @BagusTesa i have added the logger class code – codeNinja Sep 20 '18 at 01:03
  • 3
    well, i still stand for my guess that you calling `HttpContext.Current` not on a request is an issue.. `Quartz` when executing its task, it does not bound to any request and `HttpContext.Current` can return nulls. You had to find another way to find the real path without relying `HttpContext` probably using [`HttpRuntime.AppDomainPath` or some other](https://stackoverflow.com/a/22121094/4648586). though i'm not sure how they fares inside `Quartz`'s thread.. i havent tried myself.. – Bagus Tesa Sep 20 '18 at 01:26
  • is there a way to create a webservice and post to that webservice from the quartz job? – codeNinja Sep 20 '18 at 01:30
  • actually, there is a hack where you could put the bulk of your code logic into somekind of endpoint and then have the `Quartz` job to just hit that endpoint from the `Quartz` job using WebClient or HttpClient or anything else. hacky, but gets the job done. – Bagus Tesa Sep 20 '18 at 01:34
  • @codeNinja inner exception specifies that exception was raised from `RetrySyspro` class, can you post code that class as well? – Dipen Shah Sep 23 '18 at 08:07
  • @codeNinja, any feedback re provided answers? – Nkosi Sep 25 '18 at 15:43

2 Answers2

3

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.

Nkosi
  • 191,971
  • 29
  • 311
  • 378
2

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");
VDWWD
  • 32,238
  • 19
  • 56
  • 70