diff --git a/CHANGELOG.md b/CHANGELOG.md index b1d8d28f..472a0384 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +## v1.7.2 + +FIXED + +- Fixed history export failing for orchestrations that completed with an error. Events carrying failure information (orchestration completion, activity failure, sub-orchestration failure, and entity operation failure) were not JSON-serializable, causing the `durabletask.extensions.history_export` module to raise `TypeError: Object of type FailureDetails is not JSON serializable`. `FailureDetails` now serializes to a JSON object with `message`, `error_type`, and `stack_trace` fields. + ## v1.7.1 ADDED diff --git a/durabletask-azuremanaged/CHANGELOG.md b/durabletask-azuremanaged/CHANGELOG.md index 22af0a40..bf5fa5f6 100644 --- a/durabletask-azuremanaged/CHANGELOG.md +++ b/durabletask-azuremanaged/CHANGELOG.md @@ -7,6 +7,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +## v1.7.2 + +- Updates base dependency to durabletask v1.7.2. + ## v1.7.1 - Updates base dependency to durabletask v1.7.1 diff --git a/durabletask-azuremanaged/pyproject.toml b/durabletask-azuremanaged/pyproject.toml index a0527cd7..bb171b4e 100644 --- a/durabletask-azuremanaged/pyproject.toml +++ b/durabletask-azuremanaged/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "durabletask.azuremanaged" -version = "1.7.1" +version = "1.7.2" description = "Durable Task Python SDK provider implementation for the Azure Durable Task Scheduler" keywords = [ "durable", @@ -26,13 +26,13 @@ requires-python = ">=3.10" license = {file = "LICENSE"} readme = "README.md" dependencies = [ - "durabletask>=1.7.1", + "durabletask>=1.7.2", "azure-identity>=1.19.0" ] [project.optional-dependencies] azure-blob-payloads = [ - "durabletask[azure-blob-payloads]>=1.7.1" + "durabletask[azure-blob-payloads]>=1.7.2" ] [project.urls] diff --git a/durabletask/task.py b/durabletask/task.py index dab9e884..8fc7d93e 100644 --- a/durabletask/task.py +++ b/durabletask/task.py @@ -8,6 +8,7 @@ import math from abc import ABC, abstractmethod from collections.abc import Callable, Generator, Sequence +from dataclasses import dataclass from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING, Any, Generic, TypeAlias, TypeVar, cast, overload @@ -449,23 +450,11 @@ def isEnabledFor(self, level: int) -> bool: return self.logger.isEnabledFor(level) +@dataclass(frozen=True) class FailureDetails: - def __init__(self, message: str, error_type: str, stack_trace: str | None): - self._message = message - self._error_type = error_type - self._stack_trace = stack_trace - - @property - def message(self) -> str: - return self._message - - @property - def error_type(self) -> str: - return self._error_type - - @property - def stack_trace(self) -> str | None: - return self._stack_trace + message: str + error_type: str + stack_trace: str | None class TaskFailedError(Exception): diff --git a/pyproject.toml b/pyproject.toml index c565a597..fe270dad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "durabletask" -version = "1.7.1" +version = "1.7.2" description = "A Durable Task Client SDK for Python" keywords = [ "durable", diff --git a/tests/durabletask/extensions/history_export/test_serialization.py b/tests/durabletask/extensions/history_export/test_serialization.py index 6e08be58..61c19162 100644 --- a/tests/durabletask/extensions/history_export/test_serialization.py +++ b/tests/durabletask/extensions/history_export/test_serialization.py @@ -66,6 +66,30 @@ def test_includes_type_discriminator(self) -> None: assert d["event_id"] == 0 assert d["timestamp"].startswith("2025-01-01") + def test_failure_details_is_json_safe(self) -> None: + # Regression: FailureDetails-bearing events must serialize to a + # JSON-safe dict rather than a raw FailureDetails object. + e = history.ExecutionCompletedEvent( + event_id=-1, + timestamp=datetime(2025, 1, 1, tzinfo=timezone.utc), + orchestration_status=3, + result=None, + failure_details=task.FailureDetails( + message="boom", error_type="RuntimeError", stack_trace="trace", + ), + ) + d = event_to_dict(e) + assert d["failure_details"] == { + "message": "boom", + "error_type": "RuntimeError", + "stack_trace": "trace", + } + # The full document must be JSON-serializable. + fmt = ExportFormat(kind=ExportFormatKind.JSON) + out = serialize_history([e], instance_id="fail-1", fmt=fmt) + doc = json.loads(out) + assert doc["events"][0]["failure_details"]["error_type"] == "RuntimeError" + class TestSerializeJson: def test_envelope_fields(self) -> None: