before using django tasks
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
#
|
||||||
|
# Last Change: 2025-10-25
|
||||||
|
#
|
||||||
|
|
||||||
|
import threading
|
||||||
|
|
||||||
|
class Singleton:
|
||||||
|
"""
|
||||||
|
A thread lock protected singleton,
|
||||||
|
makes any inherting class into a singlton.
|
||||||
|
|
||||||
|
The SINGLETON_LOCK object is shared between all inherited children,
|
||||||
|
unless they initiate their own using the 'decouple_lock_from_parent_class' method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
SINGLETON_INSTANCE = None
|
||||||
|
SINGLETON_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def decouple_lock_from_parent_class( cls ):
|
||||||
|
if cls.SINGLETON_LOCK is not super().SINGLETON_LOCK: return
|
||||||
|
cls.SINGLETON_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
def __new__( cls, *args, **kwargs ):
|
||||||
|
if cls.SINGLETON_INSTANCE: return cls.SINGLETON_INSTANCE
|
||||||
|
|
||||||
|
with cls.SINGLETON_LOCK:
|
||||||
|
if cls.SINGLETON_INSTANCE: return cls.SINGLETON_INSTANCE
|
||||||
|
cls.SINGLETON_INSTANCE = super().__new__( cls, *args, **kwargs )
|
||||||
|
|
||||||
|
return cls.SINGLETON_INSTANCE
|
||||||
+32
-49
@@ -17,8 +17,9 @@ import random
|
|||||||
|
|
||||||
from .utils import retry_on_db_error, ignore_on_db_error
|
from .utils import retry_on_db_error, ignore_on_db_error
|
||||||
from .asynctools import AsyncOriented
|
from .asynctools import AsyncOriented
|
||||||
|
from .singleton import Singleton
|
||||||
|
|
||||||
class AsyncronWorker( AsyncOriented ):
|
class AsyncronWorker( Singleton, AsyncOriented ):
|
||||||
"""
|
"""
|
||||||
The asyncron worker for a process, limited to one per proccess.
|
The asyncron worker for a process, limited to one per proccess.
|
||||||
Other threads on the same process, can offload their work to this thread and it's single async event loop.
|
Other threads on the same process, can offload their work to this thread and it's single async event loop.
|
||||||
@@ -30,10 +31,6 @@ class AsyncronWorker( AsyncOriented ):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
IS_ACTIVE = False #Whether this proccess needs an AsyncronWorker at all, this prevents running the worker on all the things that cause asyncron.app.ready to run!
|
IS_ACTIVE = False #Whether this proccess needs an AsyncronWorker at all, this prevents running the worker on all the things that cause asyncron.app.ready to run!
|
||||||
INSTANCE = None #Singelton
|
|
||||||
INIT_CALLBACKS = [] #Once the singleton is created, it'll run through the callbacks with itself as the first arg.
|
|
||||||
|
|
||||||
THREAD_LOCK = threading.Lock() #for initial singleton creation.
|
|
||||||
|
|
||||||
EXIT_SIGNALS = [
|
EXIT_SIGNALS = [
|
||||||
signal.SIGABRT,
|
signal.SIGABRT,
|
||||||
@@ -47,28 +44,35 @@ class AsyncronWorker( AsyncOriented ):
|
|||||||
LOG_FMT = r"%(asctime)s [%(process)d.Asyncron] [%(levelname)s] %(message)s", r"[%Y-%m-%d %H:%M:%S %z]"
|
LOG_FMT = r"%(asctime)s [%(process)d.Asyncron] [%(levelname)s] %(message)s", r"[%Y-%m-%d %H:%M:%S %z]"
|
||||||
LOG_LEVEL = logging.DEBUG
|
LOG_LEVEL = logging.DEBUG
|
||||||
|
|
||||||
|
INIT_CALLBACKS = [] #Once the singleton is created, it'll run through the callbacks with itself as the first arg.
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def register_init_callback( cls, callback ):
|
def register_init_callback( cls, callback ):
|
||||||
assert not cls.INSTANCE, "Cannot register new callbacks after the worker is created!"
|
assert not cls.SINGLETON_INSTANCE, "Cannot register new callbacks after the worker is created!"
|
||||||
cls.INIT_CALLBACKS.append( callback )
|
cls.INIT_CALLBACKS.append( callback )
|
||||||
|
|
||||||
def __new__( cls, *args, **kwargs ):
|
def __init__( self ):
|
||||||
"""
|
self.log #Evaluating the log property while we have the creation lock
|
||||||
Thread safe singleton logic
|
|
||||||
"""
|
|
||||||
|
|
||||||
if cls.INSTANCE: return cls.INSTANCE
|
self.is_db_ready_event = asyncio.Event()
|
||||||
with cls.THREAD_LOCK:
|
self.is_stopping_event = asyncio.Event()
|
||||||
if cls.INSTANCE: return cls.INSTANCE
|
|
||||||
cls.INSTANCE = super().__new__( cls, *args, **kwargs )
|
|
||||||
for callback in cls.INIT_CALLBACKS: callback( cls.INSTANCE )
|
|
||||||
cls.INSTANCE.log #Evaluating the log property while we have the lock
|
|
||||||
cls.INSTANCE.register_with_exit_signals()
|
|
||||||
|
|
||||||
django_signals.connection_created.connect( cls.INSTANCE.handle_new_db_connection )
|
#Just so that the asyncron.apps.ready doens't trigger the django warning
|
||||||
cls.INSTANCE.log.debug("Worker created for this process.")
|
self.start_after_db_ready = False
|
||||||
return cls.INSTANCE
|
|
||||||
|
|
||||||
|
#Parallelism
|
||||||
|
self.thread = None #Once the worker starts, it'll be populated
|
||||||
|
|
||||||
|
self.clearing_dead_workers = False
|
||||||
|
self.watching_models = collections.defaultdict( set ) # Model -> Set of key name of the tasks
|
||||||
|
|
||||||
|
self.database_unreachable = False
|
||||||
|
|
||||||
|
for callback in self.INIT_CALLBACKS: callback( self )
|
||||||
|
self.register_with_exit_signals()
|
||||||
|
|
||||||
|
django_signals.connection_created.connect( self.handle_new_db_connection )
|
||||||
|
self.log.debug("Worker created for this process.")
|
||||||
|
|
||||||
|
|
||||||
def register_with_exit_signals( self ):
|
def register_with_exit_signals( self ):
|
||||||
@@ -103,14 +107,14 @@ class AsyncronWorker( AsyncOriented ):
|
|||||||
self.stop(f"Signal {signal.strsignal(signum)}")
|
self.stop(f"Signal {signal.strsignal(signum)}")
|
||||||
|
|
||||||
def handle_new_db_connection( self, sender, **kwargs ):
|
def handle_new_db_connection( self, sender, **kwargs ):
|
||||||
if self.is_db_ready: return
|
if self.is_db_ready_event.is_set(): return
|
||||||
self.log.debug(f"First DB connection: {sender}")
|
self.log.debug(f"First DB connection: {sender}")
|
||||||
|
|
||||||
self.is_db_ready = True
|
self.is_db_ready_event.set()
|
||||||
django_signals.connection_created.disconnect( self.handle_new_db_connection )
|
django_signals.connection_created.disconnect( self.handle_new_db_connection )
|
||||||
|
|
||||||
if not self.loop: return #We're not inside an async runner in this(?) or another thread.
|
#if not self.loop: return #We're not inside an async runner in this(?) or another thread.
|
||||||
self.loop.call_soon_threadsafe( self.is_db_ready_event.set )
|
#self.loop.call_soon_threadsafe( self.is_db_ready_event.set )
|
||||||
|
|
||||||
|
|
||||||
def start( self, daemon = False ):
|
def start( self, daemon = False ):
|
||||||
@@ -127,13 +131,13 @@ class AsyncronWorker( AsyncOriented ):
|
|||||||
|
|
||||||
|
|
||||||
def stop( self, reason = None ):
|
def stop( self, reason = None ):
|
||||||
if self.is_stopping: return #TODO: Insisting on exiting faster should probably be managed in the signal handler
|
if self.is_stopping_event.is_set(): return #TODO: Insisting on exiting faster should probably be managed in the signal handler
|
||||||
|
|
||||||
self.log.info( f"Stopping Worker: {reason}" )
|
self.log.info( f"Stopping Worker: {reason}" )
|
||||||
self.is_stopping = True
|
self.is_stopping_event.set()
|
||||||
|
|
||||||
if not self.loop: return
|
#if not self.loop: return
|
||||||
self.loop.call_soon_threadsafe( self.is_stopping_event.set )
|
#self.loop.call_soon_threadsafe( self.is_stopping_event.set )
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -143,30 +147,9 @@ class AsyncronWorker( AsyncOriented ):
|
|||||||
##
|
##
|
||||||
|
|
||||||
|
|
||||||
def __init__( self ):
|
|
||||||
|
|
||||||
#These booleans have asyncio.Event counterparts in the loop context
|
|
||||||
self.is_db_ready = False
|
|
||||||
self.is_stopping = False
|
|
||||||
|
|
||||||
#Just so that the asyncron.apps.ready doens't trigger the django warning
|
|
||||||
self.start_after_db_ready = False
|
|
||||||
|
|
||||||
#Parallelism
|
|
||||||
self.thread = None #Once the worker starts, it'll be populated
|
|
||||||
|
|
||||||
self.clearing_dead_workers = False
|
|
||||||
self.watching_models = collections.defaultdict( set ) # Model -> Set of key name of the tasks
|
|
||||||
|
|
||||||
self.database_unreachable = False
|
|
||||||
|
|
||||||
async def startup( self ):
|
async def startup( self ):
|
||||||
await super().startup()
|
await super().startup()
|
||||||
|
|
||||||
#These asyncio.Events have booleans counterparts in the __init__ section for main thread non async logic
|
|
||||||
self.is_db_ready_event = asyncio.Event()
|
|
||||||
self.is_stopping_event = asyncio.Event()
|
|
||||||
|
|
||||||
self.task_reason_jobs_queue = asyncio.Queue() #Run tasks from other threads, safely
|
self.task_reason_jobs_queue = asyncio.Queue() #Run tasks from other threads, safely
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user