feat(coordinator): dispatch versioned runtime leases and reports

This commit is contained in:
2026-10-09 15:18:08 +08:00
parent 4b8f255180
commit 53caafc157
10 changed files with 744 additions and 85 deletions
+10 -5
View File
@@ -27,8 +27,10 @@ ROOT = Path(__file__).resolve().parents[2]
@asynccontextmanager
async def fixture(postgres_url, tmp_path, monkeypatch):
_, policy = import_fixture(tmp_path / "import")
async def fixture(
postgres_url, tmp_path, monkeypatch, *, import_builder=import_fixture, node_policy=None
):
_, policy = import_builder(tmp_path / "import")
engine = create_async_engine(postgres_url)
platform_role(engine)
monkeypatch.setattr("kf_oj.api.app.build_engine", lambda _: engine)
@@ -66,7 +68,7 @@ async def fixture(postgres_url, tmp_path, monkeypatch):
database_url=coord_url,
workers=workers,
platform_token=platform,
admission=policy.model_dump(mode="json"),
admission=(node_policy or policy).model_dump(mode="json"),
lease_seconds=5,
recovery_seconds=1,
)
@@ -134,17 +136,20 @@ async def fixture(postgres_url, tmp_path, monkeypatch):
"cpu.max": "50000 100000",
}
async def create():
async def create(language="c17"):
return await app.state.submissions.create(
owner,
CreateSubmission(
problem_version_id=version,
language="c17",
language=language,
idempotency_key=uuid4(),
source="synthetic report fixture; never compiled on host",
),
)
client.fixture_app = app
client.fixture_account = owner
client.fixture_version = version
client.coordinator_unit = unit
client.coordinator_role = role
yield client, c, workers, platform, create
@@ -220,3 +220,18 @@ def test_registry_is_explicit_for_each_protocol_and_retains_legacy_configuration
raw["multi_runtime"]["prefetch"] = 2
with pytest.raises(ValueError):
Settings(_env_file=None, judge_admission=raw)
def test_generic_admission_revalidates_constructed_values_in_both_generations():
registry = fixture("judge-registry.valid", v2.AdmissionRegistry)
old = ROOT / "tests/fixtures/contracts/judge-manifest.valid.json"
legacy = v1.decode_wire(v1.JobManifest, old.read_bytes())
for manifest in (legacy, fixture("judge-manifest.valid-python313", v2.JobManifest)):
for wrong in (
manifest.model_copy(update={"schema_version": True}),
manifest.model_copy(update={"job_wall_time_us": 1}),
manifest.model_copy(update={"cases": ()}),
):
with pytest.raises(ValueError):
v2.admit_manifest(registry, wrong, 1)
v2.admit_manifest(registry, legacy, 1)
@@ -0,0 +1,241 @@
"""Actual finite Rust Coordinator/PG control acceptance; reports are synthetic, no execution."""
import asyncio
import copy
import hashlib
import json
from uuid import UUID, uuid4
import pytest
from test_coordinator import claim, fixture, manifest, notice, synthetic_report, worker_headers
from test_multi_runtime_versions import multi_import
from kf_oj.contracts import judging_v2 as v2
from kf_oj.contracts import judging_versions as versions
def report_v2(grant, task):
result = synthetic_report(grant, task)
result["schema_version"] = 2
language = v2.Language(task["language"])
kind = v2.runtime_contract(language)[3]
if kind == v2.ProgramKind.PYTHON:
files = [dict(path="main.py", artifact=task["source"])]
elif kind == v2.ProgramKind.JAVA:
files = [
dict(path=name, artifact=dict(digest="d" * 64, bytes=256))
for name in ("Helper.class", "Main$Inner.class", "Main.class")
]
else:
files = [dict(path="main", artifact=dict(digest="d" * 64, bytes=4096))]
result["compilation"]["program"] = dict(
kind=kind.value,
binding=grant["binding"],
source=task["source"],
toolchain=task["toolchain"],
files=files,
)
return result
def stage_v2(grant, language, sequence, stage="compiling", index=None):
return dict(
schema_version=2,
kind="judge.stage",
binding=grant["binding"],
sequence=sequence,
stage=stage,
case_index=index,
preparation_kind=v2.runtime_contract(v2.Language(language))[4].value,
)
def test_actual_versioned_claim_canonical_snapshot_stage_and_report(
postgres_url, tmp_path, monkeypatch
):
async def run():
async with fixture(postgres_url, tmp_path, monkeypatch, import_builder=multi_import) as (
client,
c,
workers,
_,
create,
):
for language in v2.Language:
created = await create(language.value)
grant = await claim(client, created, workers[0])
assert grant["schema_version"] == 2
task = await manifest(c, grant)
assert task["schema_version"] == 2 and task["language"] == language.value
parsed = versions.decode_manifest(json.dumps(task).encode())
canonical = versions.manifest_bytes(parsed)
assert hashlib.sha256(canonical).hexdigest() == grant["manifest_digest"]
async with client.fixture_app.state.sessions() as s:
version = await client.fixture_app.state.problems.for_submission(
s, created.problem_version_id
)
expected = version.manifest(
created.evaluation_id, created.submission_id, language, parsed.source
)
assert versions.manifest_bytes(expected) == canonical
headers = worker_headers(workers[0], grant)
bad = stage_v2(grant, language, 1)
bad["preparation_kind"] = (
"compile" if language == v2.Language.PYTHON313 else "syntax_check"
)
assert (
await client.post("/internal/stage", json=bad, headers=headers)
).status_code == 422
old = stage_v2(grant, language, 1)
old.update(schema_version=1)
old.pop("preparation_kind")
assert (
await client.post("/internal/stage", json=old, headers=headers)
).status_code == 422
for sequence, phase, index in (
(1, "compiling", None),
(2, "running", 0),
(3, "running", 1),
):
stage = stage_v2(grant, language, sequence, phase, index)
assert (
await client.post("/internal/stage", json=stage, headers=headers)
).status_code == 204
assert (
await client.post("/internal/stage", json=stage, headers=headers)
).status_code == 204
result = report_v2(grant, task)
for invalid in (
result | {"manifest_digest": "f" * 64},
result | {"binding": grant["binding"] | {"fencing_version": 2}},
):
assert (
await client.post("/internal/report", json=invalid, headers=headers)
).status_code in (409, 422)
changed = copy.deepcopy(result)
changed["compilation"]["program"]["toolchain"]["image_digest"] = "e" * 64
assert (
await client.post("/internal/report", json=changed, headers=headers)
).status_code == 422
assert (
await client.post("/internal/report", json=result, headers=headers)
).status_code == 204
assert (
await client.post("/internal/report", json=result, headers=headers)
).status_code == 204
final = await c.fetchval(
"SELECT final_report FROM evaluations WHERE id=$1", created.evaluation_id
)
assert json.loads(final) == result
assert (
await c.fetchval(
"SELECT state FROM evaluations WHERE id=$1", created.evaluation_id
)
== "finished"
)
assert (
await c.fetchval(
"SELECT count(*) FROM case_results WHERE evaluation_id=$1",
created.evaluation_id,
)
== 2
)
# No real program/Worker/Root executed; accepted is a synthetic protocol value only.
asyncio.run(run())
@pytest.mark.parametrize("kind", ["legacy", "identity", "budget"])
def test_actual_claim_refuses_missing_protocol_or_changed_capability_without_attempt(
postgres_url,
tmp_path,
monkeypatch,
kind,
):
from pathlib import Path
from kf_oj.contracts.judging import decode_wire
fixtures = Path(__file__).resolve().parents[2] / "tests/fixtures/contracts-v2"
registry = decode_wire(
v2.AdmissionRegistry, (fixtures / "judge-registry.valid.json").read_bytes()
)
raw = registry.model_dump(mode="json")
if kind == "identity":
raw["multi_runtime"]["capabilities"][2]["toolchain"]["image_digest"] = "e" * 64
elif kind == "budget":
raw["multi_runtime"]["capabilities"][2]["run_ceiling"]["memory_bytes"] = 32 * 1024 * 1024
policy = registry.legacy if kind == "legacy" else v2.AdmissionRegistry.model_validate(raw)
async def run():
async with fixture(
postgres_url,
tmp_path,
monkeypatch,
import_builder=multi_import,
node_policy=policy,
) as (client, c, workers, _, create):
created = await create("python313")
response = await client.post(
"/internal/claim",
json=notice(created.evaluation_id),
headers=worker_headers(workers[0]),
)
assert response.status_code == 422
assert await c.fetchval("SELECT count(*) FROM judge_attempts") == 0
assert await c.fetchval("SELECT state FROM evaluations") == "queued"
assert (await client.get("/health/ready")).status_code == 200
asyncio.run(run())
def test_actual_v2_cancel_requires_matching_stop_protocol_and_fence(
postgres_url, tmp_path, monkeypatch
):
async def run():
async with fixture(postgres_url, tmp_path, monkeypatch, import_builder=multi_import) as (
client,
c,
workers,
platform,
create,
):
created = await create("java21")
grant = await claim(client, created, workers[0])
assert (
await client.post(
"/internal/cancel",
json=notice(created.evaluation_id),
headers={"Authorization": "Bearer " + platform},
)
).status_code == 204
stopped = dict(
schema_version=2,
kind="judge.stopped",
binding=grant["binding"],
cleanup_complete=True,
)
headers = worker_headers(workers[0])
assert (
await client.post(
"/internal/stopped", json=stopped | {"schema_version": 1}, headers=headers
)
).status_code == 422
bad = stopped | {"binding": grant["binding"] | {"attempt_id": str(uuid4())}}
assert (
await client.post("/internal/stopped", json=bad, headers=headers)
).status_code == 409
assert (
await client.post("/internal/stopped", json=stopped, headers=headers)
).status_code == 204
assert await c.fetchval("SELECT state FROM evaluations") == "cancelled"
assert (
await c.fetchval(
"SELECT cleanup_complete FROM judge_attempts WHERE id=$1",
UUID(grant["binding"]["attempt_id"]),
)
is True
)
# This proves stop-report authority only; real JVM cleanup remains VM acceptance.
asyncio.run(run())