video-processor/examples/worker_compatibility.py
Ryan Malloy 5ca1b7a07d Migrate to Procrastinate 3.x with backward compatibility for 2.x
- Add comprehensive compatibility layer supporting both Procrastinate 2.x and 3.x
- Implement version-aware database migration system with pre/post migrations for 3.x
- Create worker option mapping for seamless transition between versions
- Add extensive test coverage for all compatibility features
- Update dependency constraints to support both 2.x and 3.x simultaneously
- Provide Docker containerization with uv caching and multi-service orchestration
- Include demo applications and web interface for testing capabilities
- Bump version to 0.2.0 reflecting new compatibility features

Key Features:
- Automatic version detection and feature flagging
- Unified connector creation across PostgreSQL drivers
- Worker option translation (timeout → fetch_job_polling_interval)
- Database migration utilities with CLI and programmatic interfaces
- Complete Docker Compose setup with PostgreSQL, Redis, workers, and demos

Files Added:
- src/video_processor/tasks/compat.py - Core compatibility layer
- src/video_processor/tasks/migration.py - Migration utilities
- src/video_processor/tasks/worker_compatibility.py - Worker CLI
- tests/test_procrastinate_compat.py - Compatibility tests
- tests/test_procrastinate_migration.py - Migration tests
- Dockerfile - Multi-stage build with uv caching
- docker-compose.yml - Complete development environment
- examples/docker_demo.py - Containerized demo application
- examples/web_demo.py - Flask web interface demo

Migration Support:
- Procrastinate 2.x: Single migration command compatibility
- Procrastinate 3.x: Separate pre/post migration phases
- Database URL validation and connection testing
- Version-specific feature detection and graceful degradation
2025-09-05 10:38:12 -06:00

189 lines
6.8 KiB
Python

#!/usr/bin/env python3
"""
Procrastinate worker compatibility example.
This example demonstrates how to run a Procrastinate worker that works
with both version 2.x and 3.x of Procrastinate.
"""
import asyncio
import logging
import signal
import sys
from pathlib import Path
from video_processor.tasks import setup_procrastinate, get_worker_kwargs
from video_processor.tasks.compat import get_version_info, IS_PROCRASTINATE_3_PLUS
from video_processor.tasks.migration import migrate_database
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def setup_and_run_worker():
"""Set up and run a Procrastinate worker with version compatibility."""
# Database connection
database_url = "postgresql://localhost/procrastinate_dev"
try:
# Print version information
version_info = get_version_info()
logger.info(f"Starting worker with Procrastinate {version_info['procrastinate_version']}")
logger.info(f"Available features: {list(version_info['features'].keys())}")
# Optionally run database migration
migrate_success = await migrate_database(database_url)
if not migrate_success:
logger.error("Database migration failed")
return
# Set up Procrastinate app
connector_kwargs = {}
if IS_PROCRASTINATE_3_PLUS:
# Procrastinate 3.x connection pool settings
connector_kwargs.update({
"pool_size": 20,
"max_pool_size": 50,
})
app = setup_procrastinate(database_url, connector_kwargs=connector_kwargs)
# Configure worker options with version compatibility
worker_options = {
"concurrency": 4,
"name": "video-processor-worker",
}
# Add version-specific options
if IS_PROCRASTINATE_3_PLUS:
# Procrastinate 3.x options
worker_options.update({
"fetch_job_polling_interval": 5, # Renamed from "timeout" in 2.x
"shutdown_graceful_timeout": 30, # New in 3.x
"remove_failed": True, # Renamed from "remove_error"
"include_failed": False, # Renamed from "include_error"
})
else:
# Procrastinate 2.x options
worker_options.update({
"timeout": 5,
"remove_error": True,
"include_error": False,
})
# Normalize options for the current version
normalized_options = get_worker_kwargs(**worker_options)
logger.info(f"Worker options: {normalized_options}")
# Create and configure worker
async with app.open_async() as app_context:
worker = app_context.create_worker(
queues=["video_processing", "thumbnail_generation", "sprite_generation"],
**normalized_options
)
# Set up signal handlers for graceful shutdown
if IS_PROCRASTINATE_3_PLUS:
# Procrastinate 3.x has improved graceful shutdown
def signal_handler(sig, frame):
logger.info(f"Received signal {sig}, shutting down gracefully...")
worker.stop()
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
logger.info("Starting Procrastinate worker...")
logger.info("Queues: video_processing, thumbnail_generation, sprite_generation")
logger.info("Press Ctrl+C to stop")
# Run the worker
await worker.run_async()
except KeyboardInterrupt:
logger.info("Worker interrupted by user")
except Exception as e:
logger.error(f"Worker error: {e}")
raise
async def test_task_submission():
"""Test task submission with both Procrastinate versions."""
database_url = "postgresql://localhost/procrastinate_dev"
try:
app = setup_procrastinate(database_url)
# Test video processing task
with Path("test_video.mp4").open("w") as f:
f.write("") # Create dummy file for testing
async with app.open_async() as app_context:
# Submit test task
job = await app_context.configure_task(
"process_video_async",
queue="video_processing"
).defer_async(
input_path="test_video.mp4",
output_dir="/tmp/test_output",
config_dict={"quality_preset": "fast"}
)
logger.info(f"Submitted test job: {job.id}")
# Clean up
Path("test_video.mp4").unlink(missing_ok=True)
except Exception as e:
logger.error(f"Task submission test failed: {e}")
def show_migration_help():
"""Show migration help for upgrading from Procrastinate 2.x to 3.x."""
print("\nProcrastinate Migration Guide")
print("=" * 40)
version_info = get_version_info()
if version_info['is_v3_plus']:
print("✅ You are running Procrastinate 3.x")
print("\nMigration steps for 3.x:")
print("1. Apply pre-migration: python -m video_processor.tasks.migration --pre")
print("2. Deploy new application code")
print("3. Apply post-migration: python -m video_processor.tasks.migration --post")
print("4. Verify: procrastinate schema --check")
else:
print("📦 You are running Procrastinate 2.x")
print("\nTo upgrade to 3.x:")
print("1. Update dependencies: uv add 'procrastinate>=3.0,<4.0'")
print("2. Apply pre-migration: python -m video_processor.tasks.migration --pre")
print("3. Deploy new code")
print("4. Apply post-migration: python -m video_processor.tasks.migration --post")
print(f"\nCurrent version: {version_info['procrastinate_version']}")
print(f"Available features: {list(version_info['features'].keys())}")
if __name__ == "__main__":
if len(sys.argv) > 1:
command = sys.argv[1]
if command == "worker":
asyncio.run(setup_and_run_worker())
elif command == "test":
asyncio.run(test_task_submission())
elif command == "help":
show_migration_help()
else:
print("Usage: python worker_compatibility.py [worker|test|help]")
else:
print("Procrastinate Worker Compatibility Demo")
print("Usage:")
print(" python worker_compatibility.py worker - Run worker")
print(" python worker_compatibility.py test - Test task submission")
print(" python worker_compatibility.py help - Show migration help")
show_migration_help()