'How to stop tasks scheduled by APScheduler properly when the trigger type is 'date'?
I have made a GUI by PyQT5. It provides two QPushButton widgets to control the start and termination of the worker thread. When the "start" QPushButton widget on GUI is clicked, the worker thread should run tasks scheduled in the scheduler with the trigger type of 'date'(APScheduler module version: 3.9.1). And the standard output of the worker thread will be redirected to a QPlainTextEdit widget on the GUI. This part of code can work as I expect.
But there is also a "stop" QPushButton widget. Once it is clicked, the worker thread should stop running tasks in the scheduler and the QPlainTextEdit widget on GUI should show a text message "***** Interrupted By User *****".
However, I found that the text message for interruption can only be shown correctly in the first time the worker thread is executed. Afterwards, if I click the "start" QPushButton widget to restart the worker thread and click the "stop" QPushButton to interrupt it later, the QPlainTextEdit widget will show the text message for interruption twice.
It seems that the issue will not occur after I change the trigger type to 'interval'. But this will make the program not meet the requirement of my application.
Does any one know why it happens and how I should fix this issue?
Below is the code in my module called "call_main_window.py". In this module, the class "MyMainWindow" implements the main thread and the class "Worker" implements the worker thread.
from PyQt5 import QtCore, QtWidgets, QtGui
from main_window import Ui_Form
import sys
import datetime
import tzlocal
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.schedulers import SchedulerNotRunningError
class Stream(QtCore.QObject):
newText = QtCore.pyqtSignal(str)
def write(self, data: str):
self.newText.emit(data)
class CustomSignal(QtCore.QObject):
finished = QtCore.pyqtSignal()
interrupted = QtCore.pyqtSignal()
class Worker(QtCore.QRunnable):
def __init__(self):
super().__init__()
self.customSignal = CustomSignal()
def start(self, timeToRunTaskAndDataPassed: tuple):
job_defaults = {'coalesce': False, 'misfire_grace_time': None, 'daemonic': True}
self.scheduler = BackgroundScheduler(timezone = str(tzlocal.get_localzone()),
daemon = True, job_defaults = job_defaults)
startTime = datetime.datetime.now()
for timeInSecs, data in timeToRunTaskAndDataPassed:
time = startTime + datetime.timedelta(seconds = timeInSecs)
self.scheduler.add_job(self.do_task, 'date', args = [data], run_date = time)
timeToEmitFishSignal = startTime + datetime.timedelta(seconds = timeToRunTaskAndDataPassed[-1][0] + 1)
self.scheduler.add_job(self.customSignal.finished.emit, 'date', run_date = timeToEmitFishSignal)
self.scheduler.start()
def stop_by_user(self):
try:
self.scheduler.shutdown(wait = False)
except SchedulerNotRunningError:
pass
finally:
sys.stdout.write(f'***** Interrupted By User *****\n')
def do_task(self, data: str):
timeStamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
sys.stdout.write(f'{timeStamp}: {data}\n')
class MyMainWindow(QtWidgets.QWidget, Ui_Form):
def __init__(self, parent = None):
super(MyMainWindow, self).__init__(parent)
self.setupUi(self)
self.customSignal = CustomSignal()
sys.stdout = Stream(newText = self.update_text)
self.pushButtonStart.clicked.connect(self.start_working)
self.pushButtonStart.clicked.connect(self.switch_the_status_of_push_btns_on_ui)
self.pushButtonStart.clicked.connect(self.plainTextEdit.clear)
self.pushButtonStop.clicked.connect(self.interrupt_task)
self.pushButtonStop.clicked.connect(self.switch_the_status_of_push_btns_on_ui)
self.destroyed.connect(sys.exit)
def start_working(self):
self.threadPool = QtCore.QThreadPool()
timeToRunTaskAndTaskID = ((1, 'A'), (10, 'B'),
(20, 'C'), (35, 'D'),
(55, 'E'), (60, 'F'))
worker = Worker()
self.customSignal.interrupted.connect(worker.stop_by_user)
worker.customSignal.finished.connect(self.switch_the_status_of_push_btns_on_ui)
self.threadPool.start(lambda: worker.start(timeToRunTaskAndTaskID))
def interrupt_task(self):
self.customSignal.interrupted.emit()
def update_the_status(self, lastStatus: str):
sys.stdout.write(lastStatus)
def switch_the_status_of_push_btns_on_ui(self):
newStatusOfStartBtn = not self.pushButtonStart.isEnabled()
newStatusOfStopBtn = not self.pushButtonStop.isEnabled()
self.pushButtonStart.setEnabled(newStatusOfStartBtn)
self.pushButtonStop.setEnabled(newStatusOfStopBtn)
def update_text(self, data: str):
cursor = self.plainTextEdit.textCursor()
cursor.movePosition(QtGui.QTextCursor.End)
cursor.insertText(data)
self.plainTextEdit.setTextCursor(cursor)
self.plainTextEdit.ensureCursorVisible()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = MyMainWindow()
window.show()
sys.exit(app.exec_())
And below is the code for the GUI in the module "main_window.py":
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'test_a.ui'
#
# Created by: PyQt5 UI code generator 5.15.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName("Form")
Form.resize(600, 300)
self.gridLayout = QtWidgets.QGridLayout(Form)
self.gridLayout.setObjectName("gridLayout")
self.plainTextEdit = QtWidgets.QPlainTextEdit(Form)
self.plainTextEdit.setObjectName("plainTextEdit")
self.gridLayout.addWidget(self.plainTextEdit, 0, 0, 1, 2)
spacerItem = QtWidgets.QSpacerItem(500, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.pushButtonStart = QtWidgets.QPushButton(Form)
self.pushButtonStart.setObjectName("pushButtonStart")
self.gridLayout.addWidget(self.pushButtonStart, 1, 0, 1, 1)
self.pushButtonStop = QtWidgets.QPushButton(Form)
self.pushButtonStop.setEnabled(False)
self.pushButtonStop.setObjectName("pushButtonStop")
self.gridLayout.addWidget(self.pushButtonStop, 1, 1, 1, 1)
self.retranslateUi(Form)
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
self.pushButtonStart.setText(_translate("Form", "Start"))
self.pushButtonStop.setText(_translate("Form", "Stop"))
Solution 1:[1]
I found a workaround by myself. After some trial and error, I found that the problem only happened when the the "stop_by_user" method of the "Worker" class was called via a pyqtSignal. And it would not show up when the "stop_by_user" method was directly called in the "interrupt_task" method of the "MyMainWindow" class.
Below is the code of my main program revised for this workaround:
from PyQt5 import QtCore, QtWidgets, QtGui
from main_window import Ui_Form
import sys
import datetime
import tzlocal
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.schedulers import SchedulerNotRunningError
class Stream(QtCore.QObject):
newText = QtCore.pyqtSignal(str)
def write(self, data: str):
self.newText.emit(data)
class CustomSignal(QtCore.QObject):
finished = QtCore.pyqtSignal()
interrupted = QtCore.pyqtSignal()
class Worker(QtCore.QRunnable):
def __init__(self):
super().__init__()
self.customSignal = CustomSignal()
def start(self, timeToRunTaskAndDataPassed: tuple):
job_defaults = {'coalesce': False, 'misfire_grace_time': None, 'daemonic': True}
self.scheduler = BackgroundScheduler(timezone = str(tzlocal.get_localzone()),
daemon = True, job_defaults = job_defaults)
startTime = datetime.datetime.now()
for timeInSecs, data in timeToRunTaskAndDataPassed:
time = startTime + datetime.timedelta(seconds = timeInSecs)
self.scheduler.add_job(self.do_task, 'date', args = [data], run_date = time)
timeToEmitFishSignal = startTime + datetime.timedelta(seconds = timeToRunTaskAndDataPassed[-1][0] + 1)
self.scheduler.add_job(self.customSignal.finished.emit, 'date', run_date = timeToEmitFishSignal)
self.scheduler.start()
def stop_by_user(self):
try:
self.scheduler.shutdown(wait = False)
except SchedulerNotRunningError:
pass
finally:
sys.stdout.write(f'***** Interrupted By User *****\n')
def do_task(self, data: str):
timeStamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
sys.stdout.write(f'{timeStamp}: {data}\n')
class MyMainWindow(QtWidgets.QWidget, Ui_Form):
def __init__(self, parent = None):
super(MyMainWindow, self).__init__(parent)
self.setupUi(self)
self.customSignal = CustomSignal()
sys.stdout = Stream(newText = self.update_text)
self.pushButtonStart.clicked.connect(self.start_working)
self.pushButtonStart.clicked.connect(self.switch_the_status_of_push_btns_on_ui)
self.pushButtonStart.clicked.connect(self.plainTextEdit.clear)
self.pushButtonStop.clicked.connect(self.interrupt_task)
self.pushButtonStop.clicked.connect(self.switch_the_status_of_push_btns_on_ui)
self.destroyed.connect(sys.exit)
def start_working(self):
self.threadPool = QtCore.QThreadPool()
timeToRunTaskAndTaskID = ((1, 'A'), (10, 'B'),
(20, 'C'), (35, 'D'),
(55, 'E'), (60, 'F'))
self.worker = Worker()
self.worker.customSignal.finished.connect(self.switch_the_status_of_push_btns_on_ui)
self.threadPool.start(lambda: self.worker.start(timeToRunTaskAndTaskID))
def interrupt_task(self):
self.worker.stop_by_user()
def update_the_status(self, lastStatus: str):
sys.stdout.write(lastStatus)
def switch_the_status_of_push_btns_on_ui(self):
newStatusOfStartBtn = not self.pushButtonStart.isEnabled()
newStatusOfStopBtn = not self.pushButtonStop.isEnabled()
self.pushButtonStart.setEnabled(newStatusOfStartBtn)
self.pushButtonStop.setEnabled(newStatusOfStopBtn)
def update_text(self, data: str):
cursor = self.plainTextEdit.textCursor()
cursor.movePosition(QtGui.QTextCursor.End)
cursor.insertText(data)
self.plainTextEdit.setTextCursor(cursor)
self.plainTextEdit.ensureCursorVisible()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = MyMainWindow()
window.show()
sys.exit(app.exec_())
Still, I would like to know why the original problem happened or if there is any issues in my code so that I can avoid the same mistake in the future. If anyone has any idea to share, I will be grateful.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | thomas_chang |