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') or transition_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 stage
  • metadata (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:

  1. Django admin updates Product
  2. Signal syncs to Sanity
  3. Sanity webhook fires back to Django
  4. Django signal syncs to Sanity again
  5. 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 check
  • transaction_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 identifier
  • timeout (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:

  1. Worker A receives webhook for User X
  2. Worker B receives webhook for User X (same revision)
  3. Both try to acquire lock simultaneously
  4. Worker A gets lock, processes webhook
  5. Worker B waits 100ms, checks if lock expired
  6. If Worker A finished: Worker B gets lock, processes
  7. 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
    }

  • 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

Was this page helpful?