how to instant kill an apscheduler.BlockingScheduler with ctrl+c?

194 Views Asked by At

BACKGROUND: PYTHON 3.8 APSCHEDULER 3.10.1

I am using BlockingScheduler for a few cron jobs and ideally I want the scheduler process can be easily stopped using ctrl+c, so I arrange my code as follow but so far it is not working properly as I expected.

THIS IS THE SCHEDULER BASE CLASS:

import traceback
from typing import Any, Callable
from apscheduler.schedulers.background import BlockingScheduler
from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR
from utils.tool.decorator import sched_job
from utils.tool.logger import log

logger = log(__file__, "utils", warning_only=False)


class APSchedulerBase:
    def __init__(self, scheduler: Any = None, add_listener: bool = True):
        if scheduler is None:
            self.sched = BlockingScheduler(timezone="Asia/Shanghai")
            if add_listener:
                self.sched.add_listener(self.ap_listener, EVENT_JOB_ERROR | EVENT_JOB_EXECUTED)
        else:
            self.sched = scheduler

    def ap_listener(self, event):
        if event.exception == KeyboardInterrupt or event.exception == SystemExit:
            self.on_kill()
        if event.exception:
            logger.error(str(traceback.format_exc()))
        else:
            pass

    def on_kill(self):
        logger.warning("Scheduler Exit.")
        self.sched.shutdown(wait=False)

    def register_task(
            self, func: Callable, trigger: str,
            **kwargs
    ):
        @sched_job(
            self.sched,
            logger,
            trigger=trigger,
            **kwargs
        )
        def wrap():
            func()
        wrap()

    def start(self):
        try:
            logger.warning("Scheduler Start.")
            self.sched.start()
        except (KeyboardInterrupt, SystemExit):
            self.on_kill()

THIS IS A SIMPLE DEMO: ctrl+c only works when f() is about to be called.

import time
def f():
   print(1)
   time.sleep(10)
   print(2)
s = APSchedulerBase()
s.register_task(f, trigger='cron', hour=12, minute=30)
s.start()

Now the whole scheduler process can only be killed when the task is just about to be executed, if you ctrl+c when the process is dormant/waiting for the next task, nothing will happen until the next task is triggered. So how exactly should I do to make ctrl+c instant kill?

1

There are 1 best solutions below

2
Christos On

You need to add in your demo code a couple of things. First we have to define a signal_handler function:

def signal_handler(signal_number, frame, scheduler):
    # here you can add logging
    scheduler.shutdown(wait=False)

Then import at the top of your file signal:

import signal

and then just after you create the scheduler:

signal.signal(signal.SIGINT, lambda signum, frame: signal_handler(signum, frame, s))

All in all,

import time
import signal 

def signal_handler(signal_number, frame, scheduler):
    # here you can add logging
    scheduler.shutdown(wait=False)

def f():
   print(1)
   time.sleep(10)
   print(2)

s = APSchedulerBase()
signal.signal(signal.SIGINT, lambda signum, frame: signal_handler(signum, frame, s.sched))
s.register_task(f, trigger='cron', hour=12, minute=30)

s.start()

Why this is needed ?

When we hit CTRL+C a SIGNINT is sent to the foreground process. Please check this. So which thread gets this signal, the thread in which scheduler is running or the thread in which the instance of the scheduler is created ? The answer is the latter. So since there isn't any handler for this, nothing happens.

On the other hand scheduler process can only be killed when the task is just about to be executed, because an exception is raised in the the thread the scheduler is running and there is an except statement that catches this and shutdown the scheduler

except (KeyboardInterrupt, SystemExit):
   self.on_kill()