fix telegram
This commit is contained in:
333
deploy-package-20260223-151231/backend/modules/reports/cache/event_monitor.py
vendored
Normal file
333
deploy-package-20260223-151231/backend/modules/reports/cache/event_monitor.py
vendored
Normal file
@@ -0,0 +1,333 @@
|
||||
"""
|
||||
Event-based cache invalidation monitor
|
||||
|
||||
Monitors {schema}.act tables for changes and invalidates cache automatically
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
# Path setup handled by main.py - this is redundant but kept for module isolation
|
||||
# sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../..')))
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventMonitor:
|
||||
"""
|
||||
Monitors schema.act tables for changes to trigger cache invalidation
|
||||
|
||||
Runs as background task, checking max(id_act) at configured intervals
|
||||
Uses permanent schema_mappings cache to avoid repeated schema lookups
|
||||
"""
|
||||
|
||||
def __init__(self, cache_manager, config):
|
||||
"""
|
||||
Initialize event monitor
|
||||
|
||||
Args:
|
||||
cache_manager: CacheManager instance
|
||||
config: CacheConfig instance
|
||||
"""
|
||||
self.cache = cache_manager
|
||||
self.config = config
|
||||
self.running = False
|
||||
self.task: Optional[asyncio.Task] = None
|
||||
|
||||
async def start(self):
|
||||
"""Start monitoring task"""
|
||||
if self.running:
|
||||
logger.warning("Event monitor already running")
|
||||
return
|
||||
|
||||
self.running = True
|
||||
self.task = asyncio.create_task(self._monitor_loop())
|
||||
logger.info(
|
||||
f"Event monitor started (interval: {self.config.check_interval}s)"
|
||||
)
|
||||
|
||||
async def stop(self):
|
||||
"""Stop monitoring task"""
|
||||
if not self.running:
|
||||
return
|
||||
|
||||
self.running = False
|
||||
if self.task:
|
||||
self.task.cancel()
|
||||
try:
|
||||
await self.task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
logger.info("Event monitor stopped")
|
||||
|
||||
async def _monitor_loop(self):
|
||||
"""Main monitoring loop"""
|
||||
while self.running:
|
||||
try:
|
||||
await self._check_all_companies()
|
||||
await asyncio.sleep(self.config.check_interval)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Event monitor error: {e}", exc_info=True)
|
||||
# Wait 1 minute on error before retrying
|
||||
await asyncio.sleep(60)
|
||||
|
||||
async def _check_all_companies(self):
|
||||
"""
|
||||
Check all companies with active cache for changes
|
||||
|
||||
Queries max(id_act) from {schema}.act for each cached company
|
||||
and invalidates cache if changes detected
|
||||
"""
|
||||
try:
|
||||
# Get list of companies with active cache entries
|
||||
cached_companies = await self.cache.sqlite.get_cached_company_ids()
|
||||
|
||||
if not cached_companies:
|
||||
logger.debug("No cached companies to monitor")
|
||||
return
|
||||
|
||||
logger.info(f"Checking {len(cached_companies)} companies for changes...")
|
||||
invalidated_count = 0
|
||||
|
||||
for company_id in cached_companies:
|
||||
try:
|
||||
# Check if company data changed
|
||||
changed = await self._check_company_changes(company_id)
|
||||
|
||||
if changed:
|
||||
# Invalidate cache for this company
|
||||
await self.cache.invalidate(company_id=company_id)
|
||||
invalidated_count += 1
|
||||
logger.info(
|
||||
f"Cache invalidated for company {company_id} due to act changes"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Error for one company shouldn't stop checking others
|
||||
logger.error(f"Error checking company {company_id}: {e}")
|
||||
continue
|
||||
|
||||
if invalidated_count > 0:
|
||||
logger.info(
|
||||
f"Auto-invalidation complete: {invalidated_count} companies affected"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Check all companies error: {e}", exc_info=True)
|
||||
|
||||
async def _check_company_changes(self, company_id: int) -> bool:
|
||||
"""
|
||||
Check if company data changed (monitor max(id_act) in schema.act)
|
||||
|
||||
Args:
|
||||
company_id: Company ID to check
|
||||
|
||||
Returns:
|
||||
True if cache should be invalidated, False otherwise
|
||||
"""
|
||||
try:
|
||||
# 1. Get schema (from permanent cache)
|
||||
schema = await self._get_schema_for_company(company_id)
|
||||
if not schema:
|
||||
logger.warning(f"Schema not found for company {company_id}")
|
||||
return False
|
||||
|
||||
# 2. Get current max(id_act) from Oracle
|
||||
current_max = await self._get_max_id_act(schema)
|
||||
|
||||
# 3. Get cached watermark
|
||||
cached_watermark = await self.cache.sqlite.get_watermark(company_id)
|
||||
|
||||
# 4. Compare
|
||||
if cached_watermark is None:
|
||||
# First time checking - store watermark, no invalidation
|
||||
await self.cache.sqlite.set_watermark(company_id, schema, current_max)
|
||||
logger.debug(
|
||||
f"Watermark initialized for company {company_id}: {current_max}"
|
||||
)
|
||||
return False
|
||||
|
||||
if current_max > cached_watermark:
|
||||
# Changes detected!
|
||||
logger.info(
|
||||
f"Schema {schema} (company {company_id}): "
|
||||
f"id_act changed {cached_watermark} -> {current_max}"
|
||||
)
|
||||
|
||||
# Update watermark
|
||||
await self.cache.sqlite.set_watermark(company_id, schema, current_max)
|
||||
|
||||
return True # Invalidate cache
|
||||
|
||||
# No changes
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Check company {company_id} changes error: {e}")
|
||||
return False # Don't invalidate on error
|
||||
|
||||
async def _get_schema_for_company(self, company_id: int) -> Optional[str]:
|
||||
"""
|
||||
Get schema for company (with permanent caching)
|
||||
|
||||
First checks permanent schema_mappings cache,
|
||||
falls back to Oracle query if not cached
|
||||
|
||||
Args:
|
||||
company_id: Company ID
|
||||
|
||||
Returns:
|
||||
Schema name or None
|
||||
"""
|
||||
# Check permanent cache first
|
||||
cached_schema = await self.cache.sqlite.get_schema_mapping(company_id)
|
||||
if cached_schema:
|
||||
logger.debug(f"Schema mapping HIT for company {company_id}: {cached_schema}")
|
||||
return cached_schema
|
||||
|
||||
# Cache MISS - query Oracle
|
||||
logger.info(f"Schema mapping MISS for company {company_id}, querying Oracle...")
|
||||
|
||||
try:
|
||||
from shared.database.oracle_pool import oracle_pool
|
||||
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT schema
|
||||
FROM CONTAFIN_ORACLE.v_nom_firme
|
||||
WHERE id_firma = :id
|
||||
""", {'id': company_id})
|
||||
result = cursor.fetchone()
|
||||
|
||||
if not result:
|
||||
logger.warning(f"Company {company_id} not found in v_nom_firme")
|
||||
return None
|
||||
|
||||
schema = result[0]
|
||||
|
||||
# Store PERMANENT in schema_mappings (never expires)
|
||||
await self.cache.sqlite.set_schema_mapping(company_id, schema)
|
||||
|
||||
logger.info(f"Schema mapping stored for company {company_id}: {schema}")
|
||||
return schema
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Get schema for company {company_id} error: {e}")
|
||||
return None
|
||||
|
||||
async def _get_max_id_act(self, schema: str) -> int:
|
||||
"""
|
||||
Query max(id_act) from {schema}.act
|
||||
|
||||
Args:
|
||||
schema: Schema name
|
||||
|
||||
Returns:
|
||||
Max id_act value (0 if table empty)
|
||||
"""
|
||||
try:
|
||||
from shared.database.oracle_pool import oracle_pool
|
||||
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# IMPORTANT: Schema comes from v_nom_firme (trusted source)
|
||||
# so it's safe from SQL injection
|
||||
query = f"SELECT MAX(id_act) FROM {schema}.act"
|
||||
cursor.execute(query)
|
||||
|
||||
result = cursor.fetchone()
|
||||
max_id_act = result[0] if result and result[0] is not None else 0
|
||||
|
||||
return max_id_act
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Get max_id_act for schema {schema} error: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
# Optional: Preload all schema mappings at startup
|
||||
|
||||
async def preload_all_schema_mappings():
|
||||
"""
|
||||
Preload all schema mappings at startup (optional)
|
||||
|
||||
Prevents cache misses on first requests by populating
|
||||
schema_mappings table with all companies
|
||||
"""
|
||||
from .cache_manager import get_cache
|
||||
|
||||
cache = get_cache()
|
||||
if not cache:
|
||||
logger.warning("Cache not initialized - skipping schema preload")
|
||||
return
|
||||
|
||||
logger.info("Preloading all schema mappings...")
|
||||
|
||||
try:
|
||||
from shared.database.oracle_pool import oracle_pool
|
||||
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT id_firma, schema
|
||||
FROM CONTAFIN_ORACLE.v_nom_firme
|
||||
""")
|
||||
results = cursor.fetchall()
|
||||
|
||||
for id_firma, schema in results:
|
||||
await cache.sqlite.set_schema_mapping(id_firma, schema)
|
||||
|
||||
logger.info(f"Preloaded {len(results)} schema mappings")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Schema preload error: {e}")
|
||||
|
||||
|
||||
# Global event monitor instance
|
||||
_event_monitor: Optional[EventMonitor] = None
|
||||
|
||||
|
||||
async def init_event_monitor(cache_manager, config):
|
||||
"""
|
||||
Initialize global event monitor
|
||||
|
||||
Args:
|
||||
cache_manager: CacheManager instance
|
||||
config: CacheConfig instance
|
||||
"""
|
||||
global _event_monitor
|
||||
_event_monitor = EventMonitor(cache_manager, config)
|
||||
|
||||
# Start if auto-invalidate enabled
|
||||
if config.auto_invalidate_enabled:
|
||||
await _event_monitor.start()
|
||||
|
||||
|
||||
def get_event_monitor() -> Optional[EventMonitor]:
|
||||
"""Get global event monitor instance"""
|
||||
return _event_monitor
|
||||
|
||||
|
||||
async def toggle_event_monitor(enabled: bool):
|
||||
"""
|
||||
Toggle event monitor on/off
|
||||
|
||||
Args:
|
||||
enabled: True to start monitoring, False to stop
|
||||
"""
|
||||
monitor = get_event_monitor()
|
||||
if not monitor:
|
||||
logger.warning("Event monitor not initialized")
|
||||
return
|
||||
|
||||
if enabled and not monitor.running:
|
||||
await monitor.start()
|
||||
elif not enabled and monitor.running:
|
||||
await monitor.stop()
|
||||
Reference in New Issue
Block a user