diff --git a/api/services/workflow_persistence.py b/api/services/workflow_persistence.py index 6707c010ebd..e7b03a369df 100644 --- a/api/services/workflow_persistence.py +++ b/api/services/workflow_persistence.py @@ -47,7 +47,7 @@ from graphon.enums import ( WorkflowType, ) from graphon.node_events import NodeRunResult -from libs.datetime_utils import naive_utc_now +from libs.datetime_utils import naive_utc_now, to_utc_timestamp from repositories.entities.workflow_execution import WorkflowExecution from services.workflow.inspector_events import ( publish_node_changed as _inspector_publish_node_changed, @@ -422,8 +422,8 @@ class WorkflowPersistenceLayer(Layer): error=event.node_run_result.error or event.error, elapsed_time=max((finished_at - event.start_at).total_seconds(), 0.0), execution_metadata={key.value: value for key, value in event.node_run_result.metadata.items()}, - created_at=int(event.start_at.timestamp()), - finished_at=int(finished_at.timestamp()), + created_at=to_utc_timestamp(event.start_at), + finished_at=to_utc_timestamp(finished_at), ) history.append(attempt.model_dump(mode="json")) process_data[RETRY_HISTORY_PROCESS_DATA_KEY] = history diff --git a/api/tests/unit_tests/services/test_workflow_persistence.py b/api/tests/unit_tests/services/test_workflow_persistence.py index 1d3bf637c4a..154c0db9f1e 100644 --- a/api/tests/unit_tests/services/test_workflow_persistence.py +++ b/api/tests/unit_tests/services/test_workflow_persistence.py @@ -1,5 +1,6 @@ from __future__ import annotations +import time from dataclasses import replace from datetime import UTC, datetime, timedelta from types import SimpleNamespace @@ -1257,6 +1258,56 @@ class TestPersistenceLayerCases: assert retry_history[0]["error"] == "attempt 1 failed" assert retry_history[0]["execution_metadata"] == {"iteration_id": "iteration-1"} + def test_retry_history_records_unix_timestamps(self, monkeypatch: pytest.MonkeyPatch): + """Retry attempts store epoch seconds, so the host timezone must not shift them.""" + if not hasattr(time, "tzset"): + pytest.skip("tzset is only available on POSIX platforms") + recorded_at = datetime(2024, 5, 1) + expected = int(recorded_at.replace(tzinfo=UTC).timestamp()) + monkeypatch.setattr(persistence_mod, "naive_utc_now", lambda: recorded_at) + monkeypatch.setenv("TZ", "Asia/Shanghai") + time.tzset() + try: + layer, _, node_repo, _ = _make_layer(node_run_indices={"exec": 1}) + layer.on_event(GraphRunStartedEvent()) + layer.on_event( + NodeRunStartedEvent( + id="exec", + node_id="node", + node_type=BuiltinNodeTypes.HTTP_REQUEST, + node_title="HTTP", + start_at=recorded_at, + ) + ) + layer.on_event( + NodeRunRetryEvent( + id="exec", + node_id="node", + node_type=BuiltinNodeTypes.HTTP_REQUEST, + node_title="HTTP", + start_at=recorded_at, + error="attempt 1 failed", + retry_index=1, + node_run_result=NodeRunResult(inputs={}, process_data={}, outputs={}, metadata={}), + ) + ) + layer.on_event( + NodeRunSucceededEvent( + id="exec", + node_id="node", + node_type=BuiltinNodeTypes.HTTP_REQUEST, + node_title="HTTP", + start_at=recorded_at, + node_run_result=NodeRunResult(inputs={}, process_data={}, outputs={}, metadata={}), + ) + ) + attempt = node_repo.saved_exec_data[-1].process_data["__dify_retry_history"][0] + finally: + monkeypatch.undo() + time.tzset() + assert attempt["created_at"] == expected + assert attempt["finished_at"] == expected + @pytest.mark.parametrize( ("event_type", "expected_status"), [