Calling Shutdown Tasks in a NancyFX self host singleton
I'm currently trying to find a way of calling a task that needs to be called at shutdown of a singleton object created in TinyIOC inside a NancyFX self hosted application.
As of yet, I've been unable to come up with an answer, and I'm also open to better ideas of implementing the scenario I'm about to describe.
Overview
I have a PHP based web application that I'm working on, because this is PHP, there's no spinning thread/server to sit listening and processing long running tasks, the php code exists for the life span of the browser request requesting it.
The application in question needs to make some requests to a web service that can potentially take time to complete, and as a result I've come up with the idea of implementing some back end services in C#, using Topshelf, NancyFX and Stackexchange.Redis.
The service is a standard NancyFX self host console app as follows:
Program.cs
using Topshelf;
namespace processor
{
public class Program
{
static void Main()
{
HostFactory.Run(x =>
{
x.UseLinuxIfAvailable();
x.Service<ServiceApp>(s =>
{
s.ConstructUsing(app => new ServiceApp());
s.WhenStarted(sa => sa.Start());
s.WhenStopped(sa => sa.Stop());
});
});
}
}
}
ServiceApp.cs
using System;
using Nancy.Hosting.Self;
namespace processor
{
class ServiceApp
{
private readonly NancyHost _server;
public ServiceApp()
{
_server = new NancyHost(new Uri(Settings.NancyUrl));
}
public void Start()
{
Console.WriteLine("processor starting.");
try
{
_server.Start();
}
catch (Exception)
{
throw new ApplicationException("ERROR: Nancy Self hosting was unable to start, Aborting.");
}
Console.WriteLine("processor has started.");
}
public void Stop()
{
Console.WriteLine("processor stopping.");
_server.Stop();
Console.WriteLine("processor has stopped.");
}
}
}
ProcessorKernel.cs
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using StackExchange.Redis;
namespace processor
{
public class ProcessorKernel
{
private readonly ConnectionMultiplexer _redis;
private ISubscriber _redisSubscriber;
public ProcessorKernel()
{
try
{
_redis = ConnectionMultiplexer.Connect(Settings.RedisHost);
}
catch (Exception ex)
{
throw new ApplicationException("ERROR: Could not connect to redis queue, aborting.");
}
RegisterProcessor();
RedisMonitor();
}
~ProcessorKernel()
{
var response = RequestDeregistration();
// Do some checks on the response here
}
private RegistrationResponse RequestRegistration()
{
string registrationResponse = string.Empty;
try
{
registrationResponse = Utils.PostData("/processorapi/register", new Dictionary<string, string>
{
{"channelname", Settings.ChannelName},
});
}
catch (ApplicationException ex)
{
throw new ApplicationException("ERROR: " + ex.Message + " occured while trying to register. Aborting");
}
return JsonConvert.DeserializeObject<RegistrationResponse>(registrationResponse);
}
private DeregistrationResponse RequestDeregistration()
{
var deregistrationResponse = "";
try
{
deregistrationResponse = Utils.PostData("/processorapi/deregister", new Dictionary<string, string>
{
{"channelname", Settings.ChannelName},
});
}
catch (ApplicationException ex)
{
throw new ApplicationException("ERROR: " + ex.Message + " occured while trying to deregister. Aborting");
}
return JsonConvert.DeserializeObject<DeregistrationResponse>(deregistrationResponse);
}
private void RegisterProcessor()
{
Console.WriteLine("Registering processor with php application");
RegistrationResponse response = RequestRegistration();
// Do some checks on the response here
Console.WriteLine("Processor Registered");
}
private void RedisMonitor(string channelName)
{
_redisSubscriber = _redis.GetSubscriber();
_redisSubscriber.Subscribe(channelName, (channel, message) =>
{
RedisMessageHandler(message);
});
}
private void RedisMessageHandler(string inboundMessage)
{
RedisRequest request = JsonConvert.DeserializeObject<RedisRequest>(inboundMessage);
// Do something with the message here
}
}
}
As well as these 3 classes, I also have a couple of standard NancyFX routing modules, to handle various endpoints etc.
All if this works great, as you can see from the ProcessorKernel I make a call to a method called RegisterProcessor this method, contacts the web application responsible for managing this processor instance and registers itself, essentially saying to the web app, hey I'm here, and ready for you to send me requests via the redis queue.
This ProcessorKernel however is instantiated as a singleton, using the Nancy Bootstrapper:
using Nancy;
using Nancy.Bootstrapper;
using Nancy.TinyIoc;
namespace processor
{
public class Bootstrapper : DefaultNancyBootstrapper
{
protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
{
base.ApplicationStartup(container, pipelines);
container.Register<ProcessorKernel>().AsSingleton();
}
}
}
The reason it has to be a singleton is because it has to accept the queued requests from redis, then process them and send them to various webservices at different locations.
There are several different channels, but ONLY one processor can be listening to one channel at a time. This ultimately means that when this ProcessorKernel shuts down, it has to make a call to the PHP web application to "De-Register" itself, and inform the application that nothing is serving the channel.
It's finding a good hook point to call this de-registration that's causing me the problem.
Approaches Tried
As you can see from the code above I've tried using the class destructor, and while not the best solution in the world, it does get called but the class get's torn down before it returns, and so the actual de-registration never completes, and thus never de-registers correctly.
I've also tried making the singleton class an IDisposable, and seeing if putting the de-register code inside "Destroy", but that didn't get called at all.
Beacuse I'm using Topshelf and I have the Start/Stop methods inside ServiceApp I know that I can call my routines there, but I can't get access to the singleton ProcessorKernel at this point beacuse it's not a Nancy Module and so TinyIOC doesn't resolve the dependency.
I initially also tried to create the ProcessorKernel in Program.cs and pass it in via the ServiceApp constructor, and while this created a service, it wasn't registered as a singleton, and wasn't accessible inside the Nancy Route modules, so the status endpoints where unable to query the created instance for status updates as they only got the singleton that TinyIOC created.
If I could get access to the singleton in Start/Stop then I could easily just place a couple of public methods on it to call the register/deregister functions.
For completeness, the code I'm using to send the post requests to the PHP web app is as follows:
Utils.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Text;
namespace processor
{
public class Utils
{
public static string PostData(string url, Dictionary<string, string> data)
{
string resultContent;
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(Settings.AppBase);
var payload = new List<KeyValuePair<string, string>>();
foreach (var item in data)
{
payload.Add(new KeyValuePair<string, string>(item.Key, item.Value));
}
var content = new FormUrlEncodedContent(payload);
var result = client.PostAsync(url, content).Result;
resultContent = result.Content.ReadAsStringAsync().Result;
var code = result.StatusCode;
if (code == HttpStatusCode.InternalServerError)
{
Console.WriteLine(resultContent);
throw new ApplicationException("Server 500 Error while posting to: " + url);
}
if (code == HttpStatusCode.NotFound)
{
Console.WriteLine(resultContent);
throw new ApplicationException("Server 404 Error " + url + " was not a valid resource");
}
}
return resultContent;
}
}
}
Summary
All I need is a reliable way to call the de-register code, and ensure the service informs the php-app that it's going away, because of the nature of the php-app I can't send a message back to it using redis as it's simply not listening, the app is a standard linear php app running under the Apache web-server.
I'm open to better ideas on how to architect this by the way, as long as there is only ever one instance of the actual ProcessorKernel running. Once it's registered it has to process every request sent to its channel, and since requests being processed may be accessed several times and processed by several different web services, then those requests need to be held in memory until their processing is fully complete.
Thanks in advance for any thoughts on this.
Update - The Next Day :-)
So after seeing Kristian's answer below, and seeing his/GrumpyDev's and Phillip Haydon's replies on my Twitter stream, I changed my Singleton class back to an IDisposable, got rid of the ~Finalizer, then changed ServiceApp.cs so that it called Host.Dispose() rather than Host.Stop()
This then correctly called the Dispose routine on my singleton class, which allowed me to shut things down correctly.
I did also come up with another workaround, which I have to admit
However in the spirit of sharing (and at the risk of embarrassment) I'm going to detail it here NOTE: I don't actually recommend doing this
First off I adjusted ServiceApp.cs so that it now looks like:
using System;
using Nancy.Hosting.Self;
using Nancy.TinyIoc;
namespace processor
{
class ServiceApp
{
private readonly NancyHost _server;
private static ProcessorKernel _kernel;
public static ProcessorKernel Kernel { get { return _kernel;}}
public ServiceApp()
{
_server = new NancyHost(new Uri(Settings.NancyUrl));
}
public void Start()
{
Console.WriteLine("Processor starting.");
_kernel = TinyIoCContainer.Current.Resolve<ProcessorKernel>();
try
{
_server.Start();
}
catch (Exception)
{
throw new ApplicationException("ERROR: Nancy Self hosting was unable to start, Aborting.");
}
_kernel.RegisterProcessor();
Console.WriteLine("Processor has started.");
}
public void Stop()
{
Console.WriteLine("Processor stopping.");
_kernel.DeRegisterProcessor();
_server.Dispose();
Console.WriteLine("Processor has stopped.");
}
}
}
As you can see this allowed me to get a reference to my singleton in the service app, using TinyIOC, that allowed me to keep the reference to call Reg/DeReg on in the Start/Stop service routines.
The Public Static Kernel property was then added so that my Nancy Modules could call ServiceApp.Kernel... to get the status info they needed in response to queries on the Nancy endpoints.
As I say however, this change has now been undone in favor of going back to the IDisposable way of doing things.
Thanks to all those who took the time to reply.
There is a standard way of dealing with "cleanup" in .NET - it's called IDisposable
, and Nancy (along with its core components, such as the IoC container) uses this extensively.
As long as your objects correctly implements IDisposable
and the Dispose
method is called (preferably through a using
block) at the top level, your objects should be disposed at the right time.
Instead of calling Stop
on the NancyHost
, you should call Dispose
. This'll result in the following:
NancyHost
(like you already do) TinyIoCContainer
) IDisposable
In order for the disposal to go all the way to your ProcessorKernel
, you have to make sure it implements IDisposable
and move the code from your finalizer into the Dispose
method.
PS! Unless you're dealing with unmanaged resources, you should never implement a finalizer. Here are some reasons. I'm sure you'll find more if you google it.
链接地址: http://www.djcxy.com/p/27480.html