mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 15: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.
6.2 KiB
6.2 KiB
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
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
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
- Valid Transitions Only: Invalid state changes are rejected at the model level
- Audit Trail: All transitions are logged with timestamps and users
- Business Logic Encapsulation: Transition methods contain related logic
- Testability: State machines are easy to unit test
- Documentation: State diagrams document valid workflows
Trade-offs
- Learning Curve: Developers need to understand FSM concepts
- Migration Complexity: Adding new states requires careful migration
- Flexibility: Rigid state transitions can be limiting for edge cases
State Change Hooks
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
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
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')