feat: migrate backend persistence from TinyDB to MongoDB Atlas
- Implement lazy MongoDB client initialization in backend/db/mongo_client.py. - Create Gunicorn configuration to ensure MongoDB client is initialized per worker. - Refactor database access layer to support MongoDB with a new MongoLockedTable adapter. - Add migration script to transfer existing TinyDB data to MongoDB, preserving idempotency. - Update tracking event handling to ensure deterministic ordering with a monotonic sequence. - Modify tests to use mongomock for MongoDB integration and ensure existing tests pass. - Add integration test script to run tests against a local MongoDB Docker container. - Document environment variables and migration process in specs/feat-database-migration.md.
This commit is contained in:
+22
-8
@@ -1,4 +1,5 @@
|
||||
"""Helper functions for tracking events database operations."""
|
||||
import itertools
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
from tinydb import Query
|
||||
@@ -8,6 +9,10 @@ from models.tracking_event import TrackingEvent, EntityType, ActionType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Monotonic sequence used as a deterministic tiebreaker when tracking events
|
||||
# share the same ``occurred_at``/``created_at`` timestamps (common in tests).
|
||||
_tracking_event_seq = itertools.count()
|
||||
|
||||
|
||||
def insert_tracking_event(event: TrackingEvent) -> str:
|
||||
"""
|
||||
@@ -20,7 +25,9 @@ def insert_tracking_event(event: TrackingEvent) -> str:
|
||||
The event ID
|
||||
"""
|
||||
try:
|
||||
tracking_events_db.insert(event.to_dict())
|
||||
event_dict = event.to_dict()
|
||||
event_dict['_seq'] = next(_tracking_event_seq)
|
||||
tracking_events_db.insert(event_dict)
|
||||
logger.info(f"Tracking event created: {event.action} {event.entity_type} {event.entity_id} for child {event.child_id}")
|
||||
return event.id
|
||||
except Exception as e:
|
||||
@@ -61,12 +68,16 @@ def get_tracking_events_by_child(
|
||||
all_results = tracking_events_db.search(query_condition)
|
||||
total = len(all_results)
|
||||
|
||||
# Sort by occurred_at desc, then created_at desc
|
||||
all_results.sort(key=lambda x: (x.get('occurred_at', ''), x.get('created_at', 0)), reverse=True)
|
||||
|
||||
# Sort by occurred_at desc, then created_at desc, then _seq desc for
|
||||
# deterministic ordering when timestamps collide (common in fast tests).
|
||||
all_results.sort(
|
||||
key=lambda x: (x.get('occurred_at', ''), x.get('created_at', 0), x.get('_seq', 0)),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
paginated = all_results[offset:offset + limit]
|
||||
events = [TrackingEvent.from_dict(r) for r in paginated]
|
||||
|
||||
|
||||
return events, total
|
||||
|
||||
|
||||
@@ -99,11 +110,14 @@ def get_tracking_events_by_user(
|
||||
all_results = tracking_events_db.search(query_condition)
|
||||
total = len(all_results)
|
||||
|
||||
all_results.sort(key=lambda x: (x.get('occurred_at', ''), x.get('created_at', 0)), reverse=True)
|
||||
|
||||
all_results.sort(
|
||||
key=lambda x: (x.get('occurred_at', ''), x.get('created_at', 0), x.get('_seq', 0)),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
paginated = all_results[offset:offset + limit]
|
||||
events = [TrackingEvent.from_dict(r) for r in paginated]
|
||||
|
||||
|
||||
return events, total
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user