在NancyFX自托管单例中调用关机任务

sha*_*wty 7 c# architecture web-services topshelf nancy

我正在尝试找到一种方法来调用一个任务,该任务需要在关闭在NancyFX自托管应用程序内的TinyIOC中创建的单个对象时调用.

到目前为止,我一直无法得出答案,而且我也愿意接受更好的实现我即将描述的场景的想法.

概观

我有一个基于PHP的Web应用程序,我正在研究,因为这是PHP,没有旋转的线程/服务器来监听和处理长时间运行的任务,php代码存在请求它的浏览器请求的生命周期.

有问题的应用程序需要向Web服务发出一些可能需要时间才能完成的请求,因此我想出了使用Topshelf,NancyFX和Stackexchange.Redis在C#中实现一些后端服务的想法. .

该服务是标准的NancyFX自主控制台应用程序,如下所示:

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());
        });
      });
    }

  }
}
Run Code Online (Sandbox Code Playgroud)

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.");
    }

  }
}
Run Code Online (Sandbox Code Playgroud)

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
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

除了这三个类,我还有一些标准的NancyFX路由模块,用于处理各种端点等.

所有这一切都很好,正如你从ProcessorKernel中看到的那样我调用一个名为RegisterProcessor的方法这个方法,联系负责管理这个处理器实例的web应用程序并注册自己,基本上是对网络应用程序说,嘿我是在这里,并准备好通过redis队列向我发送请求.

但是,使用Nancy Bootstrapper 将此ProcessorKernel实例化为单例:

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();
    }

  }
}
Run Code Online (Sandbox Code Playgroud)

它必须是单例的原因是因为它必须接受来自redis的排队请求,然后处理它们并将它们发送到不同位置的各种Web服务.

有几个不同的通道,但一个处理器一次只能监听一个通道.这最终意味着当此ProcessorKernel关闭时,它必须调用PHP Web应用程序以"取消注册"本身,并通知应用程序没有任何服务该通道.

它找到了一个很好的挂钩点,称这种取消注册导致了我的问题.

试过的方法

正如你从上面的代码中看到的那样,我已经尝试过使用类析构函数,虽然不是世界上最好的解决方案,但它确实被调用但是类在返回之前被拆除了,因此实际的取消注册永远不会完成,因此永远不会正确注销.

我也试过让单例类成为IDisposable,看看是否将de-register代码放在"Destroy"中,但是根本没有被调用.

因为我正在使用Topshelf并且我在ServiceApp中有Start/Stop方法我知道我可以在那里调用我的例程,但是此时我无法访问单例ProcessorKernel,因为它不是Nancy模块,所以TinyIOC没有解决依赖关系.

我最初还尝试在Program.cs中创建ProcessorKernel并通过ServiceApp构造函数传递它,虽然这创建了一个服务,但它没有注册为单例,并且在Nancy Route模块中无法访问,所以状态端点无法查询创建的实例以进行状态更新,因为它们只获得了TinyIOC创建的单例.

如果我可以访问Start/Stop中的单例,那么我可以轻松地在其上放置几个公共方法来调用寄存器/注销函数.

为了完整起见,我用来将帖子请求发送到PHP Web应用程序的代码如下:

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;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

摘要

所有我需要的是一种可靠的方式来调用取消注册代码,并确保服务通知php-app它正在消失,因为php-app的性质我无法使用redis将消息发回给它因为它只是不听,该应用程序是在Apache Web服务器下运行的标准线性php应用程序.

只要只有一个实际的ProcessorKernel实例运行,我就可以更好地了解如何构建这个方法.一旦注册,它必须处理发送到其通道的每个请求,并且由于处理的请求可能被访问多次并由几个不同的Web服务处理,因此这些请求需要保存在内存中,直到它们的处理完全完成.

提前感谢您对此的任何想法.

更新 - 第二天:-)

所以在看到下面的Kristian的答案,看到他的/ GrumpyDev和Phillip Haydon在我的Twitter流上的回复后,我将我的Singleton类改回了IDisposable,摆脱了~Finalizer,然后更改了ServiceApp.cs,以便它调用Host.Dispose ()而不是Host.Stop()

然后在我的单例类中正确调用了Dispose例程,这允许我正确地关闭事物.

我也提出了另一种解决方法,我不得不承认

  • a)很难看
  • b)现已被删除

然而,本着分享的精神(并有尴尬的风险),我将在这里详细说明注意:我实际上并不建议这样做

首先我调整了ServiceApp.cs,现在它看起来像:

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.");
    }

  }
}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,这允许我使用TinyIOC在服务应用程序中获取对我的单例的引用,这允许我保留在启动/停止服务例程中调用Reg/DeReg的引用.

公共静态内核然后属性添加,使我的南希模块可以调用ServiceApp.Kernel ......以获得他们需要在响应查询的南希端点的状态信息.

然而,正如我所说的那样,这种变化现在已经被取消,有利于回到IDisposable的做事方式.

感谢所有花时间回复的人.

khe*_*ang 3

.NET 中有一种处理“清理”的标准方法 - 它称为IDisposable,Nancy(及其核心组件,例如 IoC 容器)广泛使用这种方法。

只要您的对象正确实现IDisposable并且在顶层Dispose调用该方法(最好通过块),您的对象就应该在正确的时间被处置。using

您应该致电 ,Stop而不是致电。这将导致以下结果:NancyHostDispose

  • 停止NancyHost(就像你已经做的那样)
    • 处置引导程序
      • 处置应用程序容器(在本例中为 a TinyIoCContainer
        • 处置所有单例实例实现IDisposable

为了使处置一直到达您的ProcessorKernel,您必须确保它实现IDisposable并将代码从您的终结器移动到Dispose方法中。

附言!除非您正在处理非托管资源,否则永远不应该实现终结器。以下是一些原因。我相信如果你用谷歌搜索你会发现更多。