mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 13:51:09 -05:00
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols. - Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage. - Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
187 lines
6.2 KiB
Markdown
187 lines
6.2 KiB
Markdown
# ADR-003: State Machine Pattern
|
|
|
|
## Status
|
|
|
|
Accepted
|
|
|
|
## Context
|
|
|
|
Parks and rides in ThrillWiki go through various operational states:
|
|
- Parks: Operating, Closed Temporarily, Closed Permanently, Under Construction
|
|
- Rides: Operating, Closed, Under Construction, Removed, Relocated
|
|
|
|
Managing these state transitions requires:
|
|
- Valid state transition enforcement
|
|
- Audit trail of state changes
|
|
- Business logic tied to state changes (notifications, cache invalidation)
|
|
|
|
## Decision
|
|
|
|
We implemented a **Finite State Machine (FSM) pattern** for managing entity states, using django-fsm with custom enhancements.
|
|
|
|
### State Model
|
|
|
|
```python
|
|
from django_fsm import FSMField, transition
|
|
|
|
class Park(models.Model):
|
|
status = FSMField(default='OPERATING')
|
|
|
|
@transition(
|
|
field=status,
|
|
source=['OPERATING', 'CLOSED_TEMP'],
|
|
target='CLOSED_PERM'
|
|
)
|
|
def close_permanently(self, reason=None):
|
|
"""Close the park permanently."""
|
|
self.closure_reason = reason
|
|
self.closure_date = timezone.now()
|
|
|
|
@transition(
|
|
field=status,
|
|
source='CLOSED_TEMP',
|
|
target='OPERATING'
|
|
)
|
|
def reopen(self):
|
|
"""Reopen a temporarily closed park."""
|
|
self.closure_reason = None
|
|
```
|
|
|
|
### State Diagram
|
|
|
|
```
|
|
Park States:
|
|
┌──────────────┐
|
|
│ PLANNED │
|
|
└──────┬───────┘
|
|
│
|
|
▼
|
|
┌──────────────────────────────────────┐
|
|
│ UNDER_CONSTRUCTION │
|
|
└──────────────────┬───────────────────┘
|
|
│
|
|
▼
|
|
┌──────────────────────────────────────┐
|
|
│ OPERATING │◄────┐
|
|
└──────────────────┬───────────────────┘ │
|
|
│ │
|
|
┌───────────┼───────────┐ │
|
|
│ │ │ │
|
|
▼ ▼ ▼ │
|
|
┌────────────┐ ┌────────┐ ┌────────────┐ │
|
|
│CLOSED_TEMP │ │SEASONAL│ │CLOSED_PERM │ │
|
|
└─────┬──────┘ └────────┘ └────────────┘ │
|
|
│ │
|
|
└───────────────────────────────────────┘
|
|
```
|
|
|
|
### Transition Validation
|
|
|
|
```python
|
|
class ParkStateTransition(models.Model):
|
|
"""Audit log for park state transitions."""
|
|
park = models.ForeignKey(Park, on_delete=models.CASCADE)
|
|
from_state = models.CharField(max_length=20)
|
|
to_state = models.CharField(max_length=20)
|
|
transition_date = models.DateTimeField(auto_now_add=True)
|
|
transitioned_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
|
reason = models.TextField(blank=True)
|
|
```
|
|
|
|
## Consequences
|
|
|
|
### Benefits
|
|
|
|
1. **Valid Transitions Only**: Invalid state changes are rejected at the model level
|
|
2. **Audit Trail**: All transitions are logged with timestamps and users
|
|
3. **Business Logic Encapsulation**: Transition methods contain related logic
|
|
4. **Testability**: State machines are easy to unit test
|
|
5. **Documentation**: State diagrams document valid workflows
|
|
|
|
### Trade-offs
|
|
|
|
1. **Learning Curve**: Developers need to understand FSM concepts
|
|
2. **Migration Complexity**: Adding new states requires careful migration
|
|
3. **Flexibility**: Rigid state transitions can be limiting for edge cases
|
|
|
|
### State Change Hooks
|
|
|
|
```python
|
|
from django.db.models.signals import pre_save
|
|
from django.dispatch import receiver
|
|
|
|
@receiver(pre_save, sender=Park)
|
|
def park_state_change(sender, instance, **kwargs):
|
|
if instance.pk:
|
|
old_instance = Park.objects.get(pk=instance.pk)
|
|
if old_instance.status != instance.status:
|
|
# Log transition
|
|
ParkStateTransition.objects.create(
|
|
park=instance,
|
|
from_state=old_instance.status,
|
|
to_state=instance.status,
|
|
)
|
|
# Invalidate caches
|
|
invalidate_park_caches(instance)
|
|
# Send notifications
|
|
notify_state_change(instance, old_instance.status)
|
|
```
|
|
|
|
## Alternatives Considered
|
|
|
|
### Simple Status Field
|
|
|
|
**Rejected because:**
|
|
- No validation of state transitions
|
|
- Business logic scattered across codebase
|
|
- No built-in audit trail
|
|
|
|
### Event Sourcing
|
|
|
|
**Rejected because:**
|
|
- Overkill for current requirements
|
|
- Significant complexity increase
|
|
- Steeper learning curve
|
|
|
|
### Workflow Engine
|
|
|
|
**Rejected because:**
|
|
- External dependency overhead
|
|
- More complex than needed
|
|
- FSM sufficient for current use cases
|
|
|
|
## Implementation Details
|
|
|
|
### Ride Status States
|
|
|
|
```python
|
|
class RideStatus(models.TextChoices):
|
|
OPERATING = 'OPERATING', 'Operating'
|
|
CLOSED_TEMP = 'CLOSED_TEMP', 'Temporarily Closed'
|
|
CLOSED_PERM = 'CLOSED_PERM', 'Permanently Closed'
|
|
UNDER_CONSTRUCTION = 'UNDER_CONSTRUCTION', 'Under Construction'
|
|
REMOVED = 'REMOVED', 'Removed'
|
|
RELOCATED = 'RELOCATED', 'Relocated'
|
|
```
|
|
|
|
### Testing State Transitions
|
|
|
|
```python
|
|
class ParkStateTransitionTest(TestCase):
|
|
def test_cannot_reopen_permanently_closed_park(self):
|
|
park = ParkFactory(status='CLOSED_PERM')
|
|
with self.assertRaises(TransitionNotAllowed):
|
|
park.reopen()
|
|
|
|
def test_can_close_operating_park_temporarily(self):
|
|
park = ParkFactory(status='OPERATING')
|
|
park.close_temporarily(reason='Maintenance')
|
|
self.assertEqual(park.status, 'CLOSED_TEMP')
|
|
```
|
|
|
|
## References
|
|
|
|
- [django-fsm Documentation](https://github.com/viewflow/django-fsm)
|
|
- [State Machine Diagrams](../state_machines/diagrams.md)
|
|
- [Finite State Machine Wikipedia](https://en.wikipedia.org/wiki/Finite-state_machine)
|