2022-11-08 22:06:29 -08:00
|
|
|
import asyncio
|
|
|
|
import datetime
|
|
|
|
import time
|
2022-11-09 22:48:31 -08:00
|
|
|
import traceback
|
2022-11-08 22:06:29 -08:00
|
|
|
import uuid
|
2022-11-19 09:20:13 -08:00
|
|
|
from typing import List, Optional, Type
|
2022-11-08 22:06:29 -08:00
|
|
|
|
2022-11-20 11:32:49 -08:00
|
|
|
from asgiref.sync import sync_to_async
|
2022-11-20 11:24:03 -08:00
|
|
|
from django.conf import settings
|
2022-11-08 22:06:29 -08:00
|
|
|
from django.utils import timezone
|
|
|
|
|
2022-11-09 21:29:33 -08:00
|
|
|
from stator.models import StatorModel
|
2022-11-08 22:06:29 -08:00
|
|
|
|
|
|
|
|
|
|
|
class StatorRunner:
|
|
|
|
"""
|
|
|
|
Runs tasks on models that are looking for state changes.
|
2022-11-19 09:20:13 -08:00
|
|
|
Designed to run for a determinate amount of time, and then exit.
|
2022-11-08 22:06:29 -08:00
|
|
|
"""
|
|
|
|
|
2022-11-13 17:42:47 -08:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
models: List[Type[StatorModel]],
|
2022-11-18 16:24:43 -08:00
|
|
|
concurrency: int = 50,
|
|
|
|
concurrency_per_model: int = 10,
|
2022-11-19 09:20:13 -08:00
|
|
|
liveness_file: Optional[str] = None,
|
|
|
|
schedule_interval: int = 30,
|
|
|
|
lock_expiry: int = 300,
|
2022-11-13 17:42:47 -08:00
|
|
|
):
|
2022-11-08 22:06:29 -08:00
|
|
|
self.models = models
|
|
|
|
self.runner_id = uuid.uuid4().hex
|
2022-11-13 17:42:47 -08:00
|
|
|
self.concurrency = concurrency
|
|
|
|
self.concurrency_per_model = concurrency_per_model
|
2022-11-19 09:20:13 -08:00
|
|
|
self.liveness_file = liveness_file
|
|
|
|
self.schedule_interval = schedule_interval
|
|
|
|
self.lock_expiry = lock_expiry
|
2022-11-08 22:06:29 -08:00
|
|
|
|
|
|
|
async def run(self):
|
|
|
|
self.handled = 0
|
2022-11-19 09:20:13 -08:00
|
|
|
self.last_clean = time.monotonic() - self.schedule_interval
|
2022-11-08 22:06:29 -08:00
|
|
|
self.tasks = []
|
|
|
|
# For the first time period, launch tasks
|
2022-11-09 21:29:33 -08:00
|
|
|
print("Running main task loop")
|
2022-11-19 09:20:13 -08:00
|
|
|
try:
|
|
|
|
while True:
|
|
|
|
# Do we need to do cleaning?
|
|
|
|
if (time.monotonic() - self.last_clean) >= self.schedule_interval:
|
|
|
|
print(f"{self.handled} tasks processed so far")
|
|
|
|
print("Running cleaning and scheduling")
|
|
|
|
for model in self.models:
|
|
|
|
asyncio.create_task(model.atransition_clean_locks())
|
|
|
|
asyncio.create_task(model.atransition_schedule_due())
|
|
|
|
self.last_clean = time.monotonic()
|
|
|
|
# Calculate space left for tasks
|
2022-11-20 11:32:49 -08:00
|
|
|
self.remove_completed_tasks()
|
2022-11-19 09:20:13 -08:00
|
|
|
space_remaining = self.concurrency - len(self.tasks)
|
|
|
|
# Fetch new tasks
|
|
|
|
for model in self.models:
|
|
|
|
if space_remaining > 0:
|
|
|
|
for instance in await model.atransition_get_with_lock(
|
|
|
|
number=min(space_remaining, self.concurrency_per_model),
|
|
|
|
lock_expiry=(
|
|
|
|
timezone.now()
|
|
|
|
+ datetime.timedelta(seconds=self.lock_expiry)
|
|
|
|
),
|
|
|
|
):
|
|
|
|
self.tasks.append(
|
|
|
|
asyncio.create_task(self.run_transition(instance))
|
|
|
|
)
|
|
|
|
self.handled += 1
|
|
|
|
space_remaining -= 1
|
|
|
|
# Prevent busylooping
|
|
|
|
await asyncio.sleep(0.1)
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
# Wait for tasks to finish
|
|
|
|
print("Waiting for tasks to complete")
|
|
|
|
while True:
|
|
|
|
self.remove_completed_tasks()
|
|
|
|
if not self.tasks:
|
|
|
|
break
|
|
|
|
# Prevent busylooping
|
|
|
|
await asyncio.sleep(1)
|
2022-11-09 21:29:33 -08:00
|
|
|
print("Complete")
|
2022-11-08 22:06:29 -08:00
|
|
|
return self.handled
|
|
|
|
|
2022-11-09 22:48:31 -08:00
|
|
|
async def run_transition(self, instance: StatorModel):
|
|
|
|
"""
|
|
|
|
Wrapper for atransition_attempt with fallback error handling
|
|
|
|
"""
|
|
|
|
try:
|
2022-11-10 22:42:43 -08:00
|
|
|
print(
|
|
|
|
f"Attempting transition on {instance._meta.label_lower}#{instance.pk} from state {instance.state}"
|
|
|
|
)
|
2022-11-09 22:48:31 -08:00
|
|
|
await instance.atransition_attempt()
|
2022-11-20 11:24:03 -08:00
|
|
|
except BaseException as e:
|
|
|
|
if settings.SENTRY_ENABLED:
|
|
|
|
from sentry_sdk import capture_exception
|
|
|
|
|
2022-11-20 11:32:49 -08:00
|
|
|
await sync_to_async(capture_exception, thread_sensitive=False)(e)
|
2022-11-09 22:48:31 -08:00
|
|
|
traceback.print_exc()
|
|
|
|
|
2022-11-08 22:06:29 -08:00
|
|
|
def remove_completed_tasks(self):
|
2022-11-09 22:48:31 -08:00
|
|
|
"""
|
|
|
|
Removes all completed asyncio.Tasks from our local in-progress list
|
|
|
|
"""
|
2022-11-08 22:06:29 -08:00
|
|
|
self.tasks = [t for t in self.tasks if not t.done()]
|