144 lines
5.4 KiB
Python
144 lines
5.4 KiB
Python
"""Inactive ARR1 compatibility adapters; never imported by the ARR2 Web runtime."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
import uuid
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any, Dict, Protocol
|
|
|
|
from arr_ingestion.contracts import IngestionError
|
|
from arr_ingestion.direct_contracts import SubmissionGrant
|
|
from arr_ingestion.repository import JobRegistration
|
|
from arr_processing.callbacks import AgentResultWriteback
|
|
from arr_processing.contracts import ProcessingRequest
|
|
from arr_processing.errors import ProcessingError
|
|
from arr_processing.runner import processing_dispatch_idempotency_key
|
|
from arr_storage.store import ManagedObjectStore
|
|
from arr_web.contracts import PortalError, validate_upload_filename, validate_xml_payload
|
|
|
|
|
|
class AgentResultCoordinator(Protocol):
|
|
def accept(self, raw_signed_result: bytes) -> Dict[str, Any]:
|
|
...
|
|
|
|
|
|
class ProcessingStarter(Protocol):
|
|
def start(self, request: ProcessingRequest) -> Any:
|
|
...
|
|
|
|
|
|
class DirectJobRepository(Protocol):
|
|
def register_job(self, registration: JobRegistration) -> None:
|
|
...
|
|
|
|
def issue_grant(
|
|
self, job_id: str, attempt_no: int, *, ttl_seconds: int
|
|
) -> SubmissionGrant:
|
|
...
|
|
|
|
|
|
@dataclass
|
|
class ProcessingAgentResultCoordinator:
|
|
writeback: AgentResultWriteback
|
|
|
|
def accept(self, raw_signed_result: bytes) -> Dict[str, Any]:
|
|
try:
|
|
return self.writeback.accept(raw_signed_result).to_dict()
|
|
except (IngestionError, ProcessingError) as error:
|
|
code = error.code
|
|
if code in {"PROCESSING_SIGNATURE_INVALID", "PROCESSING_SIGNATURE_EXPIRED"}:
|
|
status, message = 401, "Agent 结果签名无效或已过期"
|
|
elif code.endswith("_NOT_FOUND") or code in {
|
|
"JOB_NOT_FOUND",
|
|
"PROCESSING_JOB_NOT_FOUND",
|
|
}:
|
|
status, message = 404, "Agent 结果对应的业务任务不存在"
|
|
elif "CONFLICT" in code or code in {
|
|
"JOB_TERMINAL",
|
|
"PROCESSING_RESULT_MISMATCH",
|
|
}:
|
|
status, message = 409, "Agent 结果与现有业务任务冲突"
|
|
elif getattr(error, "retryable", False) or code.startswith("DATABASE_"):
|
|
status, message = 503, "Agent 结果暂时无法写入,请使用相同回调重试"
|
|
else:
|
|
status, message = 422, "Agent 结果未通过业务验收"
|
|
raise PortalError(code, message, status) from None
|
|
|
|
|
|
@dataclass
|
|
class ObjectStoreUploadCoordinator:
|
|
"""Frozen ARR1 upload/grant/remote-dispatch adapter for compatibility tests."""
|
|
|
|
object_store: ManagedObjectStore
|
|
ingestion_repository: DirectJobRepository
|
|
processing_starter: ProcessingStarter
|
|
processor_version: str
|
|
rule_set_sha256: str
|
|
submission_grant_ttl_seconds: int = 900
|
|
|
|
def submit(self, original_filename: str, payload: bytes) -> Dict[str, Any]:
|
|
uploaded_filename = validate_upload_filename(original_filename)
|
|
validate_xml_payload(payload)
|
|
job_id = "arrjob-" + uuid.uuid4().hex
|
|
attempt_no = 1
|
|
with tempfile.TemporaryDirectory(prefix="arr-web-upload-") as temporary:
|
|
source_path = Path(temporary) / "source.xml"
|
|
descriptor = os.open(source_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
try:
|
|
with os.fdopen(descriptor, "wb") as target:
|
|
descriptor = -1
|
|
target.write(payload)
|
|
target.flush()
|
|
os.fsync(target.fileno())
|
|
finally:
|
|
if descriptor >= 0:
|
|
os.close(descriptor)
|
|
stored = self.object_store.upload_committed(
|
|
job_id=job_id,
|
|
attempt_no=attempt_no,
|
|
role="source_xml",
|
|
source=source_path,
|
|
original_filename="source.xml",
|
|
)
|
|
source_file_id = "arr_file_" + uuid.uuid4().hex
|
|
request_without_grant = ProcessingRequest(
|
|
job_id=job_id,
|
|
attempt_no=attempt_no,
|
|
source_file_id=source_file_id,
|
|
processor_version=self.processor_version,
|
|
rule_set_sha256=self.rule_set_sha256,
|
|
)
|
|
self.ingestion_repository.register_job(
|
|
JobRegistration(
|
|
job_id=job_id,
|
|
source=stored.to_artifact_ref(),
|
|
processor_version=self.processor_version,
|
|
rule_set_sha256=self.rule_set_sha256,
|
|
attempt_no=attempt_no,
|
|
idempotency_key=processing_dispatch_idempotency_key(request_without_grant),
|
|
uploaded_filename=uploaded_filename,
|
|
)
|
|
)
|
|
grant = self.ingestion_repository.issue_grant(
|
|
job_id, attempt_no, ttl_seconds=self.submission_grant_ttl_seconds
|
|
)
|
|
request = ProcessingRequest(
|
|
job_id=job_id,
|
|
attempt_no=attempt_no,
|
|
source_file_id=source_file_id,
|
|
processor_version=self.processor_version,
|
|
rule_set_sha256=self.rule_set_sha256,
|
|
submission_grant=grant.submission_grant,
|
|
)
|
|
record = self.processing_starter.start(request)
|
|
return {
|
|
"job_id": job_id,
|
|
"status": str(getattr(record, "remote_status", "queued")),
|
|
"attempt_no": attempt_no,
|
|
"source_sha256": stored.sha256,
|
|
"source_byte_size": stored.byte_size,
|
|
}
|