pika消费者程序使用Python注册Windows为服务无法停止的问题

运行环境

  • Python 3.7.7
  • pika 0.10.0

问题描述

使用 pika 库,连接 rabbitmq,对队列进行监听,并处理监听到的消息。希望将程序注册为windows服务,后台运行,开机自启动。

在查阅有关资料以及实施过程中主要遇到几个问题:

  1. 编写完以后服务没有及时响应启动或控制请求
  2. pika库的stop_consuming方法没有响应,无法停止服务
  3. 程序编写有问题时,服务启动不起来,需要查看windows事件日志

基本实现

使用第三方库pywin32
这个库有一个基础类 win32serviceutil.ServiceFramework,只需要继承该类,实现自己的方法即可,一个基础的服务模板如下面所示:

#encoding=utf-8
#ZPF
import win32serviceutil 
import win32service 
import win32event 
 
class PythonService(win32serviceutil.ServiceFramework): 
    #服务名
    _svc_name_ = "PythonService"
    #服务在windows系统中显示的名称
    _svc_display_name_ = "Python Service Test"
    #服务的描述
    _svc_description_ = "This code is a Python service Test"
 
    def __init__(self, args): 
        win32serviceutil.ServiceFramework.__init__(self, args) 
        self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
 
    def SvcDoRun(self):
        # 把自己的代码放到这里,就OK
        # 等待服务被停止 
        win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE) 
            
    def SvcStop(self): 
        # 先告诉SCM停止这个过程 
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) 
        # 设置事件 
        win32event.SetEvent(self.hWaitStop) 
 
if __name__=='__main__': 
    win32serviceutil.HandleCommandLine(PythonService)  
    #括号里参数可以改成其他名字,但是必须与class类名一致;

需要在 SvcDoRun 中实现自己需要的服务,一般说来,需要注册为服务的任务都是堵塞型的,所以这个函数应该是有一个循环体不停在运行的,直到停止服务的时候,通过 SvcStop 传入一个状态位来让 SvcDoRun 结束运行。

对于我的场景来说,我需要在 SvcDoRunchannel.start_consuming() ,而在 SvcDoRunchannel.stop_consuming()

注册、启动、结束服务

PythonService.py 所在的目录CMD(管理员)中运行以下指令,可以实现服务的注册、启动与停止等。

1.安装服务
 
python PythonService.py install
 
2.让服务自动启动
 
python PythonService.py --startup auto install 
 
3.启动服务
 
python PythonService.py start
 
4.重启服务
 
python PythonService.py restart
 
5.停止服务
 
python PythonService.py stop
 
6.删除/卸载服务
 
python PythonService.py remove

第一个问题:服务没有及时响应启动或控制请求

这主要是因为系统环境变量没设置好,需要在系统变量的path中添加如下四个路径,注意对应你的python安装路径。一般来说,python环境会配置前两个,后两个路径是我们安装完 pywin32 才有的,在注册启动服务时需要用到。

C:\Program Files\Python37\Scripts
C:\Program Files\Python37
C:\Program Files\Python37\Lib\site-packages\pywin32_system32
C:\Program Files\Python37\Lib\site-packages\win32

第二个问题:启动后报错,但不知道哪里出错

通过参考内容3(在win10中windows事件日志如何查看)的方式可以看到你注册的服务的报错信息,如果是由于代码编写有误,这里也能看出端倪

第三个问题:服务无法正确停止:无法完成操作。服务无法在此时接受控制信息

发生这个问题最可能的原因是, SvcDoRun 中的循环体没有退出,程序无法结束。
对应到我的程序来讲,在 SvcDoRunchannel.start_consuming() 开启消费程序后,而在 SvcDoRun 中 使用 channel.stop_consuming() 无法结束。主要原因还是 start_consuming() 的实现造成的,通过查看这个方法的源码:

    def start_consuming(self):
        """Processes I/O events and dispatches timers and `basic_consume`
        callbacks until all consumers are cancelled.

        NOTE: this blocking function may not be called from the scope of a
        pika callback, because dispatching `basic_consume` callbacks from this
        context would constitute recursion.

        :raises pika.exceptions.RecursionError: if called from the scope of a
            `BlockingConnection` or `BlockingChannel` callback

        """
        # Check if called from the scope of an event dispatch callback
        with self.connection._acquire_event_dispatch() as dispatch_allowed:
            if not dispatch_allowed:
                raise exceptions.RecursionError(
                    'start_consuming may not be called from the scope of '
                    'another BlockingConnection or BlockingChannel callback')

        # Process events as long as consumers exist on this channel
        while self._consumer_infos:
            print("t")
            self.connection.process_data_events(time_limit=None)

消费消息通过 self.connection.process_data_events(time_limit=None) 来实现,设计的时候,while 的条件是 self._consumer_infos,而 channel.stop_consuming() 正是通过清空 self._consumer_infos 来使消费行为停止,问题出在 (time_limit=None) 上,当 (time_limit=None) 时,process_data_events 方法本身是堵塞的,一直要等到有数据来才会循环执行一次,如果没有消息来,就一直等待,造成堵塞,永远不会退出。

解决的办法就是在调用 start_consuming 的地方,直接调用 process_data_events 并给time_limit 来设置一个时间,让这层循环即使在没有数据到来的时候也跑起来而不是一直堵塞,就能响应到 stop_consuming

值得注意的是:不同版本的pikastart_consuming中处理数据的函数名是不一样的,不一定对应了process_data_events,要查看具体的实现来进行对应的调用。

最终的实现

import win32serviceutil 
import win32service 
import win32event 
import pika
 
class PythonService(win32serviceutil.ServiceFramework): 
    #服务名
    _svc_name_ = "PythonService"
    #服务在windows系统中显示的名称
    _svc_display_name_ = "Python Service Test"
    #服务的描述
    _svc_description_ = "This code is a Python service Test"
 
    def __init__(self, args): 
        win32serviceutil.ServiceFramework.__init__(self, args) 
        self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
 
    def SvcDoRun(self):
        # 连接rabbitmq
        # 创建通道的时候要将值存储到类成员变量 self.channel,以便 SvcStop 中还等访问到这个通道
        # 绑定消费队列
        while self.channel._consumer_infos:
            # 开始消费
            self.channel.connection.process_data_events(time_limit=1)
        win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE) 
            
    def SvcStop(self): 
        # 先告诉SCM停止这个过程
        self.channel.stop_consuming()
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) 
        # 设置事件 
        win32event.SetEvent(self.hWaitStop) 
 
if __name__=='__main__': 
    win32serviceutil.HandleCommandLine(PythonService)  
    #括号里参数可以改成其他名字,但是必须与class类名一致;

参考内容

  1. Python写Windows Service服务程序
  2. Python 服务 及问题 服务没有及时响应启动或控制请求
  3. 在win10中windows事件日志如何查看
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值