Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion durabletask/internal/orchestration_entity_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ def is_inside_critical_section(self) -> bool:

def get_available_entities(self) -> Generator[EntityInstanceId, None, None]:
if self.is_inside_critical_section:
for available_lock in self.available_locks:
# Do not forward send, throw, or close to the lock iterator.
for available_lock in self.available_locks: # noqa: UP028
yield available_lock

def validate_suborchestration_transition(self) -> tuple[bool, str]:
Expand Down
3 changes: 2 additions & 1 deletion durabletask/scheduled/schedule_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from enum import Enum


class ScheduleStatus(str, Enum):
# StrEnum would change the public string and format representations.
class ScheduleStatus(str, Enum): # noqa: UP042
"""Represents the current status of a schedule."""

UNINITIALIZED = "Uninitialized"
Expand Down
6 changes: 0 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,3 @@ ignore = [
# https://github.com/microsoft/durabletask-python/issues/323
"UP017",
]

[tool.ruff.lint.per-file-ignores]
# These suggestions can change generator protocol or public enum behavior:
# https://github.com/microsoft/durabletask-python/issues/324
"durabletask/internal/orchestration_entity_context.py" = ["UP028"]
"durabletask/scheduled/schedule_status.py" = ["UP042"]
40 changes: 40 additions & 0 deletions tests/durabletask/scheduled/test_schedule_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

import json

import pytest

from durabletask.scheduled import ScheduleInvalidTransitionError, ScheduleStatus


@pytest.mark.parametrize(("status", "name", "value", "ordinal"), [
(ScheduleStatus.UNINITIALIZED, "UNINITIALIZED", "Uninitialized", 0),
(ScheduleStatus.ACTIVE, "ACTIVE", "Active", 1),
(ScheduleStatus.PAUSED, "PAUSED", "Paused", 2),
])
def test_status_preserves_string_formatting_and_conversions(
status: ScheduleStatus, name: str, value: str, ordinal: int
) -> None:
formatted = f"ScheduleStatus.{name}"
assert isinstance(status, str)
assert status == value
assert str(status) == formatted
assert f"{status}" == formatted
assert format(status, ">30") == formatted.rjust(30)
assert json.dumps(status) == json.dumps(value)
assert ScheduleStatus(value) is status
assert status.to_dotnet_ordinal() == ordinal
assert ScheduleStatus.from_dotnet(ordinal) is status
assert ScheduleStatus.from_dotnet(value) is status


def test_transition_error_preserves_status_formatting() -> None:
error = ScheduleInvalidTransitionError(
"schedule", ScheduleStatus.PAUSED, ScheduleStatus.PAUSED, "pause_schedule")

assert str(error) == (
"Invalid state transition for schedule 'schedule': operation "
"'pause_schedule' cannot transition from 'ScheduleStatus.PAUSED' "
"to 'ScheduleStatus.PAUSED'."
)
98 changes: 98 additions & 0 deletions tests/durabletask/test_orchestration_entity_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from collections.abc import Generator
from typing import cast

import pytest

from durabletask.entities import EntityInstanceId
from durabletask.internal.orchestration_entity_context import OrchestrationEntityContext


class _ProtocolAwareLocks(list[EntityInstanceId]):
def __init__(self, locks: list[EntityInstanceId]) -> None:
super().__init__(locks)
self.received_exception = False
self.closed = False
self.iterator = self._iterate()

def _iterate(self) -> Generator[EntityInstanceId, None, None]:
try:
yield from super().__iter__()
except ValueError:
self.received_exception = True
raise
finally:
self.closed = True

def __iter__(self) -> Generator[EntityInstanceId, None, None]:
return self.iterator


def _context_with_locks(locks: list[EntityInstanceId]) -> OrchestrationEntityContext:
context = OrchestrationEntityContext("owner")
context.critical_section_id = "section"
context.available_locks = locks
return context


def test_available_entities_follow_critical_section_lock_order() -> None:
first = EntityInstanceId("counter", "a")
second = EntityInstanceId("counter", "b")
context = OrchestrationEntityContext("owner")

assert list(context.get_available_entities()) == []
context.emit_acquire_message("section", [second, first])
assert list(context.get_available_entities()) == []
context.complete_acquire("section")
assert list(context.get_available_entities()) == [first, second]

assert context.validate_operation_transition(first, one_way=False) == (True, "")
assert list(context.get_available_entities()) == [second]
context.recover_lock_after_call(first)
assert list(context.get_available_entities()) == [second, first]

list(context.emit_lock_release_messages())
assert list(context.get_available_entities()) == []


def test_available_entities_ignores_sent_values() -> None:
locks = [EntityInstanceId("counter", key) for key in ("a", "b", "c")]
context = _context_with_locks(locks)
# The runtime accepts non-None sends even though the annotation requires None.
available = cast(Generator[EntityInstanceId, object, None], context.get_available_entities())

assert next(available) == locks[0]
assert available.send(None) == locks[1]
assert available.send("ignored") == locks[2]
with pytest.raises(StopIteration):
next(available)


def test_available_entities_does_not_forward_throw() -> None:
first = EntityInstanceId("counter", "a")
second = EntityInstanceId("counter", "b")
locks = _ProtocolAwareLocks([first, second])
available = _context_with_locks(locks).get_available_entities()

assert next(available) == first
with pytest.raises(ValueError, match="outer generator"):
available.throw(ValueError("outer generator"))
assert not locks.received_exception
assert not locks.closed
assert next(locks.iterator) == second
locks.iterator.close()


def test_available_entities_does_not_forward_close() -> None:
first = EntityInstanceId("counter", "a")
second = EntityInstanceId("counter", "b")
locks = _ProtocolAwareLocks([first, second])
available = _context_with_locks(locks).get_available_entities()

assert next(available) == first
available.close()
assert not locks.closed
assert next(locks.iterator) == second
locks.iterator.close()
Loading