mirror of
https://github.com/ArchiveBox/ArchiveBox.git
synced 2026-09-14 02:56:11 +05:00
1411 lines
51 KiB
Python
1411 lines
51 KiB
Python
"""
|
|
Tests for archivebox run CLI command.
|
|
|
|
Tests cover:
|
|
- run with stdin JSONL (Crawl, Snapshot, ArchiveResult)
|
|
- create-or-update behavior (records with/without id)
|
|
- pass-through output (for chaining)
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from archivebox.tests.conftest import (
|
|
run_archivebox_cmd,
|
|
parse_jsonl_output,
|
|
create_test_url,
|
|
create_test_crawl_json,
|
|
create_test_snapshot_json,
|
|
)
|
|
|
|
RUN_TEST_ENV = {
|
|
"PLUGINS": "favicon",
|
|
"SAVE_FAVICON": "True",
|
|
}
|
|
|
|
|
|
class TestRunWithCrawl:
|
|
"""Tests for `archivebox run` with Crawl input."""
|
|
|
|
def test_run_with_new_crawl(self, initialized_archive):
|
|
"""Run creates and processes a new Crawl (no id)."""
|
|
crawl_record = create_test_crawl_json()
|
|
|
|
stdout, stderr, code = run_archivebox_cmd(
|
|
["run"],
|
|
stdin=json.dumps(crawl_record),
|
|
data_dir=initialized_archive,
|
|
timeout=120,
|
|
env=RUN_TEST_ENV,
|
|
)
|
|
|
|
assert code == 0, f"Command failed: {stderr}"
|
|
|
|
# Should output the created Crawl
|
|
records = parse_jsonl_output(stdout)
|
|
crawl_records = [r for r in records if r.get("type") == "Crawl"]
|
|
assert len(crawl_records) >= 1
|
|
assert crawl_records[0].get("id") # Should have an id now
|
|
|
|
def test_run_with_existing_crawl(self, initialized_archive):
|
|
"""Run re-queues an existing Crawl (with id)."""
|
|
url = create_test_url()
|
|
|
|
# First create a crawl
|
|
stdout1, _, _ = run_archivebox_cmd(["crawl", "create", url], data_dir=initialized_archive, env=RUN_TEST_ENV)
|
|
crawl = parse_jsonl_output(stdout1)[0]
|
|
|
|
# Run with the existing crawl
|
|
stdout2, stderr, code = run_archivebox_cmd(
|
|
["run"],
|
|
stdin=json.dumps(crawl),
|
|
data_dir=initialized_archive,
|
|
timeout=120,
|
|
env=RUN_TEST_ENV,
|
|
)
|
|
|
|
assert code == 0
|
|
records = parse_jsonl_output(stdout2)
|
|
assert len(records) >= 1
|
|
|
|
|
|
class TestRunWithSnapshot:
|
|
"""Tests for `archivebox run` with Snapshot input."""
|
|
|
|
def test_run_with_new_snapshot(self, initialized_archive):
|
|
"""Run creates and processes a new Snapshot (no id, just url)."""
|
|
snapshot_record = create_test_snapshot_json()
|
|
|
|
stdout, stderr, code = run_archivebox_cmd(
|
|
["run"],
|
|
stdin=json.dumps(snapshot_record),
|
|
data_dir=initialized_archive,
|
|
timeout=120,
|
|
env=RUN_TEST_ENV,
|
|
)
|
|
|
|
assert code == 0, f"Command failed: {stderr}"
|
|
|
|
records = parse_jsonl_output(stdout)
|
|
snapshot_records = [r for r in records if r.get("type") == "Snapshot"]
|
|
assert len(snapshot_records) >= 1
|
|
assert snapshot_records[0].get("id")
|
|
|
|
def test_run_with_existing_snapshot(self, initialized_archive):
|
|
"""Run re-queues an existing Snapshot (with id)."""
|
|
url = create_test_url()
|
|
|
|
# First create a snapshot
|
|
stdout1, _, _ = run_archivebox_cmd(["snapshot", "create", url], data_dir=initialized_archive, env=RUN_TEST_ENV)
|
|
snapshot = parse_jsonl_output(stdout1)[0]
|
|
|
|
# Run with the existing snapshot
|
|
stdout2, stderr, code = run_archivebox_cmd(
|
|
["run"],
|
|
stdin=json.dumps(snapshot),
|
|
data_dir=initialized_archive,
|
|
timeout=120,
|
|
env=RUN_TEST_ENV,
|
|
)
|
|
|
|
assert code == 0
|
|
records = parse_jsonl_output(stdout2)
|
|
assert len(records) >= 1
|
|
|
|
def test_run_with_plain_url(self, initialized_archive):
|
|
"""Run accepts plain URL records (no type field)."""
|
|
url = create_test_url()
|
|
url_record = {"url": url}
|
|
|
|
stdout, stderr, code = run_archivebox_cmd(
|
|
["run"],
|
|
stdin=json.dumps(url_record),
|
|
data_dir=initialized_archive,
|
|
timeout=120,
|
|
env=RUN_TEST_ENV,
|
|
)
|
|
|
|
assert code == 0
|
|
records = parse_jsonl_output(stdout)
|
|
assert len(records) >= 1
|
|
|
|
|
|
class TestRunWithArchiveResult:
|
|
"""Tests for `archivebox run` with ArchiveResult input."""
|
|
|
|
def test_run_requeues_failed_archiveresult(self, initialized_archive):
|
|
"""Run re-queues a failed ArchiveResult."""
|
|
url = create_test_url()
|
|
|
|
# Create snapshot and archive result
|
|
stdout1, _, _ = run_archivebox_cmd(["snapshot", "create", url], data_dir=initialized_archive, env=RUN_TEST_ENV)
|
|
snapshot = parse_jsonl_output(stdout1)[0]
|
|
|
|
stdout2, _, _ = run_archivebox_cmd(
|
|
["archiveresult", "create", "--plugin=favicon"],
|
|
stdin=json.dumps(snapshot),
|
|
data_dir=initialized_archive,
|
|
env=RUN_TEST_ENV,
|
|
)
|
|
ar = next(r for r in parse_jsonl_output(stdout2) if r.get("type") == "ArchiveResult")
|
|
|
|
# Update to failed
|
|
ar["status"] = "failed"
|
|
run_archivebox_cmd(
|
|
["archiveresult", "update", "--status=failed"],
|
|
stdin=json.dumps(ar),
|
|
data_dir=initialized_archive,
|
|
env=RUN_TEST_ENV,
|
|
)
|
|
|
|
# Now run should re-queue it
|
|
stdout3, stderr, code = run_archivebox_cmd(
|
|
["run"],
|
|
stdin=json.dumps(ar),
|
|
data_dir=initialized_archive,
|
|
timeout=120,
|
|
env=RUN_TEST_ENV,
|
|
)
|
|
|
|
assert code == 0
|
|
records = parse_jsonl_output(stdout3)
|
|
ar_records = [r for r in records if r.get("type") == "ArchiveResult"]
|
|
assert len(ar_records) >= 1
|
|
|
|
|
|
class TestRunPassThrough:
|
|
"""Tests for pass-through behavior in `archivebox run`."""
|
|
|
|
def test_run_passes_through_unknown_types(self, initialized_archive):
|
|
"""Run passes through records with unknown types."""
|
|
unknown_record = {"type": "Unknown", "id": "fake-id", "data": "test"}
|
|
|
|
stdout, stderr, code = run_archivebox_cmd(
|
|
["run"],
|
|
stdin=json.dumps(unknown_record),
|
|
data_dir=initialized_archive,
|
|
)
|
|
|
|
assert code == 0
|
|
records = parse_jsonl_output(stdout)
|
|
unknown_records = [r for r in records if r.get("type") == "Unknown"]
|
|
assert len(unknown_records) == 1
|
|
assert unknown_records[0]["data"] == "test"
|
|
|
|
def test_run_outputs_all_processed_records(self, initialized_archive):
|
|
"""Run outputs all processed records for chaining."""
|
|
url = create_test_url()
|
|
crawl_record = create_test_crawl_json(urls=[url])
|
|
|
|
stdout, stderr, code = run_archivebox_cmd(
|
|
["run"],
|
|
stdin=json.dumps(crawl_record),
|
|
data_dir=initialized_archive,
|
|
timeout=120,
|
|
env=RUN_TEST_ENV,
|
|
)
|
|
|
|
assert code == 0
|
|
records = parse_jsonl_output(stdout)
|
|
# Should have at least the Crawl in output
|
|
assert len(records) >= 1
|
|
|
|
|
|
class TestRunMixedInput:
|
|
"""Tests for `archivebox run` with mixed record types."""
|
|
|
|
def test_run_handles_mixed_types(self, initialized_archive):
|
|
"""Run handles mixed Crawl/Snapshot/ArchiveResult input."""
|
|
crawl = create_test_crawl_json()
|
|
snapshot = create_test_snapshot_json()
|
|
unknown = {"type": "Tag", "id": "fake", "name": "test"}
|
|
|
|
stdin = "\n".join(
|
|
[
|
|
json.dumps(crawl),
|
|
json.dumps(snapshot),
|
|
json.dumps(unknown),
|
|
],
|
|
)
|
|
|
|
stdout, stderr, code = run_archivebox_cmd(
|
|
["run"],
|
|
stdin=stdin,
|
|
data_dir=initialized_archive,
|
|
timeout=120,
|
|
env=RUN_TEST_ENV,
|
|
)
|
|
|
|
assert code == 0
|
|
records = parse_jsonl_output(stdout)
|
|
|
|
types = {r.get("type") for r in records}
|
|
# Should have processed Crawl and Snapshot, passed through Tag
|
|
assert "Crawl" in types or "Snapshot" in types or "Tag" in types
|
|
|
|
|
|
class TestRunEmpty:
|
|
"""Tests for `archivebox run` edge cases."""
|
|
|
|
def test_run_empty_stdin(self, initialized_archive):
|
|
"""Run with empty stdin returns success."""
|
|
stdout, stderr, code = run_archivebox_cmd(
|
|
["run"],
|
|
stdin="",
|
|
data_dir=initialized_archive,
|
|
)
|
|
|
|
assert code == 0
|
|
|
|
def test_run_no_records_to_process(self, initialized_archive):
|
|
"""Run with only pass-through records shows message."""
|
|
unknown = {"type": "Unknown", "id": "fake"}
|
|
|
|
stdout, stderr, code = run_archivebox_cmd(
|
|
["run"],
|
|
stdin=json.dumps(unknown),
|
|
data_dir=initialized_archive,
|
|
)
|
|
|
|
assert code == 0
|
|
assert "No records to process" in stderr
|
|
|
|
|
|
class TestRunDaemonMode:
|
|
@pytest.mark.parametrize("stdin_kind", ["malformed", "valid-snapshot"])
|
|
def test_run_daemon_ignores_piped_stdin_and_starts_real_runner(self, initialized_archive, db, stdin_kind):
|
|
from archivebox.machine.models import Process
|
|
from archivebox.core.models import Snapshot
|
|
from archivebox.tests.test_orm_helpers import use_archivebox_db
|
|
|
|
snapshot_url = None
|
|
if stdin_kind == "valid-snapshot":
|
|
snapshot_url = create_test_url()
|
|
piped_stdin = json.dumps(create_test_snapshot_json(url=snapshot_url)) + "\n"
|
|
else:
|
|
piped_stdin = "{this is not jsonl}\n"
|
|
|
|
env = os.environ.copy()
|
|
env.update(
|
|
{
|
|
"DATA_DIR": str(initialized_archive),
|
|
"USE_COLOR": "False",
|
|
"SHOW_PROGRESS": "False",
|
|
"USE_INDEXING_BACKEND": "False",
|
|
},
|
|
)
|
|
proc = subprocess.Popen(
|
|
[sys.executable, "-m", "archivebox", "run", "--daemon"],
|
|
cwd=initialized_archive,
|
|
env=env,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
start_new_session=True,
|
|
)
|
|
assert proc.stdin is not None
|
|
assert proc.stdout is not None
|
|
assert proc.stderr is not None
|
|
|
|
try:
|
|
proc.stdin.write(piped_stdin)
|
|
proc.stdin.close()
|
|
|
|
deadline = time.monotonic() + 20
|
|
started = False
|
|
while time.monotonic() < deadline:
|
|
if proc.poll() is not None:
|
|
stdout = proc.stdout.read()
|
|
stderr = proc.stderr.read()
|
|
pytest.fail(f"daemon exited before starting runner: code={proc.returncode}\nstdout={stdout}\nstderr={stderr}")
|
|
with use_archivebox_db(initialized_archive):
|
|
started = Process.objects.filter(
|
|
process_type=Process.TypeChoices.ORCHESTRATOR,
|
|
status=Process.StatusChoices.RUNNING,
|
|
pid=proc.pid,
|
|
).exists()
|
|
if started:
|
|
break
|
|
time.sleep(0.25)
|
|
|
|
assert started is True
|
|
if snapshot_url is not None:
|
|
with use_archivebox_db(initialized_archive):
|
|
assert not Snapshot.objects.filter(url=snapshot_url).exists()
|
|
finally:
|
|
if proc.poll() is None:
|
|
os.killpg(proc.pid, signal.SIGTERM)
|
|
try:
|
|
proc.wait(timeout=15)
|
|
except subprocess.TimeoutExpired:
|
|
os.killpg(proc.pid, signal.SIGKILL)
|
|
proc.wait(timeout=5)
|
|
|
|
stdout = proc.stdout.read()
|
|
stderr = proc.stderr.read()
|
|
assert proc.returncode == 0, stdout + stderr
|
|
assert "No records to process" not in stderr
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestRecoverOrchestratorState:
|
|
def test_recover_orchestrator_state_unlocks_started_crawl_with_pending_snapshot(self):
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import Snapshot
|
|
from archivebox.services.runner import recover_orchestrator_state
|
|
|
|
crawl = Crawl.objects.create(
|
|
urls="https://example.com",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.STARTED,
|
|
retry_at=None,
|
|
)
|
|
Snapshot.objects.create(
|
|
url="https://example.com",
|
|
crawl=crawl,
|
|
status=Snapshot.StatusChoices.QUEUED,
|
|
retry_at=None,
|
|
)
|
|
|
|
recovered = recover_orchestrator_state()
|
|
|
|
crawl.refresh_from_db()
|
|
assert recovered["unlocked_crawls"] == 1
|
|
assert crawl.status == Crawl.StatusChoices.STARTED
|
|
assert crawl.retry_at is not None
|
|
|
|
def test_recover_orchestrator_state_seals_started_crawl_with_finished_snapshots(self):
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import Snapshot
|
|
from archivebox.services.runner import recover_orchestrator_state
|
|
|
|
crawl = Crawl.objects.create(
|
|
urls="https://example.com",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.STARTED,
|
|
retry_at=None,
|
|
)
|
|
Snapshot.objects.create(
|
|
url="https://example.com",
|
|
crawl=crawl,
|
|
status=Snapshot.StatusChoices.SEALED,
|
|
retry_at=None,
|
|
)
|
|
|
|
recovered = recover_orchestrator_state()
|
|
|
|
crawl.refresh_from_db()
|
|
assert recovered["sealed_crawls"] == 1
|
|
assert crawl.status == Crawl.StatusChoices.SEALED
|
|
assert crawl.retry_at is None
|
|
|
|
def test_recover_orchestrator_state_repairs_retry_at_status_invariants(self):
|
|
from django.utils import timezone
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import Snapshot
|
|
from archivebox.services.runner import recover_orchestrator_state
|
|
|
|
user_id = get_or_create_system_user_pk()
|
|
queued_crawl = Crawl.objects.create(
|
|
urls="https://example.com/queued-crawl",
|
|
created_by_id=user_id,
|
|
status=Crawl.StatusChoices.QUEUED,
|
|
retry_at=None,
|
|
)
|
|
sealed_crawl = Crawl.objects.create(
|
|
urls="https://example.com/sealed-crawl",
|
|
created_by_id=user_id,
|
|
status=Crawl.StatusChoices.SEALED,
|
|
retry_at=timezone.now(),
|
|
)
|
|
queued_snapshot = Snapshot.objects.create(
|
|
url="https://example.com/queued-snapshot",
|
|
crawl=queued_crawl,
|
|
status=Snapshot.StatusChoices.QUEUED,
|
|
retry_at=None,
|
|
)
|
|
sealed_snapshot = Snapshot.objects.create(
|
|
url="https://example.com/sealed-snapshot",
|
|
crawl=sealed_crawl,
|
|
status=Snapshot.StatusChoices.SEALED,
|
|
retry_at=timezone.now(),
|
|
)
|
|
|
|
recovered = recover_orchestrator_state()
|
|
|
|
queued_crawl.refresh_from_db()
|
|
sealed_crawl.refresh_from_db()
|
|
queued_snapshot.refresh_from_db()
|
|
sealed_snapshot.refresh_from_db()
|
|
|
|
assert recovered["queued_crawls_unlocked"] == 1
|
|
assert recovered["queued_snapshots_unlocked"] == 1
|
|
assert queued_crawl.status == Crawl.StatusChoices.QUEUED
|
|
assert queued_crawl.retry_at is not None
|
|
assert sealed_crawl.status == Crawl.StatusChoices.SEALED
|
|
assert sealed_crawl.retry_at is not None
|
|
assert queued_snapshot.status == Snapshot.StatusChoices.QUEUED
|
|
assert queued_snapshot.retry_at is not None
|
|
assert sealed_snapshot.status == Snapshot.StatusChoices.SEALED
|
|
assert sealed_snapshot.retry_at is not None
|
|
|
|
def test_recover_orchestrator_state_requeues_backoff_archiveresults(self):
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import ArchiveResult, Snapshot
|
|
from archivebox.services.runner import recover_orchestrator_state
|
|
|
|
crawl = Crawl.objects.create(
|
|
urls="https://example.com",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.SEALED,
|
|
retry_at=None,
|
|
)
|
|
snapshot = Snapshot.objects.create(
|
|
url="https://example.com",
|
|
crawl=crawl,
|
|
status=Snapshot.StatusChoices.SEALED,
|
|
retry_at=None,
|
|
)
|
|
result = ArchiveResult.objects.create(
|
|
snapshot=snapshot,
|
|
plugin="title",
|
|
hook_name="on_Snapshot__01_title",
|
|
status=ArchiveResult.StatusChoices.BACKOFF,
|
|
)
|
|
|
|
recovered = recover_orchestrator_state()
|
|
|
|
result.refresh_from_db()
|
|
snapshot.refresh_from_db()
|
|
crawl.refresh_from_db()
|
|
|
|
assert recovered["requeued_archiveresults"] == 1
|
|
assert recovered["requeued_snapshots"] == 1
|
|
assert result.status == ArchiveResult.StatusChoices.QUEUED
|
|
assert snapshot.status == Snapshot.StatusChoices.SEALED
|
|
assert snapshot.retry_at is not None
|
|
assert crawl.status == Crawl.StatusChoices.SEALED
|
|
|
|
def test_recover_orchestrator_state_leaves_due_queued_snapshot_for_runner_even_with_final_results(self):
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import ArchiveResult, Snapshot
|
|
from archivebox.services.runner import recover_orchestrator_state
|
|
|
|
crawl = Crawl.objects.create(
|
|
urls="https://example.com",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.QUEUED,
|
|
retry_at=None,
|
|
)
|
|
snapshot = Snapshot.objects.create(
|
|
url="https://example.com",
|
|
crawl=crawl,
|
|
status=Snapshot.StatusChoices.QUEUED,
|
|
retry_at=None,
|
|
)
|
|
ArchiveResult.objects.create(
|
|
snapshot=snapshot,
|
|
plugin="title",
|
|
hook_name="on_Snapshot__01_title",
|
|
status=ArchiveResult.StatusChoices.SUCCEEDED,
|
|
)
|
|
|
|
recovered = recover_orchestrator_state()
|
|
|
|
snapshot.refresh_from_db()
|
|
crawl.refresh_from_db()
|
|
|
|
assert recovered["sealed_queued_snapshots"] == 0
|
|
assert recovered["sealed_queued_crawls"] == 0
|
|
assert snapshot.status == Snapshot.StatusChoices.QUEUED
|
|
assert snapshot.retry_at is not None
|
|
assert snapshot.downloaded_at is None
|
|
assert crawl.status == Crawl.StatusChoices.QUEUED
|
|
assert crawl.retry_at is not None
|
|
|
|
def test_recover_orchestrator_state_seals_stale_queued_snapshot_with_final_results(self):
|
|
from datetime import timedelta
|
|
|
|
from django.utils import timezone
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import ArchiveResult, Snapshot
|
|
from archivebox.services.runner import recover_orchestrator_state
|
|
|
|
old = timezone.now() - timedelta(hours=13)
|
|
crawl = Crawl.objects.create(
|
|
urls="https://example.com",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.QUEUED,
|
|
retry_at=old,
|
|
)
|
|
snapshot = Snapshot.objects.create(
|
|
url="https://example.com",
|
|
crawl=crawl,
|
|
status=Snapshot.StatusChoices.QUEUED,
|
|
retry_at=old,
|
|
)
|
|
result = ArchiveResult.objects.create(
|
|
snapshot=snapshot,
|
|
plugin="title",
|
|
hook_name="on_Snapshot__01_title",
|
|
status=ArchiveResult.StatusChoices.SUCCEEDED,
|
|
)
|
|
Crawl.objects.filter(pk=crawl.pk).update(modified_at=old)
|
|
Snapshot.objects.filter(pk=snapshot.pk).update(modified_at=old)
|
|
ArchiveResult.objects.filter(pk=result.pk).update(modified_at=old)
|
|
|
|
recovered = recover_orchestrator_state()
|
|
|
|
snapshot.refresh_from_db()
|
|
crawl.refresh_from_db()
|
|
|
|
assert recovered["sealed_queued_snapshots"] == 1
|
|
assert recovered["sealed_queued_crawls"] == 1
|
|
assert snapshot.status == Snapshot.StatusChoices.SEALED
|
|
assert snapshot.retry_at is None
|
|
assert snapshot.downloaded_at is not None
|
|
assert crawl.status == Crawl.StatusChoices.SEALED
|
|
assert crawl.retry_at is None
|
|
|
|
def test_recover_orchestrator_state_raises_on_stale_active_crawl(self):
|
|
from datetime import timedelta
|
|
|
|
from django.utils import timezone
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.services.runner import recover_orchestrator_state
|
|
|
|
old = timezone.now() - timedelta(hours=13)
|
|
crawl = Crawl.objects.create(
|
|
urls="https://example.com",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.QUEUED,
|
|
retry_at=old,
|
|
)
|
|
Crawl.objects.filter(id=crawl.id).update(modified_at=old, retry_at=old)
|
|
|
|
with pytest.raises(RuntimeError, match="Stuck crawl invariant violated"):
|
|
recover_orchestrator_state()
|
|
|
|
def test_recover_orchestrator_state_unlocks_started_snapshot_without_running_result(self):
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import Snapshot
|
|
from archivebox.services.runner import recover_orchestrator_state
|
|
|
|
crawl = Crawl.objects.create(
|
|
urls="https://example.com",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.SEALED,
|
|
retry_at=None,
|
|
)
|
|
snapshot = Snapshot.objects.create(
|
|
url="https://example.com",
|
|
crawl=crawl,
|
|
status=Snapshot.StatusChoices.STARTED,
|
|
retry_at=None,
|
|
)
|
|
|
|
recovered = recover_orchestrator_state()
|
|
|
|
snapshot.refresh_from_db()
|
|
crawl.refresh_from_db()
|
|
|
|
assert recovered["unlocked_snapshots"] == 1
|
|
assert snapshot.status == Snapshot.StatusChoices.STARTED
|
|
assert snapshot.retry_at is not None
|
|
assert crawl.status == Crawl.StatusChoices.SEALED
|
|
assert crawl.retry_at is None
|
|
|
|
def test_recover_orchestrator_state_requeues_sealed_snapshot_with_queued_results(self):
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import ArchiveResult, Snapshot
|
|
from archivebox.services.runner import recover_orchestrator_state
|
|
|
|
crawl = Crawl.objects.create(
|
|
urls="https://example.com",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.SEALED,
|
|
retry_at=None,
|
|
)
|
|
snapshot = Snapshot.objects.create(
|
|
url="https://example.com",
|
|
crawl=crawl,
|
|
status=Snapshot.StatusChoices.SEALED,
|
|
retry_at=None,
|
|
)
|
|
ArchiveResult.objects.create(
|
|
snapshot=snapshot,
|
|
plugin="title",
|
|
hook_name="on_Snapshot__01_title",
|
|
status=ArchiveResult.StatusChoices.QUEUED,
|
|
)
|
|
|
|
recovered = recover_orchestrator_state()
|
|
|
|
snapshot.refresh_from_db()
|
|
crawl.refresh_from_db()
|
|
|
|
assert recovered["requeued_snapshots"] == 1
|
|
assert snapshot.status == Snapshot.StatusChoices.SEALED
|
|
assert snapshot.retry_at is not None
|
|
assert crawl.status == Crawl.StatusChoices.SEALED
|
|
assert crawl.retry_at is None
|
|
|
|
def test_recover_orchestrator_state_ignores_sealed_downloaded_snapshot_without_results(self):
|
|
from django.utils import timezone
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import Snapshot
|
|
from archivebox.services.runner import recover_orchestrator_state
|
|
|
|
crawl = Crawl.objects.create(
|
|
urls="https://example.com",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.SEALED,
|
|
retry_at=None,
|
|
)
|
|
snapshot = Snapshot.objects.create(
|
|
url="https://example.com",
|
|
crawl=crawl,
|
|
status=Snapshot.StatusChoices.SEALED,
|
|
downloaded_at=timezone.now(),
|
|
retry_at=None,
|
|
)
|
|
|
|
recovered = recover_orchestrator_state()
|
|
|
|
snapshot.refresh_from_db()
|
|
crawl.refresh_from_db()
|
|
|
|
assert recovered["requeued_snapshots"] == 0
|
|
assert recovered["unlocked_snapshots"] == 0
|
|
assert snapshot.status == Snapshot.StatusChoices.SEALED
|
|
assert snapshot.retry_at is None
|
|
assert crawl.status == Crawl.StatusChoices.SEALED
|
|
assert crawl.retry_at is None
|
|
|
|
def test_recover_orchestrator_state_seals_started_snapshot_with_final_results(self):
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import ArchiveResult, Snapshot
|
|
from archivebox.services.runner import recover_orchestrator_state
|
|
|
|
crawl = Crawl.objects.create(
|
|
urls="https://example.com",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.STARTED,
|
|
retry_at=None,
|
|
)
|
|
snapshot = Snapshot.objects.create(
|
|
url="https://example.com",
|
|
crawl=crawl,
|
|
status=Snapshot.StatusChoices.STARTED,
|
|
retry_at=None,
|
|
)
|
|
ArchiveResult.objects.create(
|
|
snapshot=snapshot,
|
|
plugin="title",
|
|
hook_name="on_Snapshot__01_title",
|
|
status=ArchiveResult.StatusChoices.SUCCEEDED,
|
|
)
|
|
|
|
recovered = recover_orchestrator_state()
|
|
|
|
snapshot.refresh_from_db()
|
|
assert recovered["sealed_snapshots"] == 1
|
|
assert snapshot.status == Snapshot.StatusChoices.SEALED
|
|
assert snapshot.retry_at is None
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestRunDueCrawlState:
|
|
def test_maintenance_only_runner_does_not_start_regular_queued_crawls(self):
|
|
from django.utils import timezone
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.services.runner import run_pending_crawls
|
|
|
|
now = timezone.now()
|
|
crawl = Crawl.objects.create(
|
|
urls="https://example.com",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.QUEUED,
|
|
retry_at=now,
|
|
)
|
|
|
|
assert run_pending_crawls(daemon=False, maintenance_only=True) == 0
|
|
|
|
crawl.refresh_from_db()
|
|
assert crawl.status == Crawl.StatusChoices.QUEUED
|
|
assert crawl.retry_at == now
|
|
assert crawl.snapshot_set.count() == 0
|
|
|
|
def test_snapshot_start_writes_short_future_lease(self):
|
|
from django.utils import timezone
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import Snapshot
|
|
|
|
crawl = Crawl.objects.create(
|
|
urls="https://example.com",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.STARTED,
|
|
retry_at=timezone.now(),
|
|
)
|
|
snapshot = Snapshot.objects.create(
|
|
url="https://example.com",
|
|
crawl=crawl,
|
|
status=Snapshot.StatusChoices.QUEUED,
|
|
retry_at=timezone.now(),
|
|
)
|
|
|
|
snapshot.sm.tick()
|
|
snapshot.refresh_from_db()
|
|
|
|
assert snapshot.status == Snapshot.StatusChoices.STARTED
|
|
assert snapshot.retry_at is not None
|
|
assert snapshot.retry_at > timezone.now()
|
|
|
|
def test_abandoned_started_snapshot_results_are_reset_locally_for_resume(self):
|
|
from django.utils import timezone
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import ArchiveResult, Snapshot
|
|
from archivebox.services.runner import reset_abandoned_snapshot_results
|
|
|
|
crawl = Crawl.objects.create(
|
|
urls="https://example.com",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.STARTED,
|
|
retry_at=timezone.now(),
|
|
)
|
|
snapshot = Snapshot.objects.create(
|
|
url="https://example.com",
|
|
crawl=crawl,
|
|
status=Snapshot.StatusChoices.STARTED,
|
|
retry_at=timezone.now(),
|
|
)
|
|
abandoned = ArchiveResult.objects.create(
|
|
snapshot=snapshot,
|
|
plugin="title",
|
|
hook_name="on_Snapshot__01_title",
|
|
status=ArchiveResult.StatusChoices.STARTED,
|
|
output_str="partial output should be cleared",
|
|
output_files={"partial.txt": {"size": 12}},
|
|
output_size=12,
|
|
start_ts=timezone.now(),
|
|
)
|
|
queued = ArchiveResult.objects.create(
|
|
snapshot=snapshot,
|
|
plugin="wget",
|
|
hook_name="on_Snapshot__40_wget",
|
|
status=ArchiveResult.StatusChoices.QUEUED,
|
|
)
|
|
finished = ArchiveResult.objects.create(
|
|
snapshot=snapshot,
|
|
plugin="favicon",
|
|
hook_name="on_Snapshot__01_favicon",
|
|
status=ArchiveResult.StatusChoices.SUCCEEDED,
|
|
output_str="keep me",
|
|
output_files={"favicon.ico": {"size": 1}},
|
|
output_size=1,
|
|
)
|
|
|
|
reset_abandoned_snapshot_results(snapshot)
|
|
|
|
abandoned.refresh_from_db()
|
|
queued.refresh_from_db()
|
|
finished.refresh_from_db()
|
|
|
|
assert abandoned.status == ArchiveResult.StatusChoices.QUEUED
|
|
assert abandoned.output_str == ""
|
|
assert abandoned.output_files == {}
|
|
assert abandoned.output_size == 0
|
|
assert queued.status == ArchiveResult.StatusChoices.QUEUED
|
|
assert finished.status == ArchiveResult.StatusChoices.SUCCEEDED
|
|
assert finished.output_str == "keep me"
|
|
assert finished.output_files == {"favicon.ico": {"size": 1}}
|
|
|
|
def test_due_started_snapshot_with_live_child_extends_lease_without_reset(self):
|
|
import os
|
|
from datetime import datetime
|
|
|
|
import psutil
|
|
from django.utils import timezone
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import ArchiveResult, Snapshot
|
|
from archivebox.machine.models import Machine, NetworkInterface, Process
|
|
from archivebox.services.runner import run_due_snapshot
|
|
|
|
now = timezone.now()
|
|
os_proc = psutil.Process(os.getpid())
|
|
crawl = Crawl.objects.create(
|
|
urls="https://example.com",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.STARTED,
|
|
retry_at=now,
|
|
)
|
|
snapshot = Snapshot.objects.create(
|
|
url="https://example.com",
|
|
crawl=crawl,
|
|
status=Snapshot.StatusChoices.STARTED,
|
|
retry_at=now,
|
|
)
|
|
process = Process.objects.create(
|
|
machine=Machine.current(),
|
|
iface=NetworkInterface.current(),
|
|
process_type=Process.TypeChoices.HOOK,
|
|
status=Process.StatusChoices.RUNNING,
|
|
pid=os.getpid(),
|
|
started_at=datetime.fromtimestamp(os_proc.create_time(), tz=timezone.get_current_timezone()),
|
|
cmd=os_proc.cmdline(),
|
|
pwd=str(snapshot.output_dir / "title"),
|
|
)
|
|
result = ArchiveResult.objects.create(
|
|
snapshot=snapshot,
|
|
process=process,
|
|
plugin="title",
|
|
hook_name="on_Snapshot__01_title",
|
|
status=ArchiveResult.StatusChoices.STARTED,
|
|
output_str="live work should not be reset",
|
|
output_files={"partial.txt": {"size": 12}},
|
|
output_size=12,
|
|
)
|
|
|
|
assert run_due_snapshot(snapshot, lock_seconds=60) is True
|
|
|
|
snapshot.refresh_from_db()
|
|
result.refresh_from_db()
|
|
assert snapshot.status == Snapshot.StatusChoices.STARTED
|
|
assert snapshot.retry_at is not None
|
|
assert snapshot.retry_at > now
|
|
assert result.status == ArchiveResult.StatusChoices.STARTED
|
|
assert result.output_str == "live work should not be reset"
|
|
assert result.output_files == {"partial.txt": {"size": 12}}
|
|
assert result.output_size == 12
|
|
|
|
def test_run_due_crawl_seals_finished_started_crawl(self):
|
|
from django.utils import timezone
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import Snapshot
|
|
from archivebox.services.runner import run_due_crawl
|
|
|
|
crawl = Crawl.objects.create(
|
|
urls="https://example.com",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.STARTED,
|
|
retry_at=timezone.now(),
|
|
)
|
|
Snapshot.objects.create(
|
|
url="https://example.com",
|
|
crawl=crawl,
|
|
status=Snapshot.StatusChoices.SEALED,
|
|
retry_at=None,
|
|
)
|
|
|
|
assert run_due_crawl(crawl, lock_seconds=10) is True
|
|
|
|
crawl.refresh_from_db()
|
|
assert crawl.status == Crawl.StatusChoices.SEALED
|
|
assert crawl.retry_at is None
|
|
|
|
def test_run_due_crawl_preserves_next_future_snapshot_retry(self):
|
|
from datetime import timedelta
|
|
|
|
from django.utils import timezone
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import Snapshot
|
|
from archivebox.services.runner import run_due_crawl
|
|
|
|
future = timezone.now() + timedelta(hours=1)
|
|
crawl = Crawl.objects.create(
|
|
urls="https://example.com",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.STARTED,
|
|
retry_at=timezone.now(),
|
|
)
|
|
Snapshot.objects.create(
|
|
url="https://example.com",
|
|
crawl=crawl,
|
|
status=Snapshot.StatusChoices.QUEUED,
|
|
retry_at=future,
|
|
)
|
|
|
|
assert run_due_crawl(crawl, lock_seconds=10) is True
|
|
|
|
crawl.refresh_from_db()
|
|
assert crawl.status == Crawl.StatusChoices.STARTED
|
|
assert crawl.retry_at == future
|
|
|
|
def test_run_due_crawl_preserves_next_future_started_snapshot_lease(self):
|
|
from datetime import timedelta
|
|
|
|
from django.utils import timezone
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import Snapshot
|
|
from archivebox.services.runner import run_due_crawl
|
|
|
|
future = timezone.now() + timedelta(minutes=5)
|
|
crawl = Crawl.objects.create(
|
|
urls="https://example.com",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.STARTED,
|
|
retry_at=timezone.now(),
|
|
)
|
|
Snapshot.objects.create(
|
|
url="https://example.com",
|
|
crawl=crawl,
|
|
status=Snapshot.StatusChoices.STARTED,
|
|
retry_at=future,
|
|
)
|
|
|
|
assert run_due_crawl(crawl, lock_seconds=10) is True
|
|
|
|
crawl.refresh_from_db()
|
|
assert crawl.status == Crawl.StatusChoices.STARTED
|
|
assert crawl.retry_at == future
|
|
|
|
def test_run_due_crawl_unlocks_null_retry_queued_snapshot(self):
|
|
from django.utils import timezone
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import Snapshot
|
|
from archivebox.services.runner import run_due_crawl
|
|
|
|
crawl = Crawl.objects.create(
|
|
urls="https://example.com",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.STARTED,
|
|
retry_at=timezone.now(),
|
|
)
|
|
snapshot = Snapshot.objects.create(
|
|
url="https://example.com",
|
|
crawl=crawl,
|
|
status=Snapshot.StatusChoices.QUEUED,
|
|
retry_at=None,
|
|
)
|
|
|
|
assert run_due_crawl(crawl, lock_seconds=10) is True
|
|
|
|
crawl.refresh_from_db()
|
|
snapshot.refresh_from_db()
|
|
assert crawl.status == Crawl.StatusChoices.STARTED
|
|
assert crawl.retry_at is not None
|
|
assert snapshot.retry_at is not None
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestRecoverOrchestratorStateRedFailureModes:
|
|
def test_recovery_does_not_seal_queued_snapshot_waiting_for_future_retry_even_with_final_results(self):
|
|
from datetime import timedelta
|
|
|
|
from django.utils import timezone
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import ArchiveResult, Snapshot
|
|
from archivebox.services.runner import recover_orchestrator_state
|
|
|
|
future = timezone.now() + timedelta(days=1)
|
|
crawl = Crawl.objects.create(
|
|
urls="https://example.com",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.STARTED,
|
|
retry_at=future,
|
|
)
|
|
snapshot = Snapshot.objects.create(
|
|
url="https://example.com",
|
|
crawl=crawl,
|
|
status=Snapshot.StatusChoices.QUEUED,
|
|
retry_at=future,
|
|
)
|
|
ArchiveResult.objects.create(
|
|
snapshot=snapshot,
|
|
plugin="title",
|
|
hook_name="on_Snapshot__01_title",
|
|
status=ArchiveResult.StatusChoices.SUCCEEDED,
|
|
)
|
|
|
|
recover_orchestrator_state()
|
|
|
|
snapshot.refresh_from_db()
|
|
assert snapshot.status == Snapshot.StatusChoices.QUEUED
|
|
assert snapshot.retry_at == future
|
|
|
|
def test_recovery_does_not_seal_queued_crawl_waiting_for_future_retry_even_with_finished_snapshots(self):
|
|
from datetime import timedelta
|
|
|
|
from django.utils import timezone
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import Snapshot
|
|
from archivebox.services.runner import recover_orchestrator_state
|
|
|
|
future = timezone.now() + timedelta(days=1)
|
|
crawl = Crawl.objects.create(
|
|
urls="https://example.com",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.QUEUED,
|
|
retry_at=future,
|
|
)
|
|
Snapshot.objects.create(url="https://example.com", crawl=crawl, status=Snapshot.StatusChoices.SEALED, retry_at=None)
|
|
|
|
recover_orchestrator_state()
|
|
|
|
crawl.refresh_from_db()
|
|
assert crawl.status == Crawl.StatusChoices.QUEUED
|
|
assert crawl.retry_at == future
|
|
|
|
def test_recovery_keeps_sealed_parent_when_future_retry_child_is_scheduled(self):
|
|
from datetime import timedelta
|
|
|
|
from django.utils import timezone
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import Snapshot
|
|
from archivebox.services.runner import recover_orchestrator_state
|
|
|
|
future = timezone.now() + timedelta(days=1)
|
|
crawl = Crawl.objects.create(
|
|
urls="https://blog.sweeting.me",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.SEALED,
|
|
retry_at=None,
|
|
)
|
|
snapshot = Snapshot.objects.create(
|
|
url="https://blog.sweeting.me",
|
|
crawl=crawl,
|
|
status=Snapshot.StatusChoices.QUEUED,
|
|
retry_at=future,
|
|
)
|
|
|
|
recover_orchestrator_state()
|
|
|
|
crawl.refresh_from_db()
|
|
snapshot.refresh_from_db()
|
|
assert crawl.status == Crawl.StatusChoices.SEALED
|
|
assert crawl.retry_at is None
|
|
assert snapshot.retry_at == future
|
|
|
|
def test_recovery_unlocks_started_parent_to_future_retry_child_not_now(self):
|
|
from datetime import timedelta
|
|
|
|
from django.utils import timezone
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import Snapshot
|
|
from archivebox.services.runner import recover_orchestrator_state
|
|
|
|
future = timezone.now() + timedelta(days=1)
|
|
crawl = Crawl.objects.create(
|
|
urls="https://www.mathjax.org/",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.STARTED,
|
|
retry_at=None,
|
|
)
|
|
Snapshot.objects.create(url="https://www.mathjax.org/", crawl=crawl, status=Snapshot.StatusChoices.QUEUED, retry_at=future)
|
|
|
|
recover_orchestrator_state()
|
|
|
|
crawl.refresh_from_db()
|
|
assert crawl.status == Crawl.StatusChoices.STARTED
|
|
assert crawl.retry_at == future
|
|
|
|
def test_recovery_requeues_started_archiveresult_without_process(self):
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import ArchiveResult, Snapshot
|
|
from archivebox.services.runner import recover_orchestrator_state
|
|
|
|
crawl = Crawl.objects.create(
|
|
urls="https://www.mathjax.org/",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.STARTED,
|
|
retry_at=None,
|
|
)
|
|
snapshot = Snapshot.objects.create(
|
|
url="https://www.mathjax.org/",
|
|
crawl=crawl,
|
|
status=Snapshot.StatusChoices.STARTED,
|
|
retry_at=None,
|
|
)
|
|
result = ArchiveResult.objects.create(
|
|
snapshot=snapshot,
|
|
plugin="title",
|
|
hook_name="on_Snapshot__01_title",
|
|
status=ArchiveResult.StatusChoices.STARTED,
|
|
)
|
|
|
|
recover_orchestrator_state()
|
|
|
|
result.refresh_from_db()
|
|
assert result.status == ArchiveResult.StatusChoices.QUEUED
|
|
|
|
def test_recovery_requeues_started_archiveresult_with_exited_process(self):
|
|
from django.utils import timezone
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import ArchiveResult, Snapshot
|
|
from archivebox.machine.models import Machine, NetworkInterface, Process
|
|
from archivebox.services.runner import recover_orchestrator_state
|
|
|
|
crawl = Crawl.objects.create(
|
|
urls="https://revealjs.com/",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.STARTED,
|
|
retry_at=None,
|
|
)
|
|
snapshot = Snapshot.objects.create(url="https://revealjs.com/", crawl=crawl, status=Snapshot.StatusChoices.STARTED, retry_at=None)
|
|
process = Process.objects.create(
|
|
machine=Machine.current(refresh=True),
|
|
iface=NetworkInterface.current(refresh=True),
|
|
process_type=Process.TypeChoices.HOOK,
|
|
worker_type="archiveresult",
|
|
pwd=str(snapshot.output_dir / "title"),
|
|
cmd=["python", "--version"],
|
|
status=Process.StatusChoices.EXITED,
|
|
retry_at=None,
|
|
exit_code=0,
|
|
ended_at=timezone.now(),
|
|
)
|
|
result = ArchiveResult.objects.create(
|
|
snapshot=snapshot,
|
|
plugin="title",
|
|
hook_name="on_Snapshot__01_title",
|
|
status=ArchiveResult.StatusChoices.STARTED,
|
|
process=process,
|
|
)
|
|
|
|
recover_orchestrator_state()
|
|
|
|
result.refresh_from_db()
|
|
assert result.status == ArchiveResult.StatusChoices.QUEUED
|
|
|
|
def test_recovery_requeues_sealed_snapshot_started_result_with_exited_process_result_too(self):
|
|
from django.utils import timezone
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import ArchiveResult, Snapshot
|
|
from archivebox.machine.models import Machine, NetworkInterface, Process
|
|
from archivebox.services.runner import recover_orchestrator_state
|
|
|
|
crawl = Crawl.objects.create(
|
|
urls="https://pdfobject.com/pdf/sample-3pp.pdf",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.SEALED,
|
|
retry_at=None,
|
|
)
|
|
snapshot = Snapshot.objects.create(
|
|
url="https://pdfobject.com/pdf/sample-3pp.pdf",
|
|
crawl=crawl,
|
|
status=Snapshot.StatusChoices.SEALED,
|
|
retry_at=None,
|
|
)
|
|
process = Process.objects.create(
|
|
machine=Machine.current(refresh=True),
|
|
iface=NetworkInterface.current(refresh=True),
|
|
process_type=Process.TypeChoices.HOOK,
|
|
worker_type="archiveresult",
|
|
pwd=str(snapshot.output_dir / "pdf"),
|
|
cmd=["python", "--version"],
|
|
status=Process.StatusChoices.EXITED,
|
|
retry_at=None,
|
|
exit_code=0,
|
|
ended_at=timezone.now(),
|
|
)
|
|
result = ArchiveResult.objects.create(
|
|
snapshot=snapshot,
|
|
plugin="pdf",
|
|
hook_name="on_Snapshot__50_pdf",
|
|
status=ArchiveResult.StatusChoices.STARTED,
|
|
process=process,
|
|
)
|
|
|
|
recover_orchestrator_state()
|
|
|
|
snapshot.refresh_from_db()
|
|
result.refresh_from_db()
|
|
assert snapshot.status == Snapshot.StatusChoices.QUEUED
|
|
assert result.status == ArchiveResult.StatusChoices.QUEUED
|
|
|
|
def test_recovery_requeues_started_snapshot_result_before_unlocking_snapshot(self):
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import ArchiveResult, Snapshot
|
|
from archivebox.services.runner import recover_orchestrator_state
|
|
|
|
crawl = Crawl.objects.create(
|
|
urls="https://mermaid-js.github.io/mermaid/",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.STARTED,
|
|
retry_at=None,
|
|
)
|
|
snapshot = Snapshot.objects.create(
|
|
url="https://mermaid-js.github.io/mermaid/",
|
|
crawl=crawl,
|
|
status=Snapshot.StatusChoices.STARTED,
|
|
retry_at=None,
|
|
)
|
|
result = ArchiveResult.objects.create(
|
|
snapshot=snapshot,
|
|
plugin="title",
|
|
hook_name="on_Snapshot__01_title",
|
|
status=ArchiveResult.StatusChoices.STARTED,
|
|
)
|
|
|
|
recover_orchestrator_state()
|
|
|
|
snapshot.refresh_from_db()
|
|
result.refresh_from_db()
|
|
assert result.status == ArchiveResult.StatusChoices.QUEUED
|
|
assert snapshot.retry_at is not None
|
|
|
|
def test_crawl_runner_load_run_state_does_not_return_future_retry_snapshots(self):
|
|
from datetime import timedelta
|
|
|
|
from django.utils import timezone
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import Snapshot
|
|
from archivebox.services.runner import CrawlRunner
|
|
|
|
future = timezone.now() + timedelta(days=1)
|
|
crawl = Crawl.objects.create(
|
|
urls="https://example.com",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.STARTED,
|
|
retry_at=future,
|
|
)
|
|
Snapshot.objects.create(url="https://example.com", crawl=crawl, status=Snapshot.StatusChoices.QUEUED, retry_at=future)
|
|
|
|
runner = CrawlRunner(crawl, selected_plugins=[])
|
|
|
|
assert runner.load_run_state() == []
|
|
|
|
def test_crawl_runner_finalize_run_state_preserves_next_future_snapshot_retry(self):
|
|
from datetime import timedelta
|
|
|
|
from django.utils import timezone
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.core.models import Snapshot
|
|
from archivebox.services.runner import CrawlRunner
|
|
|
|
future = timezone.now() + timedelta(days=1)
|
|
crawl = Crawl.objects.create(
|
|
urls="https://blog.sweeting.me",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.STARTED,
|
|
retry_at=None,
|
|
)
|
|
Snapshot.objects.create(url="https://blog.sweeting.me", crawl=crawl, status=Snapshot.StatusChoices.QUEUED, retry_at=future)
|
|
|
|
runner = CrawlRunner(crawl, selected_plugins=[])
|
|
runner.finalize_run_state()
|
|
|
|
crawl.refresh_from_db()
|
|
assert crawl.status == Crawl.StatusChoices.STARTED
|
|
assert crawl.retry_at == future
|
|
|
|
def test_recovery_raises_stale_due_crawl_even_with_recent_unrelated_process_path_containing_crawl_id(self):
|
|
from datetime import timedelta
|
|
|
|
from django.utils import timezone
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.machine.models import Machine, NetworkInterface, Process
|
|
from archivebox.services.runner import recover_orchestrator_state
|
|
|
|
old = timezone.now() - timedelta(hours=13)
|
|
crawl = Crawl.objects.create(
|
|
urls="https://github.com/nodeca/pica",
|
|
created_by_id=get_or_create_system_user_pk(),
|
|
status=Crawl.StatusChoices.QUEUED,
|
|
retry_at=old,
|
|
)
|
|
Crawl.objects.filter(id=crawl.id).update(modified_at=old, retry_at=old)
|
|
Process.objects.create(
|
|
machine=Machine.current(refresh=True),
|
|
iface=NetworkInterface.current(refresh=True),
|
|
process_type=Process.TypeChoices.HOOK,
|
|
worker_type="archiveresult",
|
|
pwd=f"/tmp/not-an-archivebox-child/{crawl.id}/title",
|
|
cmd=["python", "--version"],
|
|
status=Process.StatusChoices.EXITED,
|
|
retry_at=None,
|
|
exit_code=0,
|
|
ended_at=timezone.now(),
|
|
)
|
|
|
|
with pytest.raises(RuntimeError, match="Stuck crawl invariant violated"):
|
|
recover_orchestrator_state()
|
|
|
|
def test_recovery_does_not_crash_on_invalid_utf8_process_logs(self, tmp_path):
|
|
from datetime import timedelta
|
|
|
|
from django.utils import timezone
|
|
|
|
from archivebox.machine.models import Machine, NetworkInterface, Process
|
|
from archivebox.services.runner import recover_orchestrator_state
|
|
|
|
runtime_dir = tmp_path / "https_example_com" / ".hooks" / "on_Snapshot__01_title.py"
|
|
runtime_dir.mkdir(parents=True)
|
|
(runtime_dir / "stdout.log").write_bytes(b"\\xff\\xfe\\xfa")
|
|
process = Process.objects.create(
|
|
machine=Machine.current(refresh=True),
|
|
iface=NetworkInterface.current(refresh=True),
|
|
process_type=Process.TypeChoices.HOOK,
|
|
worker_type="archiveresult",
|
|
pwd=str(tmp_path / "https_example_com"),
|
|
cmd=["on_Snapshot__01_title.py"],
|
|
status=Process.StatusChoices.RUNNING,
|
|
retry_at=None,
|
|
pid=999999,
|
|
started_at=timezone.now() - timedelta(hours=1),
|
|
timeout=1,
|
|
)
|
|
|
|
recover_orchestrator_state()
|
|
|
|
process.refresh_from_db()
|
|
assert process.status == Process.StatusChoices.EXITED
|