Files
Warracker/backend/gunicorn_config.py
sassanix 60239bd637 Fix Apprise notification system, scheduler stability, and email configuration
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
2025-08-24 12:34:40 -03:00

111 lines
5.0 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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)