mirror of
https://github.com/ArchiveBox/ArchiveBox.git
synced 2026-01-03 17:35:45 +10:00
113 lines
3.6 KiB
Python
113 lines
3.6 KiB
Python
__package__ = 'archivebox.machine'
|
|
|
|
from datetime import timedelta
|
|
from django.utils import timezone
|
|
from django.db.models import F
|
|
|
|
from statemachine import State, StateMachine
|
|
|
|
from machine.models import Binary
|
|
|
|
|
|
class BinaryMachine(StateMachine, strict_states=True):
|
|
"""
|
|
State machine for managing Binary installation lifecycle.
|
|
|
|
Follows the unified pattern used by Crawl, Snapshot, and ArchiveResult:
|
|
- queued: Binary needs to be installed
|
|
- started: Installation hooks are running
|
|
- succeeded: Binary installed successfully (abspath, version, sha256 populated)
|
|
- failed: Installation failed permanently
|
|
"""
|
|
|
|
model: Binary
|
|
|
|
# States
|
|
queued = State(value=Binary.StatusChoices.QUEUED, initial=True)
|
|
started = State(value=Binary.StatusChoices.STARTED)
|
|
succeeded = State(value=Binary.StatusChoices.SUCCEEDED, final=True)
|
|
failed = State(value=Binary.StatusChoices.FAILED, final=True)
|
|
|
|
# Tick Event - transitions based on conditions
|
|
tick = (
|
|
queued.to.itself(unless='can_start') |
|
|
queued.to(started, cond='can_start') |
|
|
started.to.itself(unless='is_finished') |
|
|
started.to(succeeded, cond='is_succeeded') |
|
|
started.to(failed, cond='is_failed')
|
|
)
|
|
|
|
def __init__(self, binary, *args, **kwargs):
|
|
self.binary = binary
|
|
super().__init__(binary, *args, **kwargs)
|
|
|
|
def __repr__(self) -> str:
|
|
return f'Binary[{self.binary.id}]'
|
|
|
|
def __str__(self) -> str:
|
|
return self.__repr__()
|
|
|
|
def can_start(self) -> bool:
|
|
"""Check if binary installation can start."""
|
|
return bool(self.binary.name and self.binary.binproviders)
|
|
|
|
def is_succeeded(self) -> bool:
|
|
"""Check if installation succeeded (status was set by run())."""
|
|
return self.binary.status == Binary.StatusChoices.SUCCEEDED
|
|
|
|
def is_failed(self) -> bool:
|
|
"""Check if installation failed (status was set by run())."""
|
|
return self.binary.status == Binary.StatusChoices.FAILED
|
|
|
|
def is_finished(self) -> bool:
|
|
"""Check if installation has completed (success or failure)."""
|
|
return self.binary.status in (
|
|
Binary.StatusChoices.SUCCEEDED,
|
|
Binary.StatusChoices.FAILED,
|
|
)
|
|
|
|
@queued.enter
|
|
def enter_queued(self):
|
|
"""Binary is queued for installation."""
|
|
self.binary.update_for_workers(
|
|
retry_at=timezone.now(),
|
|
status=Binary.StatusChoices.QUEUED,
|
|
)
|
|
|
|
@started.enter
|
|
def enter_started(self):
|
|
"""Start binary installation."""
|
|
# Lock the binary while installation runs
|
|
self.binary.update_for_workers(
|
|
retry_at=timezone.now() + timedelta(seconds=300), # 5 min timeout for installation
|
|
status=Binary.StatusChoices.STARTED,
|
|
)
|
|
|
|
# Run installation hooks
|
|
self.binary.run()
|
|
|
|
# Save updated status (run() updates status to succeeded/failed)
|
|
self.binary.save()
|
|
|
|
@succeeded.enter
|
|
def enter_succeeded(self):
|
|
"""Binary installed successfully."""
|
|
self.binary.update_for_workers(
|
|
retry_at=None,
|
|
status=Binary.StatusChoices.SUCCEEDED,
|
|
)
|
|
|
|
# Increment health stats
|
|
Binary.objects.filter(pk=self.binary.pk).update(num_uses_succeeded=F('num_uses_succeeded') + 1)
|
|
|
|
@failed.enter
|
|
def enter_failed(self):
|
|
"""Binary installation failed."""
|
|
self.binary.update_for_workers(
|
|
retry_at=None,
|
|
status=Binary.StatusChoices.FAILED,
|
|
)
|
|
|
|
# Increment health stats
|
|
Binary.objects.filter(pk=self.binary.pk).update(num_uses_failed=F('num_uses_failed') + 1)
|