Integrations Domain - Models
Domain: Integrations
Location: api/nextango/apps/integrations/
Last Updated: 2026-07-06
Overview
The Integrations domain provides the foundational infrastructure for bidirectional synchronization between Django and Sanity CMS. It implements enterprise-grade webhook processing with state machines, audit trails, distributed locking, and ping-pong prevention mechanisms.
Key Responsibilities:
- Webhook event tracking and state management
- Outbound mutation logging for loop prevention
- Sync operation audit trails
- Distributed resource locking for concurrent operations
- Enterprise reliability patterns (retry logic, race condition handling, TTL cleanup)
Source Code Location
/home/user/reactsby_nextango/api/nextango/apps/integrations/models.py
WebhookEvent Model
Source: models.py:10-153 Purpose: Enterprise webhook event tracking with comprehensive state machine for hybrid architecture (Redis + Database)
Pattern: State machine with audit trail for processing lifecycle management
Fields
Core Identity Fields
sanity_id (CharField)
- Max Length: 255
- Indexed: Yes
- Nullable: Yes (for non-Sanity webhooks)
- Description: Sanity document ID being modified
- Example:
"product-abc123","user-xyz789"
document_type (CharField)
- Max Length: 100
- Indexed: Yes
- Description: Sanity document type
- Example:
"product","user","store","transaction"
revision (CharField)
- Max Length: 255
- Description: Sanity document revision ID
- Purpose: Version tracking and idempotency
- Example:
"dHJhbnNhY3Rpb24tMTIzNDUtMS0yMDI0"
payload (JSONField)
- Default:
{} - Description: Complete webhook payload from Sanity
- Contains: Full document data, metadata, transaction info
- Purpose: Complete audit trail and retry support
State Machine Fields
status (CharField)
- Max Length: 20
- Indexed: Yes
- Choices:
'received'- Initial state when webhook arrives'buffered'- Stored in Redis buffer'persisted'- Written to database'processing'- Currently being processed'completed'- Successfully processed'failed'- Processing failed (after max retries)'retry'- Queued for retry
- Default:
'received'
processing_path (CharField)
- Max Length: 20
- Indexed: Yes
- Choices:
'redis_buffered'- Buffered in Redis for performance'direct_fallback'- Direct database write (Redis unavailable)'dispatcher'- Legacy dispatcher processing
- Default:
'redis_buffered' - Purpose: Track which processing path was used
priority (CharField)
- Max Length: 20
- Indexed: Yes
- Choices:
'critical'- High priority (user operations, transactions)'normal'- Standard priority (products, inventory updates)
- Default:
'normal' - Purpose: Priority queue processing
processing_log (JSONField)
- Default:
[] - Description: Timestamped audit trail of all processing stages
- Structure:
[ { 'timestamp': 1700000000.123, 'stage': 'status_transition', 'status': 'processing', 'metadata': {'from_status': 'received', 'to_status': 'processing'} }, ... ]
Timing Fields
received_at (DateTimeField)
- Auto Now Add: Yes
- Indexed: Yes
- Description: When webhook was first received
- Purpose: SLA tracking, stale detection
processing_started_at (DateTimeField)
- Nullable: Yes
- Description: When processing began
- Set by:
transition_to_status('processing')
completed_at (DateTimeField)
- Nullable: Yes
- Description: When processing finished (success or failure)
- Set by:
transition_to_status('completed')ortransition_to_status('failed')
Retry Tracking
retry_count (PositiveIntegerField)
- Default: 0
- Description: Number of retry attempts
- Incremented by:
increment_retry()
max_retries (PositiveIntegerField)
- Default: 3
- Description: Maximum retry attempts before marking as failed
- Configurable per webhook type
last_error (TextField)
- Blank: True
- Description: Most recent error message
- Purpose: Debugging and monitoring
Database Indexes
Composite Indexes (models.py:61-66):
indexes = [
Index(fields=['status', 'priority', 'received_at']), # Priority queue queries
Index(fields=['processing_path', 'status']), # Path-specific monitoring
Index(fields=['sanity_id', 'revision']), # Document lookups
Index(fields=['document_type', 'priority']), # Type-based processing
]
Optimization Strategy:
- First index optimizes priority queue:
WHERE status='received' ORDER BY priority DESC, received_at ASC - Second index enables path monitoring:
WHERE processing_path='redis_buffered' AND status='failed' - Third index supports idempotency checks:
WHERE sanity_id='xyz' AND revision='abc' - Fourth index enables type-based routing:
WHERE document_type='transaction' AND priority='critical'
Constraints
Unique Constraint (models.py:68-72):
UniqueConstraint(
fields=['sanity_id', 'revision'],
name='unique_sanity_webhook_event'
)
Purpose: Prevent duplicate processing of same webhook (idempotency)
Effect: IntegrityError if same revision arrives twice
Core Methods
add_processing_log_entry()
Source: models.py:78-93 Purpose: Add timestamped audit log entry for processing stages Pattern: Append-only audit trail
Method Signature:
def add_processing_log_entry(self, stage: str, metadata: dict = None):
Parameters:
stage(str): Description of processing stagemetadata(dict, optional): Additional stage-specific data
Implementation:
entry = {
'timestamp': time.time(),
'stage': stage,
'status': self.status,
'metadata': metadata or {}
}
if not self.processing_log:
self.processing_log = []
self.processing_log.append(entry)
self.save(update_fields=['processing_log'])
Usage Example:
webhook_event.add_processing_log_entry(
stage='redis_buffer_write',
metadata={'buffer_key': 'webhook:product:123', 'ttl': 300}
)
transition_to_status()
Source: models.py:95-116 Purpose: State transition with automatic timing field updates and audit trail Pattern: State machine with side effects
Method Signature:
def transition_to_status(self, new_status: str, metadata: dict = None):
State Transition Logic (models.py:101-106):
# Update timing fields based on status transitions
if new_status == 'processing' and not self.processing_started_at:
self.processing_started_at = timezone.now()
elif new_status in ['completed', 'failed']:
if not self.completed_at:
self.completed_at = timezone.now()
Audit Trail Creation (models.py:108-112):
self.add_processing_log_entry(f'status_transition', {
'from_status': old_status,
'to_status': new_status,
**(metadata or {})
})
Usage Example:
# Start processing
webhook_event.transition_to_status('processing', metadata={
'worker_id': 'worker-1',
'domain': 'products'
})
# Complete successfully
webhook_event.transition_to_status('completed', metadata={
'duration_ms': 150,
'records_created': 1
})
# Failed processing
webhook_event.transition_to_status('failed', metadata={
'error_type': 'ValidationError',
'retry_count': webhook_event.retry_count
})
increment_retry()
Source: models.py:118-136 Purpose: Increment retry counter with automatic failure on max retries exceeded Pattern: Retry exhaustion handling
Method Signature:
def increment_retry(self, error_message: str = None):
Max Retry Logic (models.py:124-134):
if self.retry_count >= self.max_retries:
self.transition_to_status('failed', {
'retry_count': self.retry_count,
'max_retries_exceeded': True,
'final_error': error_message
})
else:
self.transition_to_status('retry', {
'retry_count': self.retry_count,
'error': error_message
})
Usage Example:
try:
process_webhook(webhook_event)
except Exception as e:
webhook_event.increment_retry(error_message=str(e))
# If retry_count < max_retries: status='retry', will be reprocessed
# If retry_count >= max_retries: status='failed', permanently failed
get_processing_duration()
Source: models.py:138-144 Purpose: Calculate total processing time in seconds Returns: Float (seconds) or None if not started
Implementation:
def get_processing_duration(self):
if not self.processing_started_at:
return None
end_time = self.completed_at or timezone.now()
return (end_time - self.processing_started_at).total_seconds()
Usage Example:
# For completed webhook
duration = webhook_event.get_processing_duration()
print(f"Processing took {duration:.2f} seconds")
# For in-progress webhook
current_duration = webhook_event.get_processing_duration()
if current_duration > 30:
print(f"WARNING: Webhook processing taking longer than expected: {current_duration}s")
is_stale()
Source: models.py:146-152 Purpose: Detect webhooks stuck in processing (potential deadlocks) Pattern: Stale resource detection
Method Signature:
def is_stale(self, max_age_minutes: int = 30):
Logic:
if self.status in ['completed', 'failed']:
return False # Completed webhooks are never stale
age_minutes = (timezone.now() - self.received_at).total_seconds() / 60
return age_minutes > max_age_minutes
Usage Example:
# Monitoring job to find stuck webhooks
stale_webhooks = WebhookEvent.objects.filter(
status__in=['received', 'processing', 'retry']
)
for webhook in stale_webhooks:
if webhook.is_stale(max_age_minutes=15): # 15 min threshold
print(f"ALERT: Stale webhook {webhook.id} - {webhook.document_type}")
# Send alert, force retry, or manual intervention
OutboundMutationLog Model
Source: models.py:155-209 Purpose: Database-backed ping-pong prevention for Django→Sanity mutations Pattern: Time-based mutation tracking with automatic TTL cleanup
Purpose and Architecture
Problem: Without tracking, this loop occurs:
- Django admin updates Product
- Signal syncs to Sanity
- Sanity webhook fires back to Django
- Django signal syncs to Sanity again
- Infinite loop
Solution: Track Django→Sanity mutations in database for 5 minutes. When webhook arrives, check if we recently sent that mutation.
Failover: Provides database-backed tracking when Redis cache is unavailable (primary ping-pong prevention is cache-based).
Fields
sanity_id (CharField)
- Max Length: 255
- Indexed: Yes
- Description: Sanity document ID being mutated
- Example:
"product-abc123"
transaction_id (CharField)
- Max Length: 255
- Indexed: Yes
- Nullable: Yes
- Description: Sanity transaction ID from API response
- Purpose: More precise mutation matching
- Example:
"transaction-xyz789"
mutation_type (CharField)
- Max Length: 20
- Description: Type of mutation performed
- Values:
"create","update","delete"
created_at (DateTimeField)
- Auto Now Add: Yes
- Indexed: Yes
- Purpose: TTL cleanup (records older than 5 minutes deleted)
Database Indexes
Indexes (models.py:169-173):
indexes = [
Index(fields=['sanity_id', 'created_at']), # Lookup by document
Index(fields=['transaction_id']), # Lookup by transaction
Index(fields=['created_at']), # TTL cleanup queries
]
Core Methods
is_django_mutation() (Class Method)
Source: models.py:179-199 Purpose: Check if a mutation originated from Django within last 5 minutes Pattern: Time-based mutation detection
Method Signature:
@classmethod
def is_django_mutation(cls, sanity_id: str = None, transaction_id: str = None) -> bool:
Parameters (one required):
sanity_id(str, optional): Sanity document ID to checktransaction_id(str, optional): Transaction ID to check
Implementation:
cutoff_time = timezone.now() - timedelta(minutes=5)
if sanity_id:
return cls.objects.filter(
sanity_id=sanity_id,
created_at__gte=cutoff_time
).exists()
if transaction_id:
return cls.objects.filter(
transaction_id=transaction_id,
created_at__gte=cutoff_time
).exists()
return False
Usage Example:
# In webhook handler
def handle_product_webhook(webhook_data):
sanity_id = webhook_data['_id']
transaction_id = webhook_data.get('transactionId')
# Check if we just sent this mutation
if OutboundMutationLog.is_django_mutation(
sanity_id=sanity_id,
transaction_id=transaction_id
):
logger.info(f"Ignoring webhook - we just sent this mutation")
return # Skip processing to prevent loop
# Process webhook normally
process_product_update(webhook_data)
cleanup_old_records() (Class Method)
Source: models.py:201-208 Purpose: Delete records older than 5 minutes (TTL cleanup) Pattern: Scheduled maintenance task
Method Signature:
@classmethod
def cleanup_old_records(cls):
Implementation:
cutoff_time = timezone.now() - timedelta(minutes=5)
deleted_count, _ = cls.objects.filter(created_at__lt=cutoff_time).delete()
if deleted_count > 0:
logger.info(f"Cleaned up {deleted_count} old OutboundMutationLog records")
return deleted_count
Deployment: Run via Celery periodic task or cron job
Usage Example:
# In Celery periodic task (celery.py or tasks.py)
from celery import shared_task
from nextango.apps.integrations.models import OutboundMutationLog
@shared_task
def cleanup_outbound_mutations():
"""Run every 5 minutes"""
deleted = OutboundMutationLog.cleanup_old_records()
return f"Cleaned up {deleted} mutation log records"
# In celerybeat schedule:
CELERYBEAT_SCHEDULE = {
'cleanup-mutation-logs': {
'task': 'cleanup_outbound_mutations',
'schedule': crontab(minute='*/5'), # Every 5 minutes
},
}
SyncAuditLog Model
Source: models.py:224-266 Purpose: Audit trail for all sync operations, and the surface where two non-terminal dispositions land: inbound documents held for identity review and outbound reference-shape findings. Pattern: Append-only audit log with timing and payload tracking
Fields
direction (CharField)
- Max Length: 20
- Choices:
'inbound'- External system → Django'outbound'- Django → External system
- Description: Direction of data flow
status (CharField)
- Max Length: 20
- Indexed: Yes
- Choices:
'pending'- Queued for processing'in_progress'- Currently processing'completed'- Successfully completed'failed'- Processing failed'retrying'- Retry in progress'quarantined'- Inbound user document held rather than applied, because it arrived without an email or its identity conflicts with an existing user. The record is not a sync failure; it marks a document waiting on identity reconciliation.'shape_warning'- Outbound reference-shape finding recorded by the ref-shape validator. Non-failing: it sits alongside the write it describes and never flips a sync to'failed'. See Validators.
- Default:
'pending'
The quarantined disposition is written by the inbound user sync path when a Sanity user document cannot be applied yet; the shape_warning disposition is written by the outbound ref-shape validator described in Validators. Both are direction-specific: quarantined rows are inbound, shape_warning rows are outbound.
sanity_id (CharField)
- Max Length: 255
- Indexed: Yes
- Nullable: Yes
- Description: Sanity document ID being synced
django_model (CharField)
- Max Length: 100
- Description: Django model name
- Format:
"app_label.ModelName" - Examples:
"products.Product","users.User"
message (TextField)
- Blank: True
- Description: Human-readable status message
- Example:
"Successfully synced Product 'T-Shirt Red' to Sanity"
payload (JSONField)
- Default:
{} - Description: Sync data payload
- Purpose: Full audit trail for debugging
retry_count (PositiveIntegerField)
- Default: 0
- Description: Number of retry attempts for failed syncs
started_at (DateTimeField)
- Nullable: Yes
- Description: When sync operation started
completed_at (DateTimeField)
- Nullable: Yes
- Description: When sync operation completed
created_at (DateTimeField)
- Auto Now Add: Yes
- Description: When audit log record was created
updated_at (DateTimeField)
- Auto Now: Yes
- Description: Last update to audit log record
Database Indexes
Indexes (models.py:259-261):
indexes = [
Index(fields=['status', 'created_at']), # Status-based queries with time ordering
Index(fields=['sanity_id', 'django_model']), # Document-specific lookups
Index(fields=['direction', 'status']), # Direction-filtered status queries
]
Query Optimization:
- Monitor failed syncs:
WHERE status='failed' ORDER BY created_at DESC - Track document sync history:
WHERE sanity_id='xyz' AND django_model='products.Product' - Inbound/outbound analysis:
WHERE direction='inbound' AND status='completed'
Usage Example
from nextango.apps.integrations.models import SyncAuditLog
# Create audit log for successful outbound sync
SyncAuditLog.objects.create(
direction='outbound',
status='completed',
sanity_id='product-abc123',
django_model='products.Product',
message='Successfully synced Product "Red T-Shirt" to Sanity',
payload={'product_id': 'abc123', 'action': 'update'},
started_at=sync_start_time,
completed_at=timezone.now()
)
# Query failed syncs for monitoring
failed_syncs = SyncAuditLog.objects.filter(
status='failed',
created_at__gte=timezone.now() - timedelta(hours=24)
).order_by('-created_at')
for sync in failed_syncs:
print(f"Failed: {sync.django_model} - {sync.message}")
# Analyze sync patterns
from django.db.models import Count
sync_stats = SyncAuditLog.objects.values('direction', 'status').annotate(
count=Count('id')
)
# Output: [{'direction': 'inbound', 'status': 'completed', 'count': 1523}, ...]
SyncLock Model
Source: models.py:254-364 Purpose: Distributed locking to prevent concurrent sync operations on same resource Pattern: Database-backed distributed lock with TTL and smart retry
Fields
resource_id (CharField)
- Max Length: 255
- Unique: Yes
- Indexed: Yes
- Description: Unique identifier for resource being synced
- Format:
"{app_label}.{ModelName}:{instance_id}" - Examples:
"products.Product:abc123","users.User:xyz789"
locked_at (DateTimeField)
- Auto Now Add: Yes
- Description: When lock was acquired
expires_at (DateTimeField)
- Description: When lock automatically expires
- Purpose: Prevent permanent deadlock if process crashes
operation (CharField)
- Max Length: 100
- Blank: True
- Description: Description of operation holding the lock
- Example:
"sync","webhook_processing"
locked_by (CharField)
- Max Length: 100
- Blank: True
- Description: Process or service that acquired lock
- Example:
"universal_sync","webhook_worker_1"
Database Indexes
Indexes (models.py:270-273):
indexes = [
Index(fields=['expires_at']), # Cleanup expired locks
Index(fields=['locked_at']), # Lock age queries
]
Core Methods
is_expired()
Source: models.py:279-282 Purpose: Check if lock has passed its expiration time Returns: Boolean
Implementation:
def is_expired(self):
from django.utils import timezone
return timezone.now() > self.expires_at
acquire_lock() (Class Method)
Source: models.py:284-363 Purpose: Context manager for acquiring distributed lock with smart retry Pattern: Database-backed distributed locking with resource-specific retry strategies
Method Signature:
@classmethod
def acquire_lock(cls, resource_id: str, timeout: int = 30):
Parameters:
resource_id(str): Unique resource identifiertimeout(int): Lock timeout in seconds (default: 30)
Returns: Context manager
Lock Acquisition Logic (models.py:306-314):
expires_at = timezone.now() + timezone.timedelta(seconds=timeout)
lock, created = cls.objects.get_or_create(
resource_id=resource_id,
defaults={
'expires_at': expires_at,
'operation': 'sync',
'locked_by': 'universal_sync'
}
)
Smart Retry Strategy (models.py:316-344):
For UserRole resources (lightweight, quick retry):
if 'users.UserRole' in resource_id:
time.sleep(0.05) # 50ms wait
lock.refresh_from_db()
if lock.is_expired():
lock.expires_at = expires_at
lock.locked_at = timezone.now()
lock.save()
else:
# Skip silently, don't raise error
logger.info(f"UserRole resource {resource_id} is locked, skipping")
return
For User models (brief retry with graceful failure):
elif any(user_type in resource_id for user_type in
['users.Customer', 'users.Employee', 'users.Manager', 'users.Influencer']):
time.sleep(0.1) # 100ms wait
lock.refresh_from_db()
if lock.is_expired():
lock.expires_at = expires_at
lock.locked_at = timezone.now()
lock.save()
else:
logger.info(f"User model resource {resource_id} is locked, skipping")
return
For other models (standard behavior):
else:
time.sleep(0.1)
raise RuntimeError(f"Resource {resource_id} is already locked")
Lock Release (models.py:355-361):
finally:
# Release lock
if lock:
try:
lock.delete()
except cls.DoesNotExist:
pass # Lock already deleted
Usage Example:
from nextango.apps.integrations.models import SyncLock
# Basic usage
try:
with SyncLock.acquire_lock('products.Product:abc123', timeout=60):
# Perform sync operation
sync_product_to_sanity(product)
# Lock automatically released on exit
except RuntimeError as e:
logger.warning(f"Could not acquire lock: {e}")
# Handle lock contention
# UserRole sync (automatic graceful skip if locked)
with SyncLock.acquire_lock('users.UserRole:xyz789'):
# If locked by another worker, this silently skips
# No RuntimeError raised for UserRole
sync_user_role_to_sanity(user_role)
Race Condition Handling: The smart retry logic handles the common webhook race condition:
- Worker A receives webhook for User X
- Worker B receives webhook for User X (same revision)
- Both try to acquire lock simultaneously
- Worker A gets lock, processes webhook
- Worker B waits 100ms, checks if lock expired
- If Worker A finished: Worker B gets lock, processes
- If Worker A still processing: Worker B skips gracefully
Cross-Model Integration Patterns
Webhook Processing Pipeline
Complete Flow:
# 1. Webhook arrives → Create WebhookEvent
webhook_event = WebhookEvent.objects.create(
sanity_id=payload['_id'],
document_type=payload['_type'],
revision=payload['_rev'],
payload=payload,
status='received',
priority='critical' if payload['_type'] == 'transaction' else 'normal'
)
# 2. Check ping-pong prevention
if OutboundMutationLog.is_django_mutation(
sanity_id=webhook_event.sanity_id,
transaction_id=payload.get('transactionId')
):
webhook_event.transition_to_status('completed',
metadata={'reason': 'ping-pong_prevented'})
return
# 3. Acquire distributed lock
resource_id = f"{webhook_event.document_type}.{webhook_event.document_type.capitalize()}:{webhook_event.sanity_id}"
try:
with SyncLock.acquire_lock(resource_id, timeout=30):
# 4. Transition to processing
webhook_event.transition_to_status('processing')
# 5. Process webhook
result = process_webhook_payload(webhook_event.payload)
# 6. Create audit log
SyncAuditLog.objects.create(
direction='inbound',
status='completed',
sanity_id=webhook_event.sanity_id,
django_model=f"{webhook_event.document_type}s.{webhook_event.document_type.capitalize()}",
message=f"Successfully synced {webhook_event.document_type}",
payload=webhook_event.payload
)
# 7. Mark webhook complete
webhook_event.transition_to_status('completed')
except Exception as e:
# 8. Handle failure with retry
webhook_event.increment_retry(error_message=str(e))
# 9. Create failure audit log
SyncAuditLog.objects.create(
direction='inbound',
status='failed',
sanity_id=webhook_event.sanity_id,
django_model=f"{webhook_event.document_type}s.{webhook_event.document_type.capitalize()}",
message=f"Failed to sync: {str(e)}",
payload=webhook_event.payload
)
Outbound Sync Pipeline
Complete Flow:
# 1. Django model changes (via signal or explicit call)
def sync_product_to_sanity(product):
# 2. Check if should skip (webhook processing context)
from nextango.apps.integrations.sync.sync_context import should_skip_sync_to_sanity
if should_skip_sync_to_sanity():
return
# 3. Acquire distributed lock
resource_id = f"products.Product:{product.sanity_id}"
try:
with SyncLock.acquire_lock(resource_id, timeout=30):
# 4. Perform Sanity API mutation
sanity_client = SanityAPIClient()
result = sanity_client.mutate_document(product.to_sanity_format())
# 5. Record outbound mutation (ping-pong prevention)
OutboundMutationLog.objects.create(
sanity_id=product.sanity_id,
transaction_id=result.get('transactionId'),
mutation_type='update'
)
# 6. Create success audit log
SyncAuditLog.objects.create(
direction='outbound',
status='completed',
sanity_id=product.sanity_id,
django_model='products.Product',
message=f"Successfully synced Product '{product.name}' to Sanity",
payload={'product_id': str(product.id)}
)
except RuntimeError as e:
# Lock contention - skip this sync
logger.info(f"Skipping sync for {product.sanity_id}: {e}")
except Exception as e:
# 7. Create failure audit log
SyncAuditLog.objects.create(
direction='outbound',
status='failed',
sanity_id=product.sanity_id,
django_model='products.Product',
message=f"Failed to sync Product: {str(e)}",
payload={'product_id': str(product.id), 'error': str(e)}
)
raise
Monitoring and Operations
Health Check Queries
Stale Webhook Detection:
# Find webhooks stuck in processing
stale_webhooks = WebhookEvent.objects.filter(
status__in=['received', 'processing', 'retry'],
received_at__lt=timezone.now() - timedelta(minutes=30)
)
for webhook in stale_webhooks:
duration = webhook.get_processing_duration() or 0
print(f"Stale webhook: {webhook.document_type} {webhook.sanity_id} ({duration}s)")
Failed Sync Analysis:
# Get failed syncs grouped by model
from django.db.models import Count
failed_by_model = SyncAuditLog.objects.filter(
status='failed',
created_at__gte=timezone.now() - timedelta(hours=24)
).values('django_model').annotate(
count=Count('id')
).order_by('-count')
for stat in failed_by_model:
print(f"{stat['django_model']}: {stat['count']} failures")
Lock Contention Monitoring:
# Find resources with high lock contention
active_locks = SyncLock.objects.filter(
locked_at__gte=timezone.now() - timedelta(minutes=5)
).order_by('resource_id')
print(f"Active locks: {active_locks.count()}")
for lock in active_locks:
age = (timezone.now() - lock.locked_at).total_seconds()
print(f" {lock.resource_id}: {age}s (expires in {(lock.expires_at - timezone.now()).total_seconds()}s)")
Maintenance Tasks
Periodic Cleanup (Celery task):
from celery import shared_task
@shared_task
def cleanup_integration_data():
"""Run daily to clean up old integration data"""
# Clean up OutboundMutationLog (5 min TTL)
mutations_deleted = OutboundMutationLog.cleanup_old_records()
# Clean up old completed webhooks (>7 days)
old_webhooks_deleted = WebhookEvent.objects.filter(
status__in=['completed', 'failed'],
completed_at__lt=timezone.now() - timedelta(days=7)
).delete()[0]
# Clean up old audit logs (>30 days)
old_audits_deleted = SyncAuditLog.objects.filter(
created_at__lt=timezone.now() - timedelta(days=30)
).delete()[0]
# Clean up expired locks
expired_locks_deleted = SyncLock.objects.filter(
expires_at__lt=timezone.now()
).delete()[0]
return {
'mutations_deleted': mutations_deleted,
'webhooks_deleted': old_webhooks_deleted,
'audits_deleted': old_audits_deleted,
'locks_deleted': expired_locks_deleted
}
Related Documentation
- sync_context.md: Thread-local context for loop prevention
- services.md: Universal sync coordinator and domain-specific sync handlers
- views.md: Webhook endpoint handlers
- field_transformation.md: Field mapping and transformation utilities
Cross-Domain References:
- users/signals.md: Uses SyncContext to prevent loops
- products/signals.md: Uses SyncContext to prevent loops
- payments/webhooks.md: Uses WebhookEvent for Stripe webhooks
- core/models.md: Base models that integrate with sync infrastructure