ADR-016: Capacity Management Integration for SimPy Stores¶
Status¶
IMPLEMENTED - Resolved in MVP
Context¶
PopUpSim had dual capacity management systems: - TrackCapacityManager: Length-based physical capacity (75m track, wagons 10-20m each) - SimPy Stores: Count-based workflow coordination (unlimited capacity)
This created potential inconsistencies where SimPy stores could accept wagons that exceeded physical track capacity.
Current Implementation¶
# Physical capacity (correct)
if track_capacity.can_add_wagon("retrofitted", wagon.length):
track_capacity.add_wagon("retrofitted", wagon.length)
# Workflow coordination (separate)
yield retrofitted_wagons_ready.put(wagon)
Problem¶
No automatic validation between physical capacity and SimPy workflow stores.
Decision¶
IMPLEMENTED: Option A (SimPy Store with Capacity Validation) - Added validation wrapper around existing SimPy stores.
The MVP implements capacity validation for all SimPy store operations:
Implementation in MVP¶
class WorkshopOrchestrator:
def put_wagon_if_fits_retrofitted(self, wagon: Wagon) -> Generator[Any, Any, bool]:
"""Put wagon in retrofitted store only if track has physical capacity."""
retrofitted_track_id = self.retrofitted_tracks[0].id
if self.track_capacity.can_add_wagon(retrofitted_track_id, wagon.length):
# ✅ Capacity validation before SimPy store operation
yield self.retrofitted_wagons_ready.put(wagon)
return True
logger.warning('Cannot add wagon %s - track %s full', wagon.id, retrofitted_track_id)
return False
def get_wagon_from_retrofitted(self) -> Generator[Any, Any, Wagon]:
"""Get wagon from retrofitted store and update capacity tracking."""
wagon = yield self.retrofitted_wagons_ready.get()
# Physical capacity updated by transport job
return wagon
Result: No capacity inconsistencies - physical capacity always validated before SimPy operations.
Decision Options (Evaluated)¶
Option A: SimPy Store with Capacity Validation¶
Approach: Add validation wrapper around existing SimPy stores
def put_wagon_if_fits(self, store_name: str, track_id: str, wagon: Wagon) -> Generator:
"""Put wagon in store only if track has physical capacity."""
if self.track_capacity.can_add_wagon(track_id, wagon.length):
self.track_capacity.add_wagon(track_id, wagon.length)
yield getattr(self, store_name).put(wagon)
return True
# Handle capacity exceeded (wait/reject)
return False
Pros: - ✅ Minimal code changes - ✅ Reuses existing TrackCapacityManager - ✅ Maintains current SimPy store behavior - ✅ Easy to implement incrementally
Cons: - ⚠️ Manual validation required at each put() - ⚠️ Easy to forget validation in new code - ⚠️ Dual responsibility (capacity + workflow)
Option B: Custom Length-Aware Store¶
Approach: Create domain-specific store that encapsulates both concerns
class LengthAwareStore:
def __init__(self, sim: SimulationAdapter, track_capacity: TrackCapacityManager, track_id: str):
self._store = sim.create_store()
self._capacity = track_capacity
self._track_id = track_id
def put(self, wagon: Wagon) -> Generator:
if not self._capacity.can_add_wagon(self._track_id, wagon.length):
raise CapacityExceededError(f"Track {self._track_id} full")
self._capacity.add_wagon(self._track_id, wagon.length)
return self._store.put(wagon)
def get(self) -> Generator:
wagon = yield self._store.get()
self._capacity.remove_wagon(self._track_id, wagon.length)
return wagon
Pros: - ✅ Automatic capacity validation - ✅ Single responsibility per store - ✅ Type-safe domain modeling - ✅ Impossible to forget validation
Cons: - ⚠️ More complex implementation - ⚠️ New abstraction to maintain - ⚠️ Requires refactoring existing code
Analysis¶
Implementation Effort¶
- Option A: Low (wrapper functions)
- Option B: Medium (new class + refactoring)
Maintainability¶
- Option A: Manual validation prone to errors
- Option B: Automatic validation, harder to misuse
Domain Alignment¶
- Option A: Procedural approach
- Option B: Object-oriented domain modeling
Risk Assessment¶
- Option A: Risk of missing validation calls
- Option B: Risk of over-engineering
Recommendation¶
Option A (SimPy Store with Capacity Validation) for the following reasons:
- Pragmatic: Solves the immediate problem with minimal changes
- Incremental: Can be implemented store-by-store
- Familiar: Uses existing patterns and components
- Testable: Easy to verify validation behavior
Implementation Strategy¶
- Create validation helper functions
- Update
move_to_parkingfirst (highest risk area) - Add validation to other store operations incrementally
- Consider Option B if validation becomes too complex
Consequences¶
Positive¶
- Quick resolution of capacity inconsistency issue
- Maintains existing SimPy integration patterns
- Low risk of breaking current functionality
Negative¶
- Requires discipline to use validation consistently
- Manual process prone to human error
- May need refactoring to Option B later if complexity grows
Implementation Results¶
Achieved in MVP¶
- ✅ W07 Problem Solved: Wagons no longer get lost between retrofit and parking
- ✅ Complete Workflow Chain: Train → Collection → Retrofit → Workshop → Retrofitted → Parking
- ✅ Separation of Concerns: WagonStateManager for tracking, SimPy stores for workflow
- ✅ Event-Driven Coordination: No polling, all SimPy store-based coordination
- ✅ Capacity Integration: Physical capacity validated before all store operations
Files Implementing This Decision¶
workshop_operations/application/orchestrator.py- Main workflow coordinationworkshop_operations/domain/services/wagon_operations.py- WagonStateManagerworkshop_operations/infrastructure/resources/track_capacity_manager.py- Capacity management