update: add logger info

This commit is contained in:
tao.chen
2026-08-07 19:19:25 +08:00
parent ed517c488b
commit 6ff2127de7
8 changed files with 217 additions and 4 deletions
+47 -2
View File
@@ -7,6 +7,8 @@ import tempfile
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path, PurePosixPath from pathlib import Path, PurePosixPath
from loguru import logger
MAX_LOG_BYTES = 4 * 1024 * 1024 MAX_LOG_BYTES = 4 * 1024 * 1024
@@ -39,6 +41,12 @@ async def _execute_notebook(
timeout_seconds: int, timeout_seconds: int,
) -> ExecutionResult: ) -> ExecutionResult:
output = artifact.with_name(f"executed-{artifact_name}") output = artifact.with_name(f"executed-{artifact_name}")
logger.debug(
"notebook exec start: artifact={} timeout={}s args={}",
artifact_name,
timeout_seconds,
len(arguments),
)
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(
sys.executable, sys.executable,
"-m", "-m",
@@ -67,6 +75,11 @@ async def _execute_notebook(
exit_code = None exit_code = None
error_code = "NODE_TIMEOUT" error_code = "NODE_TIMEOUT"
error_message = f"notebook exceeded timeout of {timeout_seconds} seconds" error_message = f"notebook exceeded timeout of {timeout_seconds} seconds"
logger.warning(
"notebook exec timed out: artifact={} timeout={}s",
artifact_name,
timeout_seconds,
)
else: else:
exit_code = process.returncode exit_code = process.returncode
status = ( status = (
@@ -90,7 +103,7 @@ async def _execute_notebook(
if status == "timed_out" if status == "timed_out"
else "Notebook execution failed; see node log" else "Notebook execution failed; see node log"
) )
return ExecutionResult( result = ExecutionResult(
status=status, status=status,
exit_code=exit_code, exit_code=exit_code,
logs=_limited_log(stdout.decode("utf-8", errors="replace")), logs=_limited_log(stdout.decode("utf-8", errors="replace")),
@@ -100,6 +113,13 @@ async def _execute_notebook(
error_code=error_code, error_code=error_code,
error_message=error_message, error_message=error_message,
) )
logger.info(
"notebook exec done: artifact={} status={} exit_code={}",
artifact_name,
result.status,
result.exit_code,
)
return result
async def _execute_python( async def _execute_python(
@@ -108,6 +128,12 @@ async def _execute_python(
arguments: list[str], arguments: list[str],
timeout_seconds: int, timeout_seconds: int,
) -> ExecutionResult: ) -> ExecutionResult:
logger.debug(
"python exec start: artifact={} timeout={}s args={}",
artifact.name,
timeout_seconds,
len(arguments),
)
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(
sys.executable, sys.executable,
str(artifact), str(artifact),
@@ -129,6 +155,11 @@ async def _execute_python(
{"status": "timed_out", "exit_code": None, "message": message}, {"status": "timed_out", "exit_code": None, "message": message},
ensure_ascii=False, ensure_ascii=False,
).encode("utf-8") ).encode("utf-8")
logger.warning(
"python exec timed out: artifact={} timeout={}s",
artifact.name,
timeout_seconds,
)
return ExecutionResult( return ExecutionResult(
status="timed_out", status="timed_out",
exit_code=None, exit_code=None,
@@ -148,7 +179,7 @@ async def _execute_python(
{"status": status, "exit_code": exit_code}, {"status": status, "exit_code": exit_code},
ensure_ascii=False, ensure_ascii=False,
).encode("utf-8") ).encode("utf-8")
return ExecutionResult( result = ExecutionResult(
status=status, status=status,
exit_code=exit_code, exit_code=exit_code,
logs=_limited_log(stdout.decode("utf-8", errors="replace")), logs=_limited_log(stdout.decode("utf-8", errors="replace")),
@@ -158,6 +189,13 @@ async def _execute_python(
error_code=None if status == "succeeded" else "PROCESS_EXIT_NONZERO", error_code=None if status == "succeeded" else "PROCESS_EXIT_NONZERO",
error_message=message, error_message=message,
) )
logger.info(
"python exec done: artifact={} status={} exit_code={}",
artifact.name,
result.status,
result.exit_code,
)
return result
async def execute_artifact( async def execute_artifact(
@@ -170,6 +208,12 @@ async def execute_artifact(
arguments: list[str], arguments: list[str],
timeout_seconds: int, timeout_seconds: int,
) -> ExecutionResult: ) -> ExecutionResult:
logger.debug(
"execute_artifact: run={} node={} script_type={}",
run_id[-12:],
node_run_id[-12:],
script_type,
)
# Stage the artifact under Python's system temp dir (cleaned on context # Stage the artifact under Python's system temp dir (cleaned on context
# exit). No local-FS volume assumption; the bytes only live for the # exit). No local-FS volume assumption; the bytes only live for the
# duration of the subprocess. # duration of the subprocess.
@@ -193,4 +237,5 @@ async def execute_artifact(
arguments=arguments, arguments=arguments,
timeout_seconds=timeout_seconds, timeout_seconds=timeout_seconds,
) )
logger.error("unsupported script_type: {}", script_type)
raise ValueError(f"unsupported script_type: {script_type}") raise ValueError(f"unsupported script_type: {script_type}")
+6
View File
@@ -3,6 +3,8 @@ from __future__ import annotations
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import Any, AsyncIterator from typing import Any, AsyncIterator
from loguru import logger
from common.config import settings from common.config import settings
from common.db import create_database_engine, create_session_factory from common.db import create_database_engine, create_session_factory
from common.service_app import create_service_app from common.service_app import create_service_app
@@ -16,6 +18,7 @@ from schedule.storage_client import SchedulerStorageClient
@asynccontextmanager @asynccontextmanager
async def lifespan(app: Any) -> AsyncIterator[None]: async def lifespan(app: Any) -> AsyncIterator[None]:
logger.info("scheduler service starting: service_name={}", settings.service_name)
engine = create_database_engine(settings.database_url) engine = create_database_engine(settings.database_url)
session_factory = create_session_factory(engine) session_factory = create_session_factory(engine)
storage_http_client = build_storage_http_client() storage_http_client = build_storage_http_client()
@@ -28,12 +31,15 @@ async def lifespan(app: Any) -> AsyncIterator[None]:
) )
app.state.scheduler_service = service app.state.scheduler_service = service
await service.start() await service.start()
logger.info("scheduler service started: cron + orchestrator loops running")
try: try:
yield yield
finally: finally:
logger.info("scheduler service shutting down")
await service.close() await service.close()
await storage_http_client.aclose() await storage_http_client.aclose()
await engine.dispose() await engine.dispose()
logger.info("scheduler service stopped")
app = create_service_app( app = create_service_app(
+15
View File
@@ -6,6 +6,8 @@ import sys
import traceback import traceback
from pathlib import Path from pathlib import Path
from loguru import logger
import nbformat import nbformat
from nbclient import NotebookClient from nbclient import NotebookClient
@@ -48,6 +50,12 @@ def main() -> None:
): ):
raise ValueError("arguments-json must contain an array of strings") raise ValueError("arguments-json must contain an array of strings")
logger.info(
"notebook runner start: input={} timeout={}s args={}",
source.name,
args.timeout,
len(arguments),
)
notebook = nbformat.read(source, as_version=4) notebook = nbformat.read(source, as_version=4)
if arguments: if arguments:
notebook.cells.insert( notebook.cells.insert(
@@ -66,17 +74,24 @@ def main() -> None:
kernel_name="python3", kernel_name="python3",
allow_errors=False, allow_errors=False,
) )
logger.debug(
"notebook client created: kernel=python3 timeout={}s",
max(1, args.timeout),
)
# No explicit cwd — the kernel inherits the parent's cwd, which the # No explicit cwd — the kernel inherits the parent's cwd, which the
# scheduler sets to the staged artifact directory. Keeping it here # scheduler sets to the staged artifact directory. Keeping it here
# avoids any "cwd must exist" requirement on the host. # avoids any "cwd must exist" requirement on the host.
client.execute() client.execute()
logger.info("notebook client execute done: input={}", source.name)
except Exception as exc: except Exception as exc:
logger.exception("notebook execute failed: input={}", source.name)
traceback.print_exc() traceback.print_exc()
exit_code = 124 if "timeout" in type(exc).__name__.lower() else 1 exit_code = 124 if "timeout" in type(exc).__name__.lower() else 1
finally: finally:
output.parent.mkdir(parents=True, exist_ok=True) output.parent.mkdir(parents=True, exist_ok=True)
nbformat.write(notebook, output) nbformat.write(notebook, output)
emit_outputs(notebook) emit_outputs(notebook)
logger.debug("notebook output written: {}", output)
raise SystemExit(exit_code) raise SystemExit(exit_code)
+59 -1
View File
@@ -125,6 +125,8 @@ class DispatchOrchestrator:
while True: while True:
try: try:
processed = await self.process_pending_events(limit=20) processed = await self.process_pending_events(limit=20)
if processed:
logger.debug("database event loop processed {} events", processed)
if not processed: if not processed:
await asyncio.sleep(0.25) await asyncio.sleep(0.25)
except asyncio.CancelledError: except asyncio.CancelledError:
@@ -137,6 +139,8 @@ class DispatchOrchestrator:
while True: while True:
try: try:
claimed = await self._claim_execution_events(limit=10) claimed = await self._claim_execution_events(limit=10)
if claimed:
logger.debug("execution loop claimed {} events", claimed)
if not claimed: if not claimed:
await asyncio.sleep(0.25) await asyncio.sleep(0.25)
except asyncio.CancelledError: except asyncio.CancelledError:
@@ -203,6 +207,7 @@ class DispatchOrchestrator:
) )
self._exec_tasks.add(task) self._exec_tasks.add(task)
task.add_done_callback(self._exec_tasks.discard) task.add_done_callback(self._exec_tasks.discard)
logger.debug("claimed {} node execute events", len(claimed))
return len(claimed) return len(claimed)
async def _run_node_execute( async def _run_node_execute(
@@ -210,6 +215,7 @@ class DispatchOrchestrator:
envelope: dict[str, Any], envelope: dict[str, Any],
message_id: str, message_id: str,
) -> None: ) -> None:
logger.debug("node execute start: event_id={}", envelope["event_id"][-12:])
async with self._exec_semaphore: async with self._exec_semaphore:
exc: Exception | None = None exc: Exception | None = None
try: try:
@@ -236,15 +242,27 @@ class DispatchOrchestrator:
item.event_status = "published" item.event_status = "published"
item.published_at = utcnow() item.published_at = utcnow()
item.last_error = None item.last_error = None
logger.info("node execute success: event_id={}", event_id[-12:])
else: else:
item.retry_count += 1 item.retry_count += 1
item.last_error = str(exc)[:2000] item.last_error = str(exc)[:2000]
if item.retry_count >= 5: if item.retry_count >= 5:
item.event_status = "failed" item.event_status = "failed"
logger.warning(
"node execute exhausted retries: event_id={} retry_count={}",
event_id[-12:],
item.retry_count,
)
else: else:
item.available_at = utcnow() + timedelta( item.available_at = utcnow() + timedelta(
seconds=min(30, 2 ** item.retry_count), seconds=min(30, 2 ** item.retry_count),
) )
logger.warning(
"node execute retry scheduled: event_id={} retry_count={} delay={}s",
event_id[-12:],
item.retry_count,
min(30, 2 ** item.retry_count),
)
async def process_pending_events( async def process_pending_events(
self, self,
@@ -274,6 +292,7 @@ class DispatchOrchestrator:
OutboxEvents.aggregate_id == aggregate_id OutboxEvents.aggregate_id == aggregate_id
) )
events = list((await session.scalars(statement)).all()) events = list((await session.scalars(statement)).all())
logger.debug("processed {} outbox events", len(events))
for item in events: for item in events:
try: try:
await self._process_outbox_event(item) await self._process_outbox_event(item)
@@ -356,6 +375,11 @@ class DispatchOrchestrator:
if event.get("event_type") != SCHEDULE_RUN_REQUESTED_EVENT: if event.get("event_type") != SCHEDULE_RUN_REQUESTED_EVENT:
raise ValueError("unexpected event type") raise ValueError("unexpected event type")
payload = event["payload"] payload = event["payload"]
logger.info(
"run requested: run={} trace={}",
payload["run_id"][-12:],
event["trace_id"][-12:],
)
async with session_scope(self.session_factory) as session: async with session_scope(self.session_factory) as session:
inbox, should_process = await self._start_inbox( inbox, should_process = await self._start_inbox(
session, session,
@@ -422,6 +446,12 @@ class DispatchOrchestrator:
if delay_seconds if delay_seconds
else utcnow() else utcnow()
) )
logger.info(
"dispatch node: run={} node={} attempt={}",
run.run_id[-12:],
node["node_id"][-12:],
attempt_no,
)
await add_outbox_event( await add_outbox_event(
session, session,
event_type=NODE_EXECUTE_EVENT, event_type=NODE_EXECUTE_EVENT,
@@ -473,7 +503,15 @@ class DispatchOrchestrator:
if node["node_id"] not in target_node_ids if node["node_id"] not in target_node_ids
] ]
max_concurrency = max(1, int(snapshot.get("max_concurrency", 1))) max_concurrency = max(1, int(snapshot.get("max_concurrency", 1)))
for node in roots[:max_concurrency]: selected_roots = roots[:max_concurrency]
if selected_roots:
logger.info(
"bootstrap roots: run={} roots_count={} max_concurrency={}",
run.run_id[-12:],
len(selected_roots),
max_concurrency,
)
for node in selected_roots:
await self._dispatch_node( await self._dispatch_node(
session, session,
run=run, run=run,
@@ -599,6 +637,12 @@ class DispatchOrchestrator:
session.add(skipped) session.add(skipped)
latest[node_id] = skipped latest[node_id] = skipped
changed = True changed = True
logger.debug(
"skipped node: run={} node={} reason={}",
run.run_id[-12:],
node_id[-12:],
"stop_policy" if stop_all else "parents_blocked",
)
elif ( elif (
# Dispatch only when every parent has actually # Dispatch only when every parent has actually
# run to completion successfully. A None parent # run to completion successfully. A None parent
@@ -663,6 +707,14 @@ class DispatchOrchestrator:
succeeded = all( succeeded = all(
status == "succeeded" for status in statuses status == "succeeded" for status in statuses
) )
logger.info(
"run finalized: run={} status={} failure_policy={} any_failure={} any_success={}",
run.run_id[-12:],
"succeeded" if succeeded else "failed",
failure_policy,
any_real_failure,
any_success,
)
run.run_status = "succeeded" if succeeded else "failed" run.run_status = "succeeded" if succeeded else "failed"
run.error_code = None if succeeded else "SCHEDULE_NODE_FAILED" run.error_code = None if succeeded else "SCHEDULE_NODE_FAILED"
run.error_message = ( run.error_message = (
@@ -686,6 +738,12 @@ class DispatchOrchestrator:
if event.get("event_type") != NODE_FINISHED_EVENT: if event.get("event_type") != NODE_FINISHED_EVENT:
raise ValueError("unexpected event type") raise ValueError("unexpected event type")
payload = event["payload"] payload = event["payload"]
logger.info(
"node finished event: run={} node={} status={}",
payload["run_id"][-12:],
payload["node_id"][-12:],
payload.get("node_status"),
)
async with session_scope(self.session_factory) as session: async with session_scope(self.session_factory) as session:
inbox, should_process = await self._start_inbox( inbox, should_process = await self._start_inbox(
session, session,
+17
View File
@@ -41,6 +41,7 @@ _ACTIVE_TRIGGER: Callable[[str], Awaitable[None]] | None = None
async def dispatch_persisted_cron(schedule_id: str) -> None: async def dispatch_persisted_cron(schedule_id: str) -> None:
"""Dispatch one persisted cron tick through the active service.""" """Dispatch one persisted cron tick through the active service."""
logger.debug("dispatch persisted cron: schedule={}", schedule_id[-12:])
callback = _ACTIVE_TRIGGER callback = _ACTIVE_TRIGGER
if callback is None: if callback is None:
raise RuntimeError("cron trigger callback is not initialized") raise RuntimeError("cron trigger callback is not initialized")
@@ -70,6 +71,7 @@ class CronScheduler:
def start(self) -> None: def start(self) -> None:
"""Start APScheduler and spawn the periodic sync loop.""" """Start APScheduler and spawn the periodic sync loop."""
logger.info("cron scheduler starting")
self.scheduler.start() self.scheduler.start()
self._sync_task = asyncio.create_task( self._sync_task = asyncio.create_task(
self._sync_loop(), self._sync_loop(),
@@ -78,6 +80,7 @@ class CronScheduler:
async def close(self) -> None: async def close(self) -> None:
"""Cancel the sync loop and shut APScheduler down.""" """Cancel the sync loop and shut APScheduler down."""
logger.info("cron scheduler closing")
global _ACTIVE_TRIGGER global _ACTIVE_TRIGGER
if self._sync_task is not None: if self._sync_task is not None:
@@ -107,6 +110,7 @@ class CronScheduler:
while True: while True:
try: try:
await self._sync_once() await self._sync_once()
logger.debug("cron sync tick ok")
except asyncio.CancelledError: except asyncio.CancelledError:
raise raise
except Exception: except Exception:
@@ -134,6 +138,8 @@ class CronScheduler:
).all() ).all()
) )
active_job_ids: set[str] = set() active_job_ids: set[str] = set()
added_count = 0
updated_count = 0
for item in schedules: for item in schedules:
job_id = f"schedule:{item.schedule_id}" job_id = f"schedule:{item.schedule_id}"
active_job_ids.add(job_id) active_job_ids.add(job_id)
@@ -144,6 +150,7 @@ class CronScheduler:
) )
if self.scheduler.get_job(job_id) is not None: if self.scheduler.get_job(job_id) is not None:
self.scheduler.reschedule_job(job_id, trigger=trigger) self.scheduler.reschedule_job(job_id, trigger=trigger)
updated_count += 1
else: else:
self.scheduler.add_job( self.scheduler.add_job(
dispatch_persisted_cron, dispatch_persisted_cron,
@@ -155,14 +162,24 @@ class CronScheduler:
max_instances=max(1, item.max_concurrency), max_instances=max(1, item.max_concurrency),
misfire_grace_time=60, misfire_grace_time=60,
) )
added_count += 1
job = self.scheduler.get_job(job_id) job = self.scheduler.get_job(job_id)
item.next_run_at = naive_utc(job.next_run_time) item.next_run_at = naive_utc(job.next_run_time)
removed_count = 0
for job in self.scheduler.get_jobs(): for job in self.scheduler.get_jobs():
if ( if (
job.id.startswith("schedule:") job.id.startswith("schedule:")
and job.id not in active_job_ids and job.id not in active_job_ids
): ):
self.scheduler.remove_job(job.id) self.scheduler.remove_job(job.id)
removed_count += 1
if added_count or updated_count or removed_count:
logger.info(
"cron sync reconciled: added={} updated={} removed={}",
added_count,
updated_count,
removed_count,
)
__all__ = ["CronScheduler"] __all__ = ["CronScheduler"]
+11
View File
@@ -102,9 +102,15 @@ class SchedulerService:
""" """
self.cron.start() self.cron.start()
self.orchestrator.start() self.orchestrator.start()
logger.info(
"scheduler service starting: cron={} orchestrator={}",
type(self.cron).__name__,
type(self.orchestrator).__name__,
)
async def close(self) -> None: async def close(self) -> None:
"""Cancel orchestrator loop, then shut APScheduler down.""" """Cancel orchestrator loop, then shut APScheduler down."""
logger.info("scheduler service closing")
await self.orchestrator.close() await self.orchestrator.close()
await self.cron.close() await self.cron.close()
@@ -140,6 +146,11 @@ class SchedulerService:
idempotency_key = ( idempotency_key = (
f"cron:{schedule_id}:{now.strftime('%Y%m%d%H%M')}" f"cron:{schedule_id}:{now.strftime('%Y%m%d%H%M')}"
) )
logger.info(
"cron trigger: schedule={} workspace={}",
schedule_id[-12:],
workspace_id[-12:],
)
try: try:
async with self.session_factory() as session: async with self.session_factory() as session:
await create_scheduled_run( await create_scheduled_run(
+21
View File
@@ -15,6 +15,8 @@ from __future__ import annotations
import base64 import base64
from typing import Any from typing import Any
from loguru import logger
import httpx import httpx
@@ -39,6 +41,13 @@ class SchedulerStorageClient:
we return the inner ``data`` dict (which includes we return the inner ``data`` dict (which includes
``storage_object_id`` and ``storage_uri``). ``storage_object_id`` and ``storage_uri``).
""" """
logger.debug(
"storage create_object: workspace={} usage_type={} file={} size={}B",
workspace_id[-12:],
usage_type,
file_name,
len(content),
)
response = await self._http.post( response = await self._http.post(
"/internal/v1/objects", "/internal/v1/objects",
json={ json={
@@ -54,8 +63,20 @@ class SchedulerStorageClient:
"relative_path": None, "relative_path": None,
}, },
) )
if response.is_error:
logger.warning(
"storage create_object HTTP error: status={} url={}",
response.status_code,
response.request.url,
)
response.raise_for_status() response.raise_for_status()
body = response.json() body = response.json()
logger.info(
"storage create_object done: workspace={} usage_type={} storage_object_id={}",
workspace_id[-12:],
usage_type,
body["data"].get("storage_object_id"),
)
return body["data"] return body["data"]
+41 -1
View File
@@ -70,6 +70,12 @@ class NodeExecutor:
payload = event["payload"] payload = event["payload"]
if not await self._set_node_running(event, message_id): if not await self._set_node_running(event, message_id):
return return
logger.info(
"node execute start: node_run={} script_type={} timeout={}s",
payload["node_run_id"][-12:],
payload["script_type"],
payload["timeout_seconds"],
)
started_at = utcnow() started_at = utcnow()
context: dict[str, Any] | None = None context: dict[str, Any] | None = None
try: try:
@@ -193,6 +199,12 @@ class NodeExecutor:
}, },
) )
self._finish_inbox(inbox) self._finish_inbox(inbox)
logger.info(
"node execute done: node_run={} status={} error_code={}",
payload["node_run_id"][-12:],
final_status,
final_error_code,
)
async def _set_node_running( async def _set_node_running(
self, self,
@@ -219,6 +231,11 @@ class NodeExecutor:
if node_run is None: if node_run is None:
raise ValueError("schedule node run does not exist") raise ValueError("schedule node run does not exist")
if node_run.node_status in TERMINAL_NODE_STATES: if node_run.node_status in TERMINAL_NODE_STATES:
logger.debug(
"node already terminal: node_run={} status={}",
payload["node_run_id"][-12:],
node_run.node_status,
)
self._finish_inbox(inbox) self._finish_inbox(inbox)
return False return False
if node_run.node_status == "queued": if node_run.node_status == "queued":
@@ -226,6 +243,10 @@ class NodeExecutor:
node_run.started_at = utcnow() node_run.started_at = utcnow()
node_run.message = "Worker 正在执行稳定版本" node_run.message = "Worker 正在执行稳定版本"
node_run.state_version += 1 node_run.state_version += 1
logger.debug(
"node running: node_run={}",
payload["node_run_id"][-12:],
)
return True return True
async def _execution_context( async def _execution_context(
@@ -279,7 +300,7 @@ class NodeExecutor:
if not storage.bucket_name or not storage.object_key: if not storage.bucket_name or not storage.object_key:
raise ValueError("stable version artifact location is incomplete") raise ValueError("stable version artifact location is incomplete")
user_id = run.triggered_by or schedule.created_by user_id = run.triggered_by or schedule.created_by
return { context = {
"node_status": node_run.node_status, "node_status": node_run.node_status,
"workspace_id": run.workspace_id, "workspace_id": run.workspace_id,
"workspace_code": workspace.workspace_code, "workspace_code": workspace.workspace_code,
@@ -288,6 +309,13 @@ class NodeExecutor:
"object_key": storage.object_key, "object_key": storage.object_key,
"content_hash": version.content_hash, "content_hash": version.content_hash,
} }
logger.debug(
"execution context loaded: node_run={} bucket={} object_key={}",
payload["node_run_id"][-12:],
context["bucket_name"],
context["object_key"][-32:],
)
return context
async def _fallback_execution_context( async def _fallback_execution_context(
self, self,
@@ -327,6 +355,12 @@ class NodeExecutor:
content = await self.object_store.get(object_key) content = await self.object_store.get(object_key)
if hashlib.sha256(content).hexdigest() != content_hash: if hashlib.sha256(content).hexdigest() != content_hash:
raise ValueError("stable version artifact hash mismatch") raise ValueError("stable version artifact hash mismatch")
logger.debug(
"artifact downloaded: bucket={} object_key={} bytes={}",
bucket_name,
object_key[-32:],
len(content),
)
return content return content
async def _upload_execution_artifacts( async def _upload_execution_artifacts(
@@ -363,6 +397,12 @@ class NodeExecutor:
except Exception as exc: except Exception as exc:
upload_error = f"result upload failed: {exc}"[:2000] upload_error = f"result upload failed: {exc}"[:2000]
logger.exception("failed to upload node execution artifacts") logger.exception("failed to upload node execution artifacts")
if log_id and result_id and not upload_error:
logger.info(
"artifacts uploaded: log_id={} result_id={}",
log_id[-12:],
result_id[-12:],
)
return log_id, result_id, upload_error return log_id, result_id, upload_error
@staticmethod @staticmethod