Avoid un-stoppable webjobs using JobHost

We started usage of Azure continuous webjobs due to the flexibility in several scenarios of Azure Service Bus messages. The deployment was easy, either simple C# Console application or nodejs for our needs. We discover quite lately that the Scale Out functionality is great if the number of message to process is high.

Coupled with our Visual Studio Team Service automatic builds & releases updating jobs, we were close to heaven.

Heaven? not completely, our C# webjobs were not (never) exiting properly ...

  • stop button not really working (even if displaying disabling / stopping / stopped status)
  • phantom / duplicate processes visible in Kudu creating several webjobs ... with different version after a deployment from VSTS
  • aggressive kill method not working properly (below on script we used)

still no luck.
Of course we knew the root cause, as mainly listening to Service Bus queues or subscriber, we were using queue/subscriber.OnMessage() event which aren't synchronous. (using a infinite loop was a no go for me)
Our way of preventing jobs to exit was using basic Console.ReadLine();. Was pretty effective, jobs were never stopping ...

Microsoft PFE supports wasn't really helpful and tried to push us into the while loop direction, without success. Using C# Webjob SDK was clunky (compared with the simplicity of a simple Console Application and so many dependencies ...), but at one point, we gave a try.

In our final scenario, our job should

  • be able to keep listening to Service Bus : JobHost.RunAndBlock() will block the main thread
  • gracefully shutdown for cleanup purpose : WebJobsShutdownWatcher().Token will detect if a shutdown action has been triggered
public class Program  
{
    static JobHost host = null;
    static void Main(string[] args)
    {
        var cancellationToken = new WebJobsShutdownWatcher().Token;
        cancellationToken.Register(() =>
        {
            // gracefully stops activities 
            // delete process dependencies
            // flush telemetry
            // etc

            host.Stop(); // allow to go through the RunAndBlock()
        });

        // HostId is mandatory
        // Both ConnectionString can be set to null preventing any dependencies
        host = new JobHost(new JobHostConfiguration { HostId = Guid.NewGuid().ToString().Substring(0, 32), StorageConnectionString = string.Empty, DashboardConnectionString = string.Empty });
        host.Call(typeof(Program).GetMethod("DoLongStuff"));
        host.RunAndBlock();

        Trace.TraceInformation("webjob fully terminated");
    }

    [NoAutomaticTrigger]
    public void DoLongStuff()
    {
        // do stuff
    }
}
  • DoLongStuff has to be public, meaning that Program has to be switch to public (default is internal). [NoAutomaticTrigger] attribute is mandatory.
  • JobHostConfiguration
    • HostId is mandatory and need to be maximum 3 characters long, we split a new Guid.
    • Both ConnectionString can be empty, no dependencies with any Azure Storage !
  • cancellationToken.Register() has to call host.Stop() in order to go through RunAndBlock().

in your log stream, you will now get few more starting & stopping messages then previously

PID[2736] Information Found the following functions:  
PID[2736] Information xxxxx.Program.DoLongStuff  
PID[2736] Information Executing 'Program.DoLongStuff' (Reason='This function was programmatically called via the host APIs.', Id=e71c8365-d15c-4e9f-901f-afb77dcb7e02)  
PID[2736] Information Executed 'Program.DoLongStuff' (Succeeded, Id=e71c8365-d15c-4e9f-901f-afb77dcb7e02)  
PID[2736] Information Job host started  
[...]
PID[2736] Information Job host stopped  

Fabien Camous

Read more posts by this author.