"""
sequence_registry.py — State storage for Sequence orchestration.
Tracks active sequence executions across handler invocations.
When a sequence starts, its state is registered here. As steps complete,
the state is updated. When all steps are done, the state is cleaned up.
Design:
- Thread-safe (same pattern as TodoRegistry)
- Keyed by sequence_id (short UUID)
- Tracks: steps list, current index, collected results
- Auto-cleanup when sequence completes or errors
Usage:
registry = get_sequence_registry()
# Start a sequence
registry.create(
sequence_id="abc123",
steps=["calculator.add", "calculator.multiply"],
return_to="greeter",
thread_id="...",
initial_payload="...",
)
# Advance on step completion
state = registry.advance(sequence_id, step_result="42")
if state.is_complete:
# All steps done
registry.remove(sequence_id)
# On error
registry.mark_failed(sequence_id, step="calculator.add", error="XSD validation failed")
"""
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any
import threading
@dataclass
class SequenceState:
"""State for an active sequence execution."""
sequence_id: str
steps: List[str] # Ordered list of listener names
return_to: str # Where to send final result
thread_id: str # Original thread for returning
from_id: str # Who started the sequence
current_index: int = 0 # Which step we're on (0-based)
results: List[str] = field(default_factory=list) # XML results from each step
last_result: Optional[str] = None # Most recent step result (for chaining)
failed: bool = False
failed_step: Optional[str] = None
error: Optional[str] = None
@property
def is_complete(self) -> bool:
"""True when all steps have been executed successfully."""
return not self.failed and self.current_index >= len(self.steps)
@property
def current_step(self) -> Optional[str]:
"""Get current step name, or None if complete/failed."""
if self.failed or self.current_index >= len(self.steps):
return None
return self.steps[self.current_index]
@property
def remaining_steps(self) -> List[str]:
"""Steps not yet executed."""
if self.failed:
return []
return self.steps[self.current_index:]
class SequenceRegistry:
"""
Registry for active sequence executions.
Thread-safe. Singleton pattern via get_sequence_registry().
"""
def __init__(self) -> None:
self._lock = threading.Lock()
self._sequences: Dict[str, SequenceState] = {}
def create(
self,
sequence_id: str,
steps: List[str],
return_to: str,
thread_id: str,
from_id: str,
initial_payload: str = "",
) -> SequenceState:
"""
Create a new sequence execution.
Args:
sequence_id: Unique ID for this sequence
steps: Ordered list of listener names to call
return_to: Listener to send SequenceComplete to
thread_id: Thread UUID for routing
from_id: Who initiated the sequence
initial_payload: XML payload for first step
Returns:
SequenceState for tracking
"""
state = SequenceState(
sequence_id=sequence_id,
steps=steps,
return_to=return_to,
thread_id=thread_id,
from_id=from_id,
last_result=initial_payload if initial_payload else None,
)
with self._lock:
self._sequences[sequence_id] = state
return state
def get(self, sequence_id: str) -> Optional[SequenceState]:
"""Get sequence state by ID."""
with self._lock:
return self._sequences.get(sequence_id)
def advance(self, sequence_id: str, step_result: str) -> Optional[SequenceState]:
"""
Record step completion and advance to next step.
Args:
sequence_id: Sequence to advance
step_result: XML result from the completed step
Returns:
Updated SequenceState, or None if not found
"""
with self._lock:
state = self._sequences.get(sequence_id)
if state is None or state.failed:
return state
# Record result
state.results.append(step_result)
state.last_result = step_result
state.current_index += 1
return state
def mark_failed(
self,
sequence_id: str,
step: str,
error: str,
) -> Optional[SequenceState]:
"""
Mark a sequence as failed.
Args:
sequence_id: Sequence that failed
step: Which step failed
error: Error message
Returns:
Updated SequenceState, or None if not found
"""
with self._lock:
state = self._sequences.get(sequence_id)
if state is None:
return None
state.failed = True
state.failed_step = step
state.error = error
return state
def remove(self, sequence_id: str) -> bool:
"""
Remove a sequence (cleanup after completion).
Returns:
True if found and removed, False if not found
"""
with self._lock:
return self._sequences.pop(sequence_id, None) is not None
def list_active(self) -> List[str]:
"""List all active sequence IDs."""
with self._lock:
return list(self._sequences.keys())
def clear(self) -> None:
"""Clear all sequences. Useful for testing."""
with self._lock:
self._sequences.clear()
# ============================================================================
# Singleton
# ============================================================================
_registry: Optional[SequenceRegistry] = None
_registry_lock = threading.Lock()
def get_sequence_registry() -> SequenceRegistry:
"""Get the global SequenceRegistry singleton."""
global _registry
if _registry is None:
with _registry_lock:
if _registry is None:
_registry = SequenceRegistry()
return _registry
def reset_sequence_registry() -> None:
"""Reset the global sequence registry (for testing)."""
global _registry
with _registry_lock:
if _registry is not None:
_registry.clear()
_registry = None