mirror of
https://github.com/sassanix/Warracker.git
synced 2026-01-06 05:29:39 -06:00
Fixes & Enhancements * Resolved five critical Apprise notification issues: • Ensured configuration reload during scheduled jobs • Fixed warranty data fetching for Apprise-only users • Refactored notification dispatch logic with dedicated helpers • Corrected handler scoping via Flask app context • Wrapped scheduler jobs with Flask app context to prevent context errors → Verified: Scheduled Apprise notifications now work reliably for "Apprise only" and "Both" channels. * Added support for SMTP\_FROM\_ADDRESS environment variable, allowing sender address customization independent of SMTP username. (PR #115) * Fixed duplicate scheduled notifications in multi-worker environments: • Strengthened should\_run\_scheduler() logic • Now guarantees exactly one scheduler instance across all Gunicorn modes. * Fixed stale database connection handling in scheduled jobs: • Fresh connection acquired each run, properly released via try/finally • Eliminates "server closed the connection" errors. * Definitive scheduler logic fix for all memory modes (ultra-light, optimized, performance): • Single-worker runs scheduler if GUNICORN\_WORKER\_ID is unset • Multi-worker: only worker 0 runs scheduler. Impact * Apprise and Email notifications are now stable, reliable, and production-ready * No more duplicate or missed notifications across all memory modes * Improved system efficiency and robustness
111 lines
5.0 KiB
Python
111 lines
5.0 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
Gunicorn configuration file for Warracker application.
|
||
Optimized for memory efficiency with configurable modes.
|
||
"""
|
||
|
||
import os
|
||
|
||
# CRITICAL: Apply gevent monkey patching VERY EARLY to prevent SSL RecursionError
|
||
# This must happen before any other imports that might use SSL
|
||
memory_mode = os.environ.get('WARRACKER_MEMORY_MODE', 'optimized').lower()
|
||
|
||
# Only apply monkey patching for modes that will use gevent workers
|
||
if memory_mode in ('optimized', 'performance'):
|
||
try:
|
||
from gevent import monkey
|
||
monkey.patch_all()
|
||
print("✅ Early gevent monkey patch applied for SSL compatibility.")
|
||
except ImportError:
|
||
print("⚠️ Gevent not found, but gevent workers will be requested. This may cause issues.")
|
||
|
||
import multiprocessing
|
||
|
||
# Server configurations - Dynamic based on memory mode
|
||
bind = "0.0.0.0:5000"
|
||
forwarded_allow_ips = '*' # Trust proxy headers from any source (safe in containerized environment)
|
||
|
||
# Determine worker class first, then apply appropriate monkey patching
|
||
if memory_mode == 'ultra-light':
|
||
# Ultra-lightweight configuration for very memory-constrained environments
|
||
workers = 1 # Single worker for minimal memory usage (~40-50MB total)
|
||
worker_class = "sync" # Sync worker for lowest memory overhead
|
||
worker_connections = 50 # Reduced connections
|
||
max_requests = 500 # More frequent worker restarts to prevent memory leaks
|
||
worker_rlimit_as = 67108864 # 64MB per worker limit
|
||
print("Using ULTRA-LIGHT memory mode - minimal RAM usage, lower concurrency")
|
||
elif memory_mode == 'performance':
|
||
# High-performance configuration for servers with plenty of RAM
|
||
workers = 4 # Original worker count for maximum concurrency
|
||
worker_class = "gevent" # Efficient async I/O handling
|
||
worker_connections = 200 # Higher connection limit per worker
|
||
max_requests = 2000 # Less frequent restarts for better performance
|
||
worker_rlimit_as = 268435456 # 256MB per worker limit
|
||
print("Using PERFORMANCE memory mode - maximum concurrency and performance")
|
||
else:
|
||
# Default optimized configuration for balanced performance and memory usage
|
||
workers = 2 # Reduced from 4 to save ~75MB RAM
|
||
worker_class = "gevent" # More memory efficient than sync workers
|
||
worker_connections = 100 # Limit concurrent connections per worker
|
||
max_requests = 1000 # Restart workers after handling requests to prevent memory leaks
|
||
worker_rlimit_as = 134217728 # 128MB per worker limit
|
||
print("Using OPTIMIZED memory mode - balanced RAM usage and performance")
|
||
|
||
# Note: Gevent monkey patching was applied early at top of file for SSL compatibility
|
||
if worker_class == "gevent":
|
||
print(f"ℹ️ Using gevent workers with early monkey patching applied.")
|
||
else:
|
||
print(f"ℹ️ Using {worker_class} workers - no monkey patching needed.")
|
||
|
||
# Common settings for both modes
|
||
timeout = 120
|
||
keepalive = 5
|
||
max_requests_jitter = 50 # Add randomness to prevent thundering herd
|
||
|
||
# Enhanced settings for file handling to prevent Content-Length mismatches
|
||
limit_request_line = 8190 # Increase request line limit
|
||
limit_request_fields = 200 # Increase header fields limit
|
||
limit_request_field_size = 8190 # Increase header field size limit
|
||
|
||
# Memory management (common to both modes)
|
||
preload_app = True # Share memory between workers (saves RAM)
|
||
worker_tmp_dir = "/dev/shm" # Use RAM disk for worker temporary files
|
||
|
||
# Process management callbacks
|
||
def worker_int(worker):
|
||
"""Called just after a worker exited on SIGINT or SIGQUIT."""
|
||
print(f"Worker {worker.pid} received SIGINT/SIGQUIT")
|
||
|
||
def worker_abort(worker):
|
||
"""Called when a worker receives the SIGABRT signal."""
|
||
print(f"Worker {worker.pid} received SIGABRT")
|
||
|
||
def worker_exit(server, worker):
|
||
"""Called just after a worker has been exited."""
|
||
print(f"Worker {worker.pid} exited")
|
||
|
||
def on_starting(server):
|
||
"""Called just before the master process is initialized."""
|
||
print("Server is starting with memory-optimized configuration")
|
||
|
||
def post_fork(server, worker):
|
||
"""Called just after a worker has been forked."""
|
||
os.environ["GUNICORN_WORKER_ID"] = str(worker.age - 1)
|
||
os.environ["GUNICORN_WORKER_PROCESS_NAME"] = f"worker-{worker.age - 1}"
|
||
os.environ["GUNICORN_WORKER_CLASS"] = worker_class
|
||
|
||
print(f"Worker {worker.pid} (ID: {worker.age - 1}) forked with memory optimization")
|
||
|
||
def pre_fork(server, worker):
|
||
"""Called just before a worker is forked."""
|
||
print(f"Forking worker #{worker.age}")
|
||
|
||
print(f"Gunicorn configuration loaded: {workers} {worker_class} workers in {memory_mode.upper()} mode")
|
||
print(f"Memory limit per worker: {worker_rlimit_as // 1024 // 1024}MB, Max connections: {worker_connections if 'worker_connections' in locals() else 'N/A'}")
|
||
|
||
# To switch memory modes, set WARRACKER_MEMORY_MODE environment variable:
|
||
# - "optimized" (default): 2 gevent workers, balanced performance and memory usage (~60-80MB)
|
||
# - "ultra-light": 1 sync worker, minimal memory usage (~40-50MB, lower concurrency)
|
||
# - "performance": 4 gevent workers, high-performance mode (~200MB) |