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
|
|
|
|
from typing import List, Type
|
|
|
|
|
|
|
|
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.
|
|
|
|
Designed to run in a one-shot mode, living inside a request.
|
|
|
|
"""
|
|
|
|
|
|
|
|
START_TIMEOUT = 30
|
|
|
|
TOTAL_TIMEOUT = 60
|
|
|
|
LOCK_TIMEOUT = 120
|
|
|
|
|
|
|
|
MAX_TASKS = 30
|
2022-11-09 21:29:33 -08:00
|
|
|
MAX_TASKS_PER_MODEL = 5
|
2022-11-08 22:06:29 -08:00
|
|
|
|
|
|
|
def __init__(self, models: List[Type[StatorModel]]):
|
|
|
|
self.models = models
|
|
|
|
self.runner_id = uuid.uuid4().hex
|
|
|
|
|
|
|
|
async def run(self):
|
|
|
|
start_time = time.monotonic()
|
|
|
|
self.handled = 0
|
|
|
|
self.tasks = []
|
|
|
|
# Clean up old locks
|
2022-11-09 21:29:33 -08:00
|
|
|
print("Running initial cleaning and scheduling")
|
|
|
|
initial_tasks = []
|
|
|
|
for model in self.models:
|
|
|
|
initial_tasks.append(model.atransition_clean_locks())
|
|
|
|
initial_tasks.append(model.atransition_schedule_due())
|
|
|
|
await asyncio.gather(*initial_tasks)
|
2022-11-08 22:06:29 -08:00
|
|
|
# For the first time period, launch tasks
|
2022-11-09 21:29:33 -08:00
|
|
|
print("Running main task loop")
|
2022-11-08 22:06:29 -08:00
|
|
|
while (time.monotonic() - start_time) < self.START_TIMEOUT:
|
|
|
|
self.remove_completed_tasks()
|
|
|
|
space_remaining = self.MAX_TASKS - len(self.tasks)
|
|
|
|
# Fetch new tasks
|
2022-11-09 21:29:33 -08:00
|
|
|
for model in self.models:
|
|
|
|
if space_remaining > 0:
|
|
|
|
for instance in await model.atransition_get_with_lock(
|
|
|
|
min(space_remaining, self.MAX_TASKS_PER_MODEL),
|
|
|
|
timezone.now() + datetime.timedelta(seconds=self.LOCK_TIMEOUT),
|
|
|
|
):
|
|
|
|
print(
|
|
|
|
f"Attempting transition on {instance._meta.label_lower}#{instance.pk}"
|
|
|
|
)
|
|
|
|
self.tasks.append(
|
2022-11-09 22:48:31 -08:00
|
|
|
asyncio.create_task(self.run_transition(instance))
|
2022-11-09 21:29:33 -08:00
|
|
|
)
|
|
|
|
self.handled += 1
|
|
|
|
space_remaining -= 1
|
2022-11-08 22:06:29 -08:00
|
|
|
# Prevent busylooping
|
2022-11-09 21:29:33 -08:00
|
|
|
await asyncio.sleep(0.1)
|
2022-11-08 22:06:29 -08:00
|
|
|
# Then wait for tasks to finish
|
2022-11-09 21:29:33 -08:00
|
|
|
print("Waiting for tasks to complete")
|
2022-11-08 22:06:29 -08:00
|
|
|
while (time.monotonic() - start_time) < self.TOTAL_TIMEOUT:
|
|
|
|
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:
|
|
|
|
await instance.atransition_attempt()
|
|
|
|
except BaseException:
|
|
|
|
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()]
|