mirror of
https://github.com/ArchiveBox/ArchiveBox.git
synced 2026-09-13 18:46:17 +05:00
957 lines
37 KiB
Python
957 lines
37 KiB
Python
#!/usr/bin/env python3
|
|
"""Takeover utility tests and live command handoff flows."""
|
|
|
|
import os
|
|
import json
|
|
import re
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import psutil
|
|
|
|
from archivebox.core.models import ArchiveResult, Snapshot
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.machine.models import Process
|
|
from archivebox.tests.conftest import (
|
|
assert_no_processes_for_data_dir,
|
|
get_free_port,
|
|
kill_processes_for_data_dir,
|
|
cli_env,
|
|
pid_is_alive,
|
|
run_archivebox_cmd,
|
|
start_archivebox_server,
|
|
stop_archivebox_process,
|
|
supervisor_pid_from_log,
|
|
get_http_response,
|
|
wait_for_log,
|
|
wait_for_log_count,
|
|
wait_for_log_pattern,
|
|
wait_for_pid_to_disappear,
|
|
wait_for_snapshot_capture,
|
|
wait_for_worker_pid_from_log,
|
|
worker_pid_from_log,
|
|
)
|
|
from archivebox.tests.test_orm_helpers import use_archivebox_db
|
|
|
|
pytestmark = pytest.mark.django_db(transaction=True)
|
|
|
|
|
|
def test_pid_is_alive_treats_unreaped_archivebox_cli_as_exited(tmp_path, initialized_archive):
|
|
proc = run_archivebox_cmd(
|
|
["version"],
|
|
cwd=tmp_path,
|
|
default_cli_env=True,
|
|
disable_extractors=True,
|
|
wait=False,
|
|
)
|
|
try:
|
|
os.waitid(os.P_PID, proc.pid, os.WEXITED | os.WNOWAIT)
|
|
assert psutil.Process(proc.pid).status() == psutil.STATUS_ZOMBIE
|
|
assert not pid_is_alive(proc.pid)
|
|
finally:
|
|
proc.wait(timeout=5)
|
|
|
|
|
|
def _resolve_sonic_env(env: dict[str, str]) -> dict[str, str]:
|
|
from abx_plugins import get_plugins_dir
|
|
|
|
config = Path(get_plugins_dir()) / "search_backend_sonic" / "config.json"
|
|
result = subprocess.run(
|
|
[
|
|
str(Path(sys.executable).with_name("abxpkg")),
|
|
"env",
|
|
"--install",
|
|
"--json",
|
|
f"--lib={env['ABXPKG_LIB_DIR']}",
|
|
f"--deps-from={config}:required_binaries",
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
)
|
|
assert result.returncode == 0, result.stderr or result.stdout
|
|
payload = {str(key): str(value) for key, value in json.loads(result.stdout).items()}
|
|
assert Path(payload["SONIC_BINARY"]).is_file()
|
|
return payload
|
|
|
|
|
|
def _archive_pages_for_sqlite_reindexing(data_dir: Path, env: dict[str, str], root_url: str) -> None:
|
|
add_env = dict(env)
|
|
add_env["SEARCH_BACKEND_ENGINE"] = "ripgrep"
|
|
_cmd_result = run_archivebox_cmd(
|
|
[
|
|
"add",
|
|
"--depth=2",
|
|
"--max-urls=20",
|
|
"--crawl-max-size=50mb",
|
|
"--plugins=wget,parse_html_urls",
|
|
root_url,
|
|
],
|
|
cwd=data_dir,
|
|
env=add_env,
|
|
timeout=240,
|
|
)
|
|
stdout, stderr, returncode = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
|
assert returncode == 0, stderr or stdout
|
|
|
|
with use_archivebox_db(data_dir):
|
|
assert Snapshot.objects.filter(status=Snapshot.StatusChoices.SEALED).count() >= 1
|
|
assert not ArchiveResult.objects.filter(plugin="search_backend_sqlite").exists()
|
|
|
|
|
|
@pytest.mark.timeout(360)
|
|
def test_behavior_update_index_only_keeps_server_http_and_search_visible(tmp_path, initialized_archive, recursive_test_site):
|
|
env = cli_env(
|
|
live=True,
|
|
PLUGINS="wget,parse_html_urls,search_backend_sqlite",
|
|
SEARCH_BACKEND_ENGINE="sqlite",
|
|
SEARCH_BACKEND_SONIC_PORT=str(get_free_port()),
|
|
)
|
|
env.update(_resolve_sonic_env(env))
|
|
root_url = recursive_test_site["root_url"]
|
|
_archive_pages_for_sqlite_reindexing(tmp_path, env, root_url)
|
|
|
|
port = get_free_port()
|
|
server = None
|
|
try:
|
|
server = start_archivebox_server(tmp_path, port=port, log_name="behavior-server-update.log", env=env)
|
|
assert get_http_response(port, host=f"archivebox.localhost:{port}").status_code < 500
|
|
|
|
update = run_archivebox_cmd(
|
|
["update", "--index-only", "--batch-size=1"],
|
|
cwd=tmp_path,
|
|
env=env,
|
|
timeout=180,
|
|
)
|
|
|
|
assert update.returncode == 0, update.stderr or update.stdout
|
|
assert get_http_response(port, host=f"archivebox.localhost:{port}").status_code < 500
|
|
|
|
search = run_archivebox_cmd(
|
|
["list", "--search=contents", "--csv=url", "Root"],
|
|
cwd=tmp_path,
|
|
env=env,
|
|
timeout=60,
|
|
)
|
|
|
|
assert search.returncode == 0, search.stderr or search.stdout
|
|
assert root_url in search.stdout
|
|
finally:
|
|
if server is not None:
|
|
stop_archivebox_process(server, signal.SIGTERM)
|
|
kill_processes_for_data_dir(tmp_path)
|
|
assert_no_processes_for_data_dir(tmp_path, timeout=12)
|
|
|
|
|
|
@pytest.mark.timeout(420)
|
|
def test_behavior_update_yields_to_server_then_finishes_visible_indexing(tmp_path, initialized_archive, recursive_test_site):
|
|
env = cli_env(
|
|
live=True,
|
|
PLUGINS="wget,parse_html_urls,search_backend_sqlite",
|
|
SEARCH_BACKEND_ENGINE="sqlite",
|
|
SEARCH_BACKEND_SONIC_PORT=str(get_free_port()),
|
|
)
|
|
env.update(_resolve_sonic_env(env))
|
|
root_url = recursive_test_site["root_url"]
|
|
_archive_pages_for_sqlite_reindexing(tmp_path, env, root_url)
|
|
|
|
port = get_free_port()
|
|
update_proc = None
|
|
server = None
|
|
try:
|
|
update_log = tmp_path / "behavior-update-yields.log"
|
|
update_log_handle = update_log.open("w", encoding="utf-8")
|
|
update_proc = run_archivebox_cmd(
|
|
["update", "--index-only", "--batch-size=1"],
|
|
cwd=tmp_path,
|
|
env=env,
|
|
stdout=update_log_handle,
|
|
stderr=subprocess.STDOUT,
|
|
start_new_session=True,
|
|
wait=False,
|
|
)
|
|
update_log_handle.close()
|
|
wait_for_log(update_log, "[*] Reindexing", timeout=90)
|
|
|
|
server = start_archivebox_server(tmp_path, port=port, log_name="behavior-server-takes-update.log", env=env)
|
|
assert get_http_response(port, host=f"archivebox.localhost:{port}").status_code < 500
|
|
wait_for_log(update_log, "A newer archivebox process took over the orchestrator, sonic", timeout=90)
|
|
|
|
stop_archivebox_process(server, signal.SIGTERM)
|
|
server = None
|
|
update_proc.wait(timeout=180)
|
|
update_text = update_log.read_text(encoding="utf-8", errors="replace")
|
|
assert update_proc.returncode == 0, update_text
|
|
|
|
search = run_archivebox_cmd(
|
|
["list", "--search=contents", "--csv=url", "Root"],
|
|
cwd=tmp_path,
|
|
env=env,
|
|
timeout=60,
|
|
)
|
|
|
|
assert search.returncode == 0, search.stderr or search.stdout
|
|
assert root_url in search.stdout
|
|
finally:
|
|
if update_proc is not None:
|
|
stop_archivebox_process(update_proc, signal.SIGTERM)
|
|
if server is not None:
|
|
stop_archivebox_process(server, signal.SIGTERM)
|
|
kill_processes_for_data_dir(tmp_path)
|
|
assert_no_processes_for_data_dir(tmp_path, timeout=12)
|
|
|
|
|
|
@pytest.mark.timeout(300)
|
|
def test_behavior_foreground_add_keeps_existing_server_http_visible(tmp_path, initialized_archive, recursive_test_site):
|
|
|
|
port = get_free_port()
|
|
env = cli_env(live=True, server=True, port=port, SEARCH_BACKEND_ENGINE="ripgrep")
|
|
server = None
|
|
try:
|
|
server = start_archivebox_server(tmp_path, port=port, log_name="behavior-server-add.log", env=env)
|
|
assert get_http_response(port, host=f"archivebox.localhost:{port}").status_code < 500
|
|
|
|
add = run_archivebox_cmd(
|
|
[
|
|
"add",
|
|
"--depth=0",
|
|
"--plugins=wget",
|
|
recursive_test_site["root_url"],
|
|
],
|
|
cwd=tmp_path,
|
|
env=env,
|
|
timeout=180,
|
|
)
|
|
|
|
assert add.returncode == 0, add.stderr or add.stdout
|
|
assert get_http_response(port, host=f"archivebox.localhost:{port}").status_code < 500
|
|
captured_text = wait_for_snapshot_capture(tmp_path, recursive_test_site["root_url"], timeout=120)
|
|
assert "Root" in captured_text
|
|
finally:
|
|
if server is not None:
|
|
stop_archivebox_process(server, signal.SIGTERM)
|
|
kill_processes_for_data_dir(tmp_path)
|
|
assert_no_processes_for_data_dir(tmp_path, timeout=12)
|
|
|
|
|
|
@pytest.mark.timeout(300)
|
|
def test_behavior_background_add_returns_and_server_archives_visible_url(tmp_path, initialized_archive, recursive_test_site):
|
|
|
|
port = get_free_port()
|
|
env = cli_env(live=True, server=True, port=port, SEARCH_BACKEND_ENGINE="ripgrep")
|
|
server = None
|
|
try:
|
|
server = start_archivebox_server(tmp_path, port=port, log_name="behavior-server-bg-add.log", env=env)
|
|
assert get_http_response(port, host=f"archivebox.localhost:{port}").status_code < 500
|
|
|
|
add = run_archivebox_cmd(
|
|
[
|
|
"add",
|
|
"--bg",
|
|
"--depth=0",
|
|
"--plugins=wget",
|
|
recursive_test_site["root_url"],
|
|
],
|
|
cwd=tmp_path,
|
|
env=env,
|
|
timeout=60,
|
|
)
|
|
|
|
assert add.returncode == 0, add.stderr or add.stdout
|
|
assert "background runner will process" in add.stdout
|
|
assert get_http_response(port, host=f"archivebox.localhost:{port}").status_code < 500
|
|
captured_text = wait_for_snapshot_capture(tmp_path, recursive_test_site["root_url"], timeout=180)
|
|
assert "Root" in captured_text
|
|
finally:
|
|
if server is not None:
|
|
stop_archivebox_process(server, signal.SIGTERM)
|
|
kill_processes_for_data_dir(tmp_path)
|
|
assert_no_processes_for_data_dir(tmp_path, timeout=12)
|
|
|
|
|
|
@pytest.mark.timeout(300)
|
|
def test_behavior_daemonized_server_restarts_cleanly_after_forced_stop(tmp_path, initialized_archive):
|
|
|
|
port = get_free_port()
|
|
env = cli_env(live=True, server=True, port=port)
|
|
try:
|
|
first = start_archivebox_server(tmp_path, port=port, env=env, daemonize=True)
|
|
assert first.returncode == 0, first.stderr or first.stdout
|
|
assert get_http_response(port, host=f"archivebox.localhost:{port}").status_code < 500
|
|
|
|
kill_processes_for_data_dir(tmp_path)
|
|
assert_no_processes_for_data_dir(tmp_path, timeout=12)
|
|
|
|
second = start_archivebox_server(tmp_path, port=port, env=env, daemonize=True)
|
|
assert second.returncode == 0, second.stderr or second.stdout
|
|
assert get_http_response(port, host=f"archivebox.localhost:{port}").status_code < 500
|
|
finally:
|
|
kill_processes_for_data_dir(tmp_path)
|
|
assert_no_processes_for_data_dir(tmp_path, timeout=12)
|
|
|
|
|
|
@pytest.mark.timeout(240)
|
|
def test_live_second_server_takes_over_existing_server_process(tmp_path, initialized_archive):
|
|
|
|
env = cli_env(live=True)
|
|
port = get_free_port()
|
|
first = None
|
|
second = None
|
|
try:
|
|
first = start_archivebox_server(tmp_path, port=port, log_name="server-first.log", env=env)
|
|
first_log = first.log_path
|
|
second = start_archivebox_server(tmp_path, port=port, log_name="server-second.log", env=env)
|
|
second_log = second.log_path
|
|
|
|
assert pid_is_alive(first.pid)
|
|
first_text = first_log.read_text(encoding="utf-8", errors="replace")
|
|
second_text = second_log.read_text(encoding="utf-8", errors="replace")
|
|
assert "A newer archivebox process took over the orchestrator, server" in first_text
|
|
assert "Starting orchestrator, server" in second_text
|
|
|
|
_cmd_result = run_archivebox_cmd(
|
|
["status"],
|
|
cwd=tmp_path,
|
|
env=env,
|
|
timeout=60,
|
|
)
|
|
stdout, stderr, returncode = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
|
assert returncode == 0, stderr or stdout
|
|
|
|
first_resumes = first_log.read_text(encoding="utf-8", errors="replace").count("Other newer archivebox process")
|
|
stop_archivebox_process(second, signal.SIGTERM)
|
|
second = None
|
|
wait_for_log_count(first_log, "Other newer archivebox process", first_resumes + 1, timeout=35)
|
|
assert pid_is_alive(first.pid)
|
|
finally:
|
|
if second is not None:
|
|
stop_archivebox_process(second, signal.SIGTERM)
|
|
if first is not None:
|
|
stop_archivebox_process(first, signal.SIGTERM)
|
|
kill_processes_for_data_dir(tmp_path)
|
|
assert_no_processes_for_data_dir(tmp_path, timeout=12)
|
|
|
|
|
|
@pytest.mark.timeout(180)
|
|
def test_live_update_index_only_does_not_take_over_server_runtime(tmp_path, initialized_archive):
|
|
|
|
env = cli_env(live=True)
|
|
port = get_free_port()
|
|
server = None
|
|
try:
|
|
server = start_archivebox_server(tmp_path, port=port, log_name="server-update-owner.log", env=env)
|
|
server_log = server.log_path
|
|
supervisor_pid_before = supervisor_pid_from_log(server_log)
|
|
daphne_pid_before = worker_pid_from_log(server_log, "worker_daphne")
|
|
|
|
_cmd_result = run_archivebox_cmd(
|
|
["update", "--index-only", "--before=0"],
|
|
cwd=tmp_path,
|
|
env=env,
|
|
timeout=90,
|
|
)
|
|
update_stdout, update_stderr, update_returncode = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
|
|
|
assert update_returncode == 0, update_stderr or update_stdout
|
|
assert pid_is_alive(server.pid)
|
|
assert pid_is_alive(supervisor_pid_before)
|
|
assert pid_is_alive(daphne_pid_before)
|
|
assert supervisor_pid_from_log(server_log) == supervisor_pid_before
|
|
assert worker_pid_from_log(server_log, "worker_daphne") == daphne_pid_before
|
|
assert "A newer archivebox process took over the orchestrator, server" not in server_log.read_text(
|
|
encoding="utf-8",
|
|
errors="replace",
|
|
)
|
|
finally:
|
|
if server is not None:
|
|
stop_archivebox_process(server, signal.SIGTERM)
|
|
kill_processes_for_data_dir(tmp_path)
|
|
assert_no_processes_for_data_dir(tmp_path, timeout=12)
|
|
|
|
|
|
@pytest.mark.timeout(360)
|
|
def test_live_server_keeps_http_runtime_while_update_runs_real_sqlite_indexer(tmp_path, initialized_archive, recursive_test_site):
|
|
env = cli_env(
|
|
live=True,
|
|
PLUGINS="wget,parse_html_urls,search_backend_sqlite,search_backend_sonic",
|
|
SEARCH_BACKEND_ENGINE="sqlite",
|
|
SEARCH_BACKEND_SONIC_PORT=str(get_free_port()),
|
|
)
|
|
env.update(_resolve_sonic_env(env))
|
|
_archive_pages_for_sqlite_reindexing(tmp_path, env, recursive_test_site["root_url"])
|
|
|
|
port = get_free_port()
|
|
server = None
|
|
try:
|
|
server = start_archivebox_server(tmp_path, port=port, log_name="server-real-sqlite-update.log", env=env)
|
|
server_log = server.log_path
|
|
supervisor_pid_before = supervisor_pid_from_log(server_log)
|
|
daphne_pid_before = worker_pid_from_log(server_log, "worker_daphne")
|
|
runner_pid_before = worker_pid_from_log(server_log, "worker_runner")
|
|
sonic_pid_before = worker_pid_from_log(server_log, "worker_sonic")
|
|
supervisord_log = tmp_path / "logs" / "supervisord.log"
|
|
runner_spawn_text = "spawned: 'worker_runner' with pid"
|
|
runner_spawn_count = supervisord_log.read_text(encoding="utf-8", errors="replace").count(runner_spawn_text)
|
|
|
|
_cmd_result = run_archivebox_cmd(
|
|
["update", "--index-only", "--batch-size=1"],
|
|
cwd=tmp_path,
|
|
env=env,
|
|
timeout=180,
|
|
)
|
|
update_stdout, update_stderr, update_returncode = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
|
|
|
assert update_returncode == 0, update_stderr or update_stdout
|
|
assert pid_is_alive(server.pid)
|
|
assert supervisor_pid_from_log(server_log) == supervisor_pid_before
|
|
assert pid_is_alive(daphne_pid_before)
|
|
assert pid_is_alive(sonic_pid_before)
|
|
assert worker_pid_from_log(server_log, "worker_daphne") == daphne_pid_before
|
|
assert worker_pid_from_log(server_log, "worker_sonic") == sonic_pid_before
|
|
assert "A newer archivebox process took over the orchestrator, server" not in server_log.read_text(
|
|
encoding="utf-8",
|
|
errors="replace",
|
|
)
|
|
assert "Stopping older ArchiveBox runner process" in update_stdout
|
|
|
|
supervisord_text = wait_for_log_count(supervisord_log, runner_spawn_text, runner_spawn_count + 1, timeout=30)
|
|
runner_pid_after = int(re.findall(r"spawned: 'worker_runner' with pid (\d+)", supervisord_text)[-1])
|
|
assert runner_pid_after != runner_pid_before
|
|
assert pid_is_alive(runner_pid_after)
|
|
|
|
with use_archivebox_db(tmp_path):
|
|
indexed_results = list(
|
|
ArchiveResult.objects.filter(plugin="search_backend_sqlite").values_list("status", flat=True),
|
|
)
|
|
assert indexed_results
|
|
assert all(status in ArchiveResult.FINAL_STATES for status in indexed_results)
|
|
|
|
stop_archivebox_process(server, signal.SIGTERM)
|
|
server = None
|
|
wait_for_pid_to_disappear(daphne_pid_before, timeout=20)
|
|
wait_for_pid_to_disappear(sonic_pid_before, timeout=20)
|
|
wait_for_pid_to_disappear(runner_pid_after, timeout=20)
|
|
assert_no_processes_for_data_dir(tmp_path, timeout=12)
|
|
finally:
|
|
if server is not None:
|
|
stop_archivebox_process(server, signal.SIGTERM)
|
|
kill_processes_for_data_dir(tmp_path)
|
|
assert_no_processes_for_data_dir(tmp_path, timeout=12)
|
|
|
|
|
|
@pytest.mark.timeout(420)
|
|
def test_live_update_yields_to_server_then_reclaims_real_sqlite_indexing(tmp_path, initialized_archive, recursive_test_site):
|
|
env = cli_env(
|
|
live=True,
|
|
PLUGINS="wget,parse_html_urls,search_backend_sqlite,search_backend_sonic",
|
|
SEARCH_BACKEND_ENGINE="sqlite",
|
|
SEARCH_BACKEND_SONIC_PORT=str(get_free_port()),
|
|
)
|
|
env.update(_resolve_sonic_env(env))
|
|
_archive_pages_for_sqlite_reindexing(tmp_path, env, recursive_test_site["root_url"])
|
|
|
|
port = get_free_port()
|
|
update_proc = None
|
|
server = None
|
|
try:
|
|
update_log = tmp_path / "update-real-sqlite-owner.log"
|
|
update_log_handle = update_log.open("w", encoding="utf-8")
|
|
update_proc = run_archivebox_cmd(
|
|
["update", "--index-only", "--batch-size=1"],
|
|
cwd=tmp_path,
|
|
env=env,
|
|
stdout=update_log_handle,
|
|
stderr=subprocess.STDOUT,
|
|
start_new_session=True,
|
|
wait=False,
|
|
)
|
|
update_log_handle.close()
|
|
wait_for_log(update_log, "[*] Reindexing", timeout=90)
|
|
update_supervisor_match = wait_for_log_pattern(update_log, r"Supervisord connected \(pid=(\d+)\)", timeout=90)
|
|
update_supervisor_pid_before = int(update_supervisor_match.group(1))
|
|
update_sonic_pid_before = wait_for_worker_pid_from_log(update_log, "worker_sonic", timeout=90)
|
|
update_runner_pid_before = wait_for_worker_pid_from_log(update_log, f"worker_runner_update_{update_proc.pid}", timeout=90)
|
|
assert pid_is_alive(update_supervisor_pid_before)
|
|
assert pid_is_alive(update_sonic_pid_before)
|
|
assert pid_is_alive(update_runner_pid_before)
|
|
assert "worker_daphne" not in update_log.read_text(encoding="utf-8", errors="replace")
|
|
|
|
server = start_archivebox_server(tmp_path, port=port, log_name="server-takes-real-sqlite-update.log", env=env)
|
|
server_log = server.log_path
|
|
wait_for_log(update_log, "A newer archivebox process took over the orchestrator, sonic", timeout=90)
|
|
assert pid_is_alive(update_proc.pid)
|
|
assert pid_is_alive(server.pid)
|
|
server_text = server_log.read_text(encoding="utf-8", errors="replace")
|
|
# The older update process can yield orchestrator ownership just before
|
|
# the server logs its takeover, but sonic must always move to the server.
|
|
assert (
|
|
"Taking over orchestrator, sonic from older existing archivebox process" in server_text
|
|
or "Taking over sonic from older existing archivebox process" in server_text
|
|
)
|
|
assert "worker_daphne" in server_text
|
|
assert "worker_sonic" in server_text
|
|
server_daphne_pid = worker_pid_from_log(server_log, "worker_daphne")
|
|
server_runner_pid = worker_pid_from_log(server_log, "worker_runner")
|
|
server_sonic_pid = worker_pid_from_log(server_log, "worker_sonic")
|
|
assert pid_is_alive(server_daphne_pid)
|
|
assert pid_is_alive(server_runner_pid)
|
|
assert pid_is_alive(server_sonic_pid)
|
|
wait_for_pid_to_disappear(update_supervisor_pid_before, timeout=30)
|
|
wait_for_pid_to_disappear(update_sonic_pid_before, timeout=30)
|
|
wait_for_pid_to_disappear(update_runner_pid_before, timeout=30)
|
|
|
|
stop_archivebox_process(server, signal.SIGTERM)
|
|
server = None
|
|
update_proc.wait(timeout=180)
|
|
update_text = update_log.read_text(encoding="utf-8", errors="replace")
|
|
assert update_proc.returncode == 0, update_text
|
|
wait_for_pid_to_disappear(server_daphne_pid, timeout=20)
|
|
wait_for_pid_to_disappear(server_runner_pid, timeout=20)
|
|
wait_for_pid_to_disappear(server_sonic_pid, timeout=20)
|
|
|
|
with use_archivebox_db(tmp_path):
|
|
indexed_results = list(
|
|
ArchiveResult.objects.filter(plugin="search_backend_sqlite").values_list("status", flat=True),
|
|
)
|
|
assert indexed_results
|
|
assert all(status in ArchiveResult.FINAL_STATES for status in indexed_results)
|
|
assert_no_processes_for_data_dir(tmp_path, timeout=12)
|
|
finally:
|
|
if update_proc is not None:
|
|
stop_archivebox_process(update_proc, signal.SIGTERM)
|
|
if server is not None:
|
|
stop_archivebox_process(server, signal.SIGTERM)
|
|
kill_processes_for_data_dir(tmp_path)
|
|
assert_no_processes_for_data_dir(tmp_path, timeout=12)
|
|
|
|
|
|
@pytest.mark.timeout(420)
|
|
def test_live_repeated_server_startups_take_over_cleanly(tmp_path, initialized_archive):
|
|
|
|
env = cli_env(live=True)
|
|
port = get_free_port()
|
|
servers: list[subprocess.Popen[str]] = []
|
|
server_pids: list[int] = []
|
|
daphne_pids: list[int] = []
|
|
runner_pids: list[int] = []
|
|
try:
|
|
for index in range(5):
|
|
server = start_archivebox_server(tmp_path, port=port, log_name=f"server-chaos-{index}.log", env=env)
|
|
log_path = server.log_path
|
|
servers.append(server)
|
|
server_pids.append(server.pid)
|
|
daphne_pids.append(worker_pid_from_log(log_path, "worker_daphne"))
|
|
runner_pids.append(worker_pid_from_log(log_path, "worker_runner"))
|
|
|
|
if index > 0:
|
|
previous_server = servers[index - 1]
|
|
previous_log = (tmp_path / f"server-chaos-{index - 1}.log").read_text(encoding="utf-8", errors="replace")
|
|
current_log = log_path.read_text(encoding="utf-8", errors="replace")
|
|
assert pid_is_alive(previous_server.pid)
|
|
assert pid_is_alive(server_pids[index - 1])
|
|
assert "A newer archivebox process took over the orchestrator, server" in previous_log
|
|
assert "Starting orchestrator, server" in current_log
|
|
wait_for_pid_to_disappear(daphne_pids[index - 1], timeout=15)
|
|
wait_for_pid_to_disappear(runner_pids[index - 1], timeout=15)
|
|
|
|
_cmd_result = run_archivebox_cmd(
|
|
["status"],
|
|
cwd=tmp_path,
|
|
env=env,
|
|
timeout=60,
|
|
)
|
|
stdout, stderr, returncode = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
|
assert returncode == 0, stderr or stdout
|
|
|
|
assert pid_is_alive(servers[-1].pid)
|
|
assert all(pid_is_alive(server.pid) for server in servers)
|
|
listener = subprocess.run(
|
|
["lsof", "-nP", f"-iTCP:{port}", "-sTCP:LISTEN"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
)
|
|
assert listener.returncode == 0, listener.stderr or listener.stdout
|
|
assert listener.stdout.count(f":{port} (LISTEN)") == 1
|
|
|
|
previous_log_path = tmp_path / "server-chaos-3.log"
|
|
previous_takeovers = previous_log_path.read_text(encoding="utf-8", errors="replace").count(
|
|
"Other newer archivebox process",
|
|
)
|
|
stop_archivebox_process(servers[-1], signal.SIGTERM)
|
|
wait_for_log_count(previous_log_path, "Other newer archivebox process", previous_takeovers + 1, timeout=35)
|
|
assert pid_is_alive(servers[3].pid)
|
|
finally:
|
|
for server in reversed(servers):
|
|
stop_archivebox_process(server, signal.SIGTERM)
|
|
kill_processes_for_data_dir(tmp_path)
|
|
assert_no_processes_for_data_dir(tmp_path, timeout=12)
|
|
|
|
|
|
@pytest.mark.timeout(420)
|
|
def test_live_background_add_survives_server_exit_and_foreground_run_reclaims(tmp_path, initialized_archive, recursive_test_site):
|
|
env = cli_env(live=True, SEARCH_BACKEND_ENGINE="ripgrep")
|
|
port = get_free_port()
|
|
server = None
|
|
server2 = None
|
|
try:
|
|
server = start_archivebox_server(tmp_path, port=port, log_name="server-add-owner-1.log", env=env)
|
|
server_log = server.log_path
|
|
supervisor_pid_before = supervisor_pid_from_log(server_log)
|
|
|
|
_cmd_result = run_archivebox_cmd(
|
|
["update", "--index-only", "--batch-size=10"],
|
|
cwd=tmp_path,
|
|
env=env,
|
|
timeout=90,
|
|
)
|
|
update_stdout, update_stderr, update_returncode = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
|
assert update_returncode == 0, update_stderr or update_stdout
|
|
assert pid_is_alive(server.pid)
|
|
assert pid_is_alive(supervisor_pid_before)
|
|
assert supervisor_pid_from_log(server_log) == supervisor_pid_before
|
|
|
|
add = run_archivebox_cmd(
|
|
[
|
|
"add",
|
|
"--bg",
|
|
"--depth=1",
|
|
"--max-urls=20",
|
|
"--crawl-max-size=50mb",
|
|
"--plugins=wget,parse_html_urls",
|
|
recursive_test_site["root_url"],
|
|
],
|
|
cwd=tmp_path,
|
|
env=env,
|
|
timeout=60,
|
|
)
|
|
assert add.returncode == 0, add.stderr or add.stdout
|
|
assert "background runner will process" in add.stdout
|
|
|
|
stop_archivebox_process(server, signal.SIGTERM)
|
|
server = None
|
|
assert "Got SIGTERM" in server_log.read_text(encoding="utf-8", errors="replace")
|
|
assert_no_processes_for_data_dir(tmp_path, timeout=12)
|
|
|
|
with use_archivebox_db(tmp_path):
|
|
crawl_id = str(Crawl.objects.get().id)
|
|
run = run_archivebox_cmd(
|
|
["run", f"--crawl-id={crawl_id}"],
|
|
cwd=tmp_path,
|
|
env=env,
|
|
timeout=180,
|
|
)
|
|
assert run.returncode == 0, run.stderr or run.stdout
|
|
|
|
server2 = start_archivebox_server(tmp_path, port=port, log_name="server-add-owner-2.log", env=env)
|
|
assert get_http_response(port, host=f"archivebox.localhost:{port}").status_code < 500
|
|
captured_text = wait_for_snapshot_capture(tmp_path, recursive_test_site["root_url"], timeout=180)
|
|
assert "Root" in captured_text
|
|
stop_archivebox_process(server2, signal.SIGTERM)
|
|
server2 = None
|
|
with use_archivebox_db(tmp_path):
|
|
crawls = list(Crawl.objects.order_by("created_at").values_list("status", "retry_at"))
|
|
snapshots = list(Snapshot.objects.order_by("created_at").values_list("url", "status", "retry_at"))
|
|
bad_results = list(
|
|
ArchiveResult.objects.filter(
|
|
status__in=[
|
|
ArchiveResult.StatusChoices.FAILED,
|
|
ArchiveResult.StatusChoices.SKIPPED,
|
|
],
|
|
).values_list("plugin", "status", "output_str"),
|
|
)
|
|
assert crawls
|
|
assert snapshots
|
|
assert all(status == Crawl.StatusChoices.SEALED for status, _retry_at in crawls)
|
|
assert all(status == Snapshot.StatusChoices.SEALED for _url, status, _retry_at in snapshots)
|
|
assert not bad_results
|
|
finally:
|
|
for proc in (server, server2):
|
|
if proc is not None:
|
|
stop_archivebox_process(proc, signal.SIGTERM, timeout=10)
|
|
kill_processes_for_data_dir(tmp_path)
|
|
assert_no_processes_for_data_dir(tmp_path, timeout=12)
|
|
|
|
|
|
# Utility-level takeover selection tests.
|
|
|
|
|
|
def _start_archivebox_shell(tmp_path: Path):
|
|
process = run_archivebox_cmd(
|
|
["manage", "shell"],
|
|
cwd=tmp_path,
|
|
env=cli_env(live=True),
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
capture_output=False,
|
|
start_new_session=True,
|
|
wait=False,
|
|
)
|
|
assert process.pid is not None
|
|
assert process.stdin is not None
|
|
assert pid_is_alive(process.pid)
|
|
return process
|
|
|
|
|
|
def _stop_archivebox_shells(processes) -> None:
|
|
for process in processes:
|
|
if process.stdin is not None and not process.stdin.closed:
|
|
process.stdin.close()
|
|
for process in processes:
|
|
process.wait(timeout=20)
|
|
assert not pid_is_alive(process.pid)
|
|
|
|
|
|
def test_runtime_stack_owner_prefers_newer_server_over_older_update(tmp_path, initialized_archive):
|
|
from archivebox.machine.models import Machine
|
|
from archivebox.core.takeover_util import runtime_stack_owner
|
|
|
|
procs: list[subprocess.Popen[str]] = []
|
|
try:
|
|
for process_type in (Process.TypeChoices.UPDATE, Process.TypeChoices.SERVER):
|
|
proc = _start_archivebox_shell(tmp_path)
|
|
procs.append(proc)
|
|
Process.objects.create(
|
|
machine=Machine.current(),
|
|
process_type=process_type,
|
|
worker_type=process_type,
|
|
pwd=str(tmp_path),
|
|
cmd=[],
|
|
pid=proc.pid,
|
|
status=Process.StatusChoices.RUNNING,
|
|
)
|
|
|
|
owner = runtime_stack_owner(data_dir=tmp_path)
|
|
|
|
assert owner is not None
|
|
assert owner.process_type == Process.TypeChoices.SERVER
|
|
assert owner.pid == procs[-1].pid
|
|
finally:
|
|
_stop_archivebox_shells(procs)
|
|
|
|
|
|
def test_runtime_stack_owner_keeps_server_over_newer_update(tmp_path, initialized_archive):
|
|
from archivebox.machine.models import Machine
|
|
from archivebox.core.takeover_util import runtime_stack_owner
|
|
|
|
procs: list[subprocess.Popen[str]] = []
|
|
try:
|
|
for process_type in (Process.TypeChoices.SERVER, Process.TypeChoices.UPDATE):
|
|
proc = _start_archivebox_shell(tmp_path)
|
|
procs.append(proc)
|
|
Process.objects.create(
|
|
machine=Machine.current(),
|
|
process_type=process_type,
|
|
worker_type=process_type,
|
|
pwd=str(tmp_path),
|
|
cmd=[],
|
|
pid=proc.pid,
|
|
status=Process.StatusChoices.RUNNING,
|
|
)
|
|
|
|
owner = runtime_stack_owner(data_dir=tmp_path)
|
|
|
|
assert owner is not None
|
|
assert owner.process_type == Process.TypeChoices.SERVER
|
|
assert owner.pid == procs[0].pid
|
|
finally:
|
|
_stop_archivebox_shells(procs)
|
|
|
|
|
|
def test_foreground_runner_owner_prefers_newer_update_over_server(tmp_path, initialized_archive):
|
|
from archivebox.machine.models import Machine
|
|
from archivebox.core.takeover_util import foreground_runner_owner, runtime_stack_owner
|
|
|
|
procs: list[subprocess.Popen[str]] = []
|
|
try:
|
|
for process_type in (Process.TypeChoices.SERVER, Process.TypeChoices.UPDATE):
|
|
proc = _start_archivebox_shell(tmp_path)
|
|
procs.append(proc)
|
|
Process.objects.create(
|
|
machine=Machine.current(),
|
|
process_type=process_type,
|
|
worker_type=process_type,
|
|
pwd=str(tmp_path),
|
|
cmd=[],
|
|
pid=proc.pid,
|
|
status=Process.StatusChoices.RUNNING,
|
|
)
|
|
|
|
runtime_owner = runtime_stack_owner(data_dir=tmp_path)
|
|
runner_owner = foreground_runner_owner(data_dir=tmp_path)
|
|
|
|
assert runtime_owner is not None
|
|
assert runtime_owner.process_type == Process.TypeChoices.SERVER
|
|
assert runner_owner is not None
|
|
assert runner_owner.process_type == Process.TypeChoices.UPDATE
|
|
assert runner_owner.pid == procs[-1].pid
|
|
finally:
|
|
_stop_archivebox_shells(procs)
|
|
|
|
|
|
def test_foreground_runner_owner_prefers_newer_server_over_update(tmp_path, initialized_archive):
|
|
from archivebox.machine.models import Machine
|
|
from archivebox.core.takeover_util import foreground_runner_owner, runtime_stack_owner
|
|
|
|
procs: list[subprocess.Popen[str]] = []
|
|
try:
|
|
for process_type in (Process.TypeChoices.UPDATE, Process.TypeChoices.SERVER):
|
|
proc = _start_archivebox_shell(tmp_path)
|
|
procs.append(proc)
|
|
Process.objects.create(
|
|
machine=Machine.current(),
|
|
process_type=process_type,
|
|
worker_type=process_type,
|
|
pwd=str(tmp_path),
|
|
cmd=[],
|
|
pid=proc.pid,
|
|
status=Process.StatusChoices.RUNNING,
|
|
)
|
|
|
|
runtime_owner = runtime_stack_owner(data_dir=tmp_path)
|
|
runner_owner = foreground_runner_owner(data_dir=tmp_path)
|
|
|
|
assert runtime_owner is not None
|
|
assert runtime_owner.process_type == Process.TypeChoices.SERVER
|
|
assert runner_owner is not None
|
|
assert runner_owner.process_type == Process.TypeChoices.SERVER
|
|
assert runner_owner.pid == procs[-1].pid
|
|
finally:
|
|
_stop_archivebox_shells(procs)
|
|
|
|
|
|
def test_runtime_stack_owner_keeps_server_over_newer_supervised_runner(tmp_path, initialized_archive):
|
|
from archivebox.machine.models import Machine
|
|
from archivebox.core.takeover_util import RUNNER_ACTIVE_WORKER_TYPE, runtime_stack_owner
|
|
|
|
procs: list[subprocess.Popen[str]] = []
|
|
try:
|
|
for process_type, worker_type in (
|
|
(Process.TypeChoices.SERVER, ""),
|
|
(Process.TypeChoices.ORCHESTRATOR, RUNNER_ACTIVE_WORKER_TYPE),
|
|
):
|
|
proc = _start_archivebox_shell(tmp_path)
|
|
procs.append(proc)
|
|
Process.objects.create(
|
|
machine=Machine.current(),
|
|
process_type=process_type,
|
|
worker_type=worker_type,
|
|
pwd=str(tmp_path),
|
|
cmd=[],
|
|
pid=proc.pid,
|
|
status=Process.StatusChoices.RUNNING,
|
|
)
|
|
|
|
owner = runtime_stack_owner(data_dir=tmp_path)
|
|
|
|
assert owner is not None
|
|
assert owner.process_type == Process.TypeChoices.SERVER
|
|
assert owner.pid == procs[0].pid
|
|
finally:
|
|
_stop_archivebox_shells(procs)
|
|
|
|
|
|
def test_runtime_stack_owner_reaps_dead_server_without_promoting_update(tmp_path, initialized_archive):
|
|
from archivebox.machine.models import Machine
|
|
from archivebox.core.takeover_util import runtime_stack_owner
|
|
|
|
procs: list[subprocess.Popen[str]] = []
|
|
older_row = None
|
|
newer_row = None
|
|
try:
|
|
for process_type in (Process.TypeChoices.UPDATE, Process.TypeChoices.SERVER):
|
|
proc = _start_archivebox_shell(tmp_path)
|
|
procs.append(proc)
|
|
row = Process.objects.create(
|
|
machine=Machine.current(),
|
|
process_type=process_type,
|
|
worker_type=process_type,
|
|
pwd=str(tmp_path),
|
|
cmd=[],
|
|
pid=proc.pid,
|
|
status=Process.StatusChoices.RUNNING,
|
|
)
|
|
if process_type == Process.TypeChoices.UPDATE:
|
|
older_row = row
|
|
else:
|
|
newer_row = row
|
|
|
|
assert older_row is not None
|
|
assert newer_row is not None
|
|
|
|
os.kill(procs[-1].pid, signal.SIGTERM)
|
|
procs[-1].wait(timeout=20)
|
|
|
|
owner = runtime_stack_owner(data_dir=tmp_path)
|
|
|
|
assert owner is None
|
|
newer_row.refresh_from_db()
|
|
assert newer_row.status == Process.StatusChoices.EXITED
|
|
older_row.refresh_from_db()
|
|
assert older_row.status == Process.StatusChoices.RUNNING
|
|
finally:
|
|
_stop_archivebox_shells(procs)
|
|
|
|
|
|
def test_runtime_stack_owner_ignores_supervised_orphan_runner(tmp_path, initialized_archive):
|
|
from archivebox.machine.models import Machine
|
|
from archivebox.core.takeover_util import RUNNER_ACTIVE_WORKER_TYPE, runtime_stack_owner
|
|
|
|
procs: list[subprocess.Popen[str]] = []
|
|
try:
|
|
for _ in range(2):
|
|
proc = _start_archivebox_shell(tmp_path)
|
|
procs.append(proc)
|
|
|
|
supervisor_row = Process.objects.create(
|
|
machine=Machine.current(),
|
|
process_type=Process.TypeChoices.SUPERVISORD,
|
|
worker_type="supervisord",
|
|
pwd=str(tmp_path),
|
|
cmd=[],
|
|
pid=procs[0].pid,
|
|
status=Process.StatusChoices.RUNNING,
|
|
)
|
|
Process.objects.create(
|
|
machine=Machine.current(),
|
|
parent=supervisor_row,
|
|
process_type=Process.TypeChoices.ORCHESTRATOR,
|
|
worker_type=RUNNER_ACTIVE_WORKER_TYPE,
|
|
pwd=str(tmp_path),
|
|
cmd=[],
|
|
pid=procs[1].pid,
|
|
status=Process.StatusChoices.RUNNING,
|
|
)
|
|
|
|
assert runtime_stack_owner(data_dir=tmp_path) is None
|
|
finally:
|
|
_stop_archivebox_shells(procs)
|
|
|
|
|
|
def test_runtime_stack_owner_allows_top_level_runner_when_no_parent_command_exists(tmp_path, initialized_archive):
|
|
from archivebox.machine.models import Machine
|
|
from archivebox.core.takeover_util import RUNNER_ACTIVE_WORKER_TYPE, runtime_stack_owner
|
|
|
|
proc = _start_archivebox_shell(tmp_path)
|
|
try:
|
|
runner_row = Process.objects.create(
|
|
machine=Machine.current(),
|
|
process_type=Process.TypeChoices.ORCHESTRATOR,
|
|
worker_type=RUNNER_ACTIVE_WORKER_TYPE,
|
|
pwd=str(tmp_path),
|
|
cmd=[],
|
|
pid=proc.pid,
|
|
status=Process.StatusChoices.RUNNING,
|
|
)
|
|
|
|
owner = runtime_stack_owner(data_dir=tmp_path)
|
|
|
|
assert owner is not None
|
|
assert owner.id == runner_row.id
|
|
finally:
|
|
_stop_archivebox_shells([proc])
|