| """
|
| K1RL_QU1NT V10.2 - Redis Connection Manager (HuggingFace Spaces Edition)
|
| =========================================================================
|
| Production-grade replacement for DedicatedAblyConnectionManager.
|
| Optimized for container environments and HuggingFace Spaces.
|
|
|
| LATENCY FIXES (V10.0):
|
| V9.0 PROBLEM:
|
| - pubsub.get_message(timeout=0.1) → 100ms polling delay
|
| - command_queue.get(timeout=1.0) → 1000ms polling delay
|
| - Total worst-case latency: ~1100ms
|
|
|
| V10.1 SOLUTION (HF Spaces):
|
| - pubsub.get_message(timeout=None) → TRUE BLOCKING (0ms latency)
|
| - threading.Event() for commands → INSTANT signaling (0ms latency)
|
| - Separate threads for pub vs sub → No blocking interference
|
| - Container-safe Redis configuration
|
| - Environment-aware connection settings
|
| - Expected latency: <1ms (network RTT only)
|
|
|
| LATEST-STATE LATCH MODEL (V10.2 — transport-side coalescing):
|
| PROBLEM:
|
| Redis pub/sub is an ordered transport, NOT a latest-state store.
|
| When QUASAR is busy processing one bound state, subsequent feature/state
|
| messages accumulate and the consumer is forced to walk through every
|
| stale intermediate state one-by-one before reaching the current one.
|
| This causes the live trading path to replay old feature transactions
|
| that are already superseded by the time they are processed.
|
|
|
| V10.2 SOLUTION (latest-state latch):
|
| For hot-path live-state channels (feature vectors, bound state updates),
|
| subscribe with latest_only=True. The manager then maintains a single
|
| pending-latest slot per channel inside the manager process:
|
|
|
| ARRIVING MSG ──► consumer BUSY? ──YES──► overwrite pending_latest[ch]
|
| │ (previous pending discarded)
|
| NO
|
| │
|
| ▼
|
| fire callback immediately
|
| mark in_flight[ch] = True
|
| │
|
| callback returns
|
| │
|
| pending_latest[ch] exists?
|
| YES ──► dispatch freshest, clear slot, repeat
|
| NO ──► in_flight[ch] = False (go idle)
|
|
|
| Net result: QUASAR sees at most ONE waiting message per channel after
|
| each processing cycle — always the freshest available state.
|
|
|
| SCOPE:
|
| - latest_only=True → latch model (hot-path live state, feature channels)
|
| - latest_only=False → lossless queue (rewards, audit/replay channels — UNCHANGED)
|
|
|
| DIAGNOSTICS:
|
| manager.latch_stats → coalescing counters (property)
|
| manager.get_latch_diagnostics() → detailed in-flight + pending inspection
|
| manager.get_state() → includes 'latch_model' sub-dict
|
|
|
| ARCHITECTURE:
|
| ┌─────────────────────────────────────────────────────────────────┐
|
| │ Main Application Thread │
|
| │ subscribe() ──┬──► CommandQueue ──► CommandThread (non-block) │
|
| │ publish() ──┘ │
|
| └─────────────────────────────────────────────────────────────────┘
|
| │
|
| ▼
|
| ┌─────────────────────────────────────────────────────────────────┐
|
| │ ListenerThread (BLOCKING) │
|
| │ pubsub.listen() ──► Yield messages instantly │
|
| │ │ │
|
| │ ├─ latest_only channel ──► latch model (coalesce-to-freshest)│
|
| │ └─ normal channel ──► direct callback (lossless) │
|
| └─────────────────────────────────────────────────────────────────┘
|
|
|
| HUGGINGFACE SPACES OPTIMIZATIONS:
|
| - Auto-detect container environment
|
| - Use redis_config for connection settings
|
| - Container-safe logging and paths
|
| - Resource-optimized connection pools
|
|
|
| Author: K1RL_QU1NT System
|
| Version: 10.2.0 (Latest-State Latch Edition)
|
| """
|
|
|
| import asyncio
|
| import json
|
| import queue
|
| import threading
|
| import time
|
| import logging
|
| import weakref
|
| import os
|
| from datetime import datetime
|
| from typing import Optional, Callable, Dict, Any, List, Union
|
| from dataclasses import dataclass, field
|
| from contextlib import contextmanager
|
| from enum import Enum, auto
|
|
|
| import redis
|
| import redis.asyncio as aioredis
|
|
|
|
|
| try:
|
| from redis_config_v75_1s import REDIS_URL, REDIS_PASSWORD, REDIS_DB_FEATURES, REDIS_DB_REWARDS
|
| HAS_REDIS_CONFIG = True
|
| except ImportError:
|
|
|
| REDIS_URL = "redis://localhost:6379/0"
|
| REDIS_PASSWORD = os.environ.get("REDIS_PASSWORD", "k1rl_v751s_8e4b2f6a0d3c9517")
|
| REDIS_DB_FEATURES = 0
|
| REDIS_DB_REWARDS = 1
|
| HAS_REDIS_CONFIG = False
|
|
|
| logger = logging.getLogger(__name__)
|
|
|
|
|
| IS_HF_SPACES = os.environ.get('SPACE_ID') is not None
|
|
|
|
|
|
|
|
|
|
|
|
|
| class ConnectionState(Enum):
|
| """Connection state enum (mirrors Ably states)."""
|
| INITIALIZED = auto()
|
| CONNECTING = auto()
|
| CONNECTED = auto()
|
| DISCONNECTED = auto()
|
| SUSPENDED = auto()
|
| CLOSING = auto()
|
| CLOSED = auto()
|
| FAILED = auto()
|
|
|
|
|
| @dataclass
|
| class RedisConfig:
|
| """Centralized configuration for Redis connection (HuggingFace Spaces optimized)."""
|
|
|
| url: str = REDIS_URL
|
| password: Optional[str] = REDIS_PASSWORD
|
| socket_timeout: float = 10.0 if not IS_HF_SPACES else 15.0
|
| socket_connect_timeout: float = 10.0 if not IS_HF_SPACES else 15.0
|
| health_check_interval: int = 15 if not IS_HF_SPACES else 30
|
| retry_on_timeout: bool = True
|
| decode_responses: bool = True
|
|
|
|
|
| reconnect_base_interval: float = 0.5 if not IS_HF_SPACES else 2.0
|
| reconnect_max_interval: float = 30.0 if not IS_HF_SPACES else 60.0
|
| reconnect_max_attempts: Optional[int] = None
|
|
|
|
|
| use_streams: bool = False
|
| stream_maxlen: int = 10000
|
|
|
| @classmethod
|
| def from_url(cls, url: str, **kwargs) -> 'RedisConfig':
|
| return cls(url=url, **kwargs)
|
|
|
| @classmethod
|
| def for_database(cls, db_number: int, **kwargs) -> 'RedisConfig':
|
| """Create config for specific database number"""
|
| base_url = REDIS_URL.rsplit('/', 1)[0] if '/' in REDIS_URL else REDIS_URL
|
| url = f"{base_url}/{db_number}"
|
| return cls(url=url, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
| @dataclass
|
| class RedisMessage:
|
| """
|
| Mimics ably.types.message.Message interface.
|
|
|
| Ably messages have: .name (event), .data (payload), .timestamp, .id
|
| This wrapper provides the same attributes so existing callbacks work unchanged.
|
| """
|
| channel: str
|
| name: str
|
| data: Any
|
| timestamp: float = field(default_factory=time.time)
|
|
|
|
|
| @property
|
| def id(self) -> str:
|
| return f"{self.channel}:{int(self.timestamp * 1000)}"
|
|
|
|
|
| encoding: Optional[str] = None
|
| client_id: Optional[str] = None
|
| connection_id: Optional[str] = None
|
|
|
| def __repr__(self):
|
| data_preview = list(self.data.keys())[:3] if isinstance(self.data, dict) else '...'
|
| return f"RedisMessage(channel={self.channel}, event={self.name}, data_keys={data_preview})"
|
|
|
|
|
|
|
|
|
|
|
|
|
| @dataclass
|
| class Command:
|
| """Base command class for thread-safe operations."""
|
| pass
|
|
|
|
|
| @dataclass
|
| class SubscribeCommand(Command):
|
| channel: str
|
| callback: Optional[Callable] = None
|
| event_filter: Optional[str] = None
|
| latest_only: bool = False
|
|
|
|
|
| @dataclass
|
| class UnsubscribeCommand(Command):
|
| channel: str
|
|
|
|
|
| @dataclass
|
| class PublishCommand(Command):
|
| channel: str
|
| message: dict
|
| event_name: str = "message"
|
| response_event: Optional[threading.Event] = None
|
| result: Optional[int] = None
|
|
|
|
|
| @dataclass
|
| class GetStateCommand(Command):
|
| response_event: threading.Event = field(default_factory=threading.Event)
|
| result: Optional[dict] = None
|
|
|
|
|
| @dataclass
|
| class StopCommand(Command):
|
| pass
|
|
|
|
|
|
|
|
|
|
|
|
|
| class DedicatedRedisConnectionManager:
|
| """
|
| V10.1: Zero-latency Redis-based replacement for DedicatedAblyConnectionManager.
|
| HuggingFace Spaces optimized with container-safe configuration.
|
|
|
| KEY IMPROVEMENTS OVER V10.0:
|
| 1. HuggingFace Spaces environment detection
|
| 2. redis_config integration for consistent connection settings
|
| 3. Container-optimized timeouts and intervals
|
| 4. Enhanced error handling for container networking
|
| 5. Resource-efficient connection pooling
|
|
|
| IDENTICAL PUBLIC API:
|
| .start() → Start background threads
|
| .subscribe(channel, callback) → Subscribe to channel
|
| .publish(channel, message, event) → Publish message
|
| .get_state(timeout) → Get connection state
|
| .stop() → Stop manager
|
| .connected → Bool connection state
|
| .stats → Dict of statistics
|
| """
|
|
|
| def __init__(
|
| self,
|
| redis_url: Optional[str] = None,
|
| password: Optional[str] = None,
|
| on_connected: Optional[Callable] = None,
|
| on_disconnected: Optional[Callable] = None,
|
| on_message: Optional[Callable] = None,
|
| reconnect_interval: Optional[float] = None,
|
| max_reconnect_attempts: Optional[int] = None,
|
| use_streams: bool = False,
|
| stream_maxlen: int = 10000,
|
| database: int = 0,
|
|
|
| api_key: str = None,
|
| ):
|
|
|
| if redis_url is None:
|
| if database == REDIS_DB_FEATURES:
|
| config = RedisConfig.for_database(REDIS_DB_FEATURES)
|
| elif database == REDIS_DB_REWARDS:
|
| config = RedisConfig.for_database(REDIS_DB_REWARDS)
|
| else:
|
| config = RedisConfig.for_database(database)
|
| else:
|
| config = RedisConfig.from_url(redis_url)
|
|
|
|
|
| if password is not None:
|
| config.password = password
|
| if reconnect_interval is not None:
|
| config.reconnect_base_interval = reconnect_interval
|
| if max_reconnect_attempts is not None:
|
| config.reconnect_max_attempts = max_reconnect_attempts
|
|
|
| config.use_streams = use_streams
|
| config.stream_maxlen = stream_maxlen
|
|
|
| self.config = config
|
|
|
|
|
| self.on_connected = on_connected
|
| self.on_disconnected = on_disconnected
|
| self.on_message = on_message
|
|
|
| if api_key:
|
| logger.info(f"ℹ️ [DedicatedRedisConnectionManager] Ably API key ignored (migration compatibility)")
|
|
|
|
|
| self._redis: Optional[redis.Redis] = None
|
| self._pubsub: Optional[redis.client.PubSub] = None
|
| self._state = ConnectionState.INITIALIZED
|
| self._connected = False
|
| self._running = False
|
|
|
|
|
| self._listener_thread: Optional[threading.Thread] = None
|
| self._command_thread: Optional[threading.Thread] = None
|
| self._command_queue: queue.Queue = queue.Queue()
|
| self._shutdown_event = threading.Event()
|
|
|
|
|
| self._subscriptions: Dict[str, Callable] = {}
|
| self._subscription_lock = threading.RLock()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| self._latest_only_channels: set = set()
|
| self._latch_lock = threading.RLock()
|
|
|
|
|
| self._in_flight: Dict[str, bool] = {}
|
|
|
|
|
|
|
| self._pending_latest: Dict[str, Any] = {}
|
|
|
|
|
| self._latch_stats: Dict[str, int] = {
|
| 'messages_overwritten': 0,
|
| 'messages_dispatched': 0,
|
| 'busy_drops_avoided': 0,
|
| 'pending_slot_occupancy': 0,
|
| }
|
| self._latch_stats_lock = threading.RLock()
|
|
|
|
|
|
|
|
|
| self._stream_consumer_group = f"quasar-{int(time.time())}"
|
| self._stream_consumer_name = f"consumer-{os.getpid()}"
|
| self._stream_last_ids: Dict[str, str] = {}
|
| self._stream_dedup: set = set()
|
| self._stream_dedup_max = 5000
|
| self._stream_channels: List[str] = []
|
|
|
|
|
| self._stats = {
|
| 'connections': 0,
|
| 'disconnections': 0,
|
| 'reconnections': 0,
|
| 'messages_received': 0,
|
| 'messages_published': 0,
|
| 'publish_errors': 0,
|
| 'last_connected': None,
|
| 'total_uptime': 0,
|
| 'avg_latency_ms': 0,
|
| }
|
| self._stats_lock = threading.RLock()
|
| self._start_time = time.time()
|
|
|
| if IS_HF_SPACES:
|
| logger.info("🤗 HuggingFace Spaces environment detected - using container-optimized settings")
|
|
|
| logger.info(f"✅ DedicatedRedisConnectionManager V10.1 initialized")
|
| logger.info(f" Redis URL: {self.config.url}")
|
| logger.info(f" Database: {database}")
|
| logger.info(f" HF Spaces: {IS_HF_SPACES}")
|
|
|
| def start(self, timeout: float = 10.0) -> bool:
|
| """Start the connection manager with HuggingFace Spaces compatibility."""
|
| if self._running:
|
| logger.warning("Manager already running")
|
| return True
|
|
|
| try:
|
| logger.info("🚀 Starting DedicatedRedisConnectionManager V10.1...")
|
|
|
|
|
| actual_timeout = timeout * 2 if IS_HF_SPACES else timeout
|
|
|
|
|
| if not self._connect(timeout=actual_timeout):
|
| logger.error("❌ Failed to establish Redis connection")
|
| return False
|
|
|
|
|
| self._running = True
|
| self._start_threads()
|
|
|
|
|
| start_time = time.time()
|
| while not self._connected and (time.time() - start_time) < actual_timeout:
|
| time.sleep(0.1)
|
|
|
| if self._connected:
|
| logger.info(f"✅ DedicatedRedisConnectionManager started successfully")
|
| if self.on_connected:
|
| self.on_connected()
|
| return True
|
| else:
|
| logger.error("❌ Failed to confirm connection within timeout")
|
| self.stop()
|
| return False
|
|
|
| except Exception as e:
|
| logger.error(f"❌ Start error: {e}")
|
| self.stop()
|
| return False
|
|
|
| def _connect(self, timeout: float = 10.0) -> bool:
|
| """Establish Redis connection with HuggingFace Spaces resilience."""
|
| try:
|
| logger.info(f"🔌 Connecting to Redis: {self.config.url}")
|
|
|
|
|
| connection_kwargs = {
|
| 'decode_responses': self.config.decode_responses,
|
| 'socket_timeout': self.config.socket_timeout,
|
| 'socket_connect_timeout': self.config.socket_connect_timeout,
|
| 'health_check_interval': self.config.health_check_interval,
|
| 'retry_on_timeout': self.config.retry_on_timeout,
|
| }
|
|
|
| if self.config.password:
|
| connection_kwargs['password'] = self.config.password
|
|
|
| self._redis = redis.from_url(self.config.url, **connection_kwargs)
|
|
|
|
|
| self._redis.ping()
|
|
|
|
|
| pubsub_kwargs = connection_kwargs.copy()
|
|
|
|
|
|
|
|
|
| pubsub_kwargs['socket_timeout'] = 30
|
|
|
| pubsub_redis = redis.from_url(self.config.url, **pubsub_kwargs)
|
| self._pubsub = pubsub_redis.pubsub(ignore_subscribe_messages=True)
|
|
|
| self._state = ConnectionState.CONNECTED
|
| self._connected = True
|
|
|
| with self._stats_lock:
|
| self._stats['connections'] += 1
|
| self._stats['last_connected'] = datetime.now().isoformat()
|
|
|
| logger.info("✅ Redis connection established")
|
| return True
|
|
|
| except Exception as e:
|
| logger.error(f"❌ Redis connection failed: {e}")
|
| self._state = ConnectionState.FAILED
|
| self._connected = False
|
| return False
|
|
|
| def _start_threads(self):
|
| """Start background threads for listening and command processing."""
|
|
|
| self._command_thread = threading.Thread(
|
| target=self._command_loop,
|
| name="RedisCommandProcessor-V10.1",
|
| daemon=True
|
| )
|
| self._command_thread.start()
|
|
|
|
|
| self._listener_thread = threading.Thread(
|
| target=self._listener_loop,
|
| name="RedisListener-V10.1",
|
| daemon=True
|
| )
|
| self._listener_thread.start()
|
|
|
|
|
|
|
|
|
| if self.config.use_streams:
|
| self._stream_thread = threading.Thread(
|
| target=self._stream_catchup_loop,
|
| name="RedisStreamCatchup-V10.2",
|
| daemon=True
|
| )
|
| self._stream_thread.start()
|
| logger.info("🧵 Background threads started (pub/sub + stream catchup)")
|
| else:
|
| logger.info("🧵 Background threads started")
|
|
|
| def _command_loop(self):
|
| """Process commands from the queue (non-blocking thread)."""
|
| while self._running and not self._shutdown_event.is_set():
|
| try:
|
|
|
| command = self._command_queue.get(timeout=1.0)
|
|
|
| if isinstance(command, StopCommand):
|
| break
|
| elif isinstance(command, SubscribeCommand):
|
| self._handle_subscribe(command)
|
| elif isinstance(command, UnsubscribeCommand):
|
| self._handle_unsubscribe(command)
|
| elif isinstance(command, PublishCommand):
|
| self._handle_publish(command)
|
| elif isinstance(command, GetStateCommand):
|
| self._handle_get_state(command)
|
|
|
| self._command_queue.task_done()
|
|
|
| except queue.Empty:
|
| continue
|
| except Exception as e:
|
| logger.error(f"❌ Command processing error: {e}")
|
|
|
| def _listener_loop(self):
|
| """Listen for messages (BLOCKING thread with zero latency).
|
|
|
| ✅ ROOT CAUSE FIX: Added heartbeat detection via socket_timeout.
|
| If no messages arrive for STALL_THRESHOLD seconds, forces full reconnect.
|
| This catches silent pub/sub subscription drops that were causing buffer stalls.
|
| """
|
| reconnect_attempts = 0
|
| STALL_THRESHOLD = 120
|
|
|
|
|
| self._last_message_time = time.time()
|
| self._listener_alive = True
|
|
|
| while self._running and not self._shutdown_event.is_set():
|
| try:
|
| if not self._pubsub:
|
| time.sleep(self.config.reconnect_base_interval)
|
| continue
|
|
|
|
|
|
|
|
|
| for message in self._pubsub.listen():
|
| if not self._running or self._shutdown_event.is_set():
|
| break
|
|
|
| if message['type'] == 'message':
|
| self._handle_message(message)
|
| self._last_message_time = time.time()
|
|
|
| with self._stats_lock:
|
| self._stats['messages_received'] += 1
|
|
|
|
|
| reconnect_attempts = 0
|
|
|
| except (redis.TimeoutError, TimeoutError):
|
|
|
|
|
| silence_duration = time.time() - self._last_message_time
|
|
|
| if silence_duration > STALL_THRESHOLD:
|
| logger.warning(
|
| f"⚠️ [LISTENER] No messages for {silence_duration:.0f}s — "
|
| f"forcing reconnect (subscription may have silently dropped)"
|
| )
|
|
|
| self._connected = False
|
| self._state = ConnectionState.DISCONNECTED
|
|
|
| if self._connect():
|
| logger.info("✅ [LISTENER] Reconnected after silent stall")
|
| self._resubscribe_all()
|
| self._last_message_time = time.time()
|
|
|
| with self._stats_lock:
|
| self._stats['reconnections'] += 1
|
|
|
| if self.on_connected:
|
| self.on_connected()
|
| else:
|
| logger.error("❌ [LISTENER] Reconnection failed after silent stall")
|
| else:
|
|
|
| pass
|
|
|
| continue
|
|
|
| except redis.ConnectionError as e:
|
| logger.warning(f"⚠️ Redis connection lost: {e}")
|
| self._connected = False
|
| self._state = ConnectionState.DISCONNECTED
|
|
|
| if self.on_disconnected:
|
| self.on_disconnected()
|
|
|
|
|
| if reconnect_attempts < 10:
|
| reconnect_attempts += 1
|
|
|
| delay = min(
|
| self.config.reconnect_base_interval * (2 ** reconnect_attempts),
|
| self.config.reconnect_max_interval
|
| )
|
|
|
| logger.info(f"🔄 Reconnecting in {delay:.1f}s... (attempt {reconnect_attempts})")
|
| time.sleep(delay)
|
|
|
|
|
| if self._connect():
|
| logger.info("✅ Reconnected successfully")
|
| self._resubscribe_all()
|
| self._last_message_time = time.time()
|
|
|
| with self._stats_lock:
|
| self._stats['reconnections'] += 1
|
|
|
| if self.on_connected:
|
| self.on_connected()
|
|
|
| except Exception as e:
|
| logger.error(f"❌ Listener error: {e}")
|
| time.sleep(1)
|
|
|
| self._listener_alive = False
|
|
|
| def _stream_catchup_loop(self):
|
| """
|
| ✅ REDIS STREAMS CATCHUP CONSUMER (V10.2)
|
|
|
| Architecture inspired by:
|
| - Ably: Timeserial tracking, resume from exact position, 2-min recovery window
|
| - Redis Streams docs: Consumer groups, XREADGROUP, XACK for exactly-once processing
|
| - Arxiv (Borgarelli 2024): Lost rewards in distributed RL are catastrophic
|
| since policy learning propagates rewards backward in time
|
|
|
| This thread runs alongside the pub/sub listener:
|
| - Pub/sub: Real-time, zero-latency delivery (primary path)
|
| - Streams: Durable recovery for messages missed during disconnections
|
|
|
| When pub/sub is healthy, stream messages are deduped and skipped.
|
| When pub/sub stalls (silent disconnect), stream catchup delivers the missed rewards.
|
| """
|
| logger.info("🔄 [STREAM_CATCHUP] Starting Redis Streams recovery consumer")
|
|
|
|
|
| time.sleep(5)
|
|
|
|
|
| stream_redis = None
|
| catchup_stats = {
|
| 'messages_recovered': 0,
|
| 'messages_deduped': 0,
|
| 'read_errors': 0,
|
| 'reconnections': 0,
|
| }
|
|
|
| while self._running and not self._shutdown_event.is_set():
|
| try:
|
|
|
| if stream_redis is None:
|
| connection_kwargs = {
|
| 'decode_responses': True,
|
| 'socket_timeout': 10,
|
| 'socket_connect_timeout': 5,
|
| 'retry_on_timeout': True,
|
| }
|
| if self.config.password:
|
| connection_kwargs['password'] = self.config.password
|
|
|
| stream_redis = redis.from_url(self.config.url, **connection_kwargs)
|
| stream_redis.ping()
|
| logger.info("🔄 [STREAM_CATCHUP] Connected to Redis for stream reading")
|
|
|
|
|
| with self._subscription_lock:
|
| for channel in self._subscriptions:
|
| stream_key = f"stream:{channel}"
|
| try:
|
| stream_redis.xgroup_create(
|
| stream_key,
|
| self._stream_consumer_group,
|
| id='$',
|
| mkstream=True
|
| )
|
| if stream_key not in self._stream_channels:
|
| self._stream_channels.append(stream_key)
|
| except redis.ResponseError as e:
|
| if "BUSYGROUP" in str(e):
|
| if stream_key not in self._stream_channels:
|
| self._stream_channels.append(stream_key)
|
| else:
|
| logger.warning(f"⚠️ [STREAM_CATCHUP] Group create error: {e}")
|
|
|
|
|
| if not self._stream_channels:
|
| time.sleep(2)
|
| continue
|
|
|
| streams_to_read = {sk: '>' for sk in self._stream_channels}
|
|
|
|
|
|
|
| results = stream_redis.xreadgroup(
|
| self._stream_consumer_group,
|
| self._stream_consumer_name,
|
| streams_to_read,
|
| count=50,
|
| block=5000,
|
| noack=False
|
| )
|
|
|
| if not results:
|
| continue
|
|
|
| for stream_key, messages in results:
|
|
|
| channel_name = stream_key.replace("stream:", "", 1)
|
|
|
| for msg_id, msg_data in messages:
|
| try:
|
|
|
|
|
|
|
| envelope = {}
|
| for k, v in msg_data.items():
|
| try:
|
| envelope[k] = json.loads(v)
|
| except (json.JSONDecodeError, TypeError):
|
| envelope[k] = v
|
|
|
| event_name = envelope.get('event', 'message')
|
| data = envelope.get('data', envelope)
|
| timestamp = envelope.get('timestamp', time.time())
|
|
|
|
|
| dedup_key = f"{channel_name}:{timestamp}"
|
| if dedup_key in self._stream_dedup:
|
| catchup_stats['messages_deduped'] += 1
|
|
|
| stream_redis.xack(stream_key, self._stream_consumer_group, msg_id)
|
| continue
|
|
|
|
|
| redis_msg = RedisMessage(
|
| channel=channel_name,
|
| name=event_name,
|
| data=data,
|
| timestamp=timestamp if isinstance(timestamp, float) else time.time()
|
| )
|
|
|
|
|
| with self._subscription_lock:
|
| if channel_name in self._subscriptions:
|
| callback = self._subscriptions[channel_name]
|
| if callback:
|
| callback(redis_msg)
|
|
|
|
|
| stream_redis.xack(stream_key, self._stream_consumer_group, msg_id)
|
|
|
| catchup_stats['messages_recovered'] += 1
|
|
|
| if catchup_stats['messages_recovered'] % 100 == 0:
|
| logger.info(
|
| f"🔄 [STREAM_CATCHUP] Recovered {catchup_stats['messages_recovered']} msgs "
|
| f"(deduped: {catchup_stats['messages_deduped']}, "
|
| f"errors: {catchup_stats['read_errors']})"
|
| )
|
|
|
|
|
| if catchup_stats['messages_recovered'] == 1:
|
| logger.warning(
|
| f"🔄 [STREAM_CATCHUP] *** FIRST RECOVERY *** "
|
| f"Delivering missed {channel_name} message via stream! "
|
| f"Pub/sub may have dropped this."
|
| )
|
|
|
| except Exception as e:
|
| logger.error(f"❌ [STREAM_CATCHUP] Message processing error: {e}")
|
| catchup_stats['read_errors'] += 1
|
|
|
| try:
|
| stream_redis.xack(stream_key, self._stream_consumer_group, msg_id)
|
| except Exception:
|
| pass
|
|
|
| except redis.ConnectionError as e:
|
| logger.warning(f"⚠️ [STREAM_CATCHUP] Connection lost: {e}")
|
| stream_redis = None
|
| catchup_stats['reconnections'] += 1
|
| time.sleep(5)
|
|
|
| except redis.TimeoutError:
|
|
|
| continue
|
|
|
| except Exception as e:
|
| logger.error(f"❌ [STREAM_CATCHUP] Error: {e}")
|
| catchup_stats['read_errors'] += 1
|
| time.sleep(2)
|
|
|
|
|
| if stream_redis:
|
| try:
|
| stream_redis.close()
|
| except Exception:
|
| pass
|
|
|
| logger.info(
|
| f"🔄 [STREAM_CATCHUP] Stopped. Final stats: "
|
| f"recovered={catchup_stats['messages_recovered']}, "
|
| f"deduped={catchup_stats['messages_deduped']}, "
|
| f"errors={catchup_stats['read_errors']}"
|
| )
|
|
|
| def _handle_message(self, message: dict):
|
| """Handle incoming Redis message.
|
|
|
| For channels subscribed with latest_only=True, this method implements
|
| transport-side coalescing (the latest-state latch model):
|
|
|
| - If no callback is currently in-flight for this channel, fire the
|
| callback immediately and mark in_flight[channel] = True.
|
| - If a callback IS already in-flight (consumer is busy), store the
|
| arriving message in pending_latest[channel], overwriting any
|
| previously pending-but-unprocessed message. The overwritten message
|
| is intentionally discarded — we only ever want the freshest state.
|
| - When the callback finishes, it checks pending_latest and, if a newer
|
| message arrived while it was busy, dispatches that single pending
|
| message and clears the slot. All intermediate arrivals are silently
|
| dropped.
|
|
|
| Channels NOT registered as latest_only pass straight through the
|
| original queue-style path and are completely unaffected.
|
| """
|
| try:
|
| channel = message['channel']
|
| raw_data = message['data']
|
|
|
|
|
| try:
|
| if isinstance(raw_data, str):
|
| envelope = json.loads(raw_data)
|
| event_name = envelope.get('event', 'message')
|
| data = envelope.get('data', envelope)
|
| timestamp = envelope.get('timestamp', time.time())
|
| else:
|
| event_name = 'message'
|
| data = raw_data
|
| timestamp = time.time()
|
| except (json.JSONDecodeError, TypeError):
|
| event_name = 'message'
|
| data = raw_data
|
| timestamp = time.time()
|
|
|
|
|
| if self.config.use_streams and isinstance(timestamp, (int, float)):
|
| dedup_key = f"{channel}:{timestamp}"
|
| self._stream_dedup.add(dedup_key)
|
| if len(self._stream_dedup) > self._stream_dedup_max:
|
| excess = len(self._stream_dedup) - self._stream_dedup_max // 2
|
| for _ in range(excess):
|
| try:
|
| self._stream_dedup.pop()
|
| except KeyError:
|
| break
|
|
|
|
|
| redis_msg = RedisMessage(
|
| channel=channel,
|
| name=event_name,
|
| data=data,
|
| timestamp=timestamp
|
| )
|
|
|
|
|
| if self.on_message:
|
| self.on_message(channel, redis_msg)
|
|
|
|
|
|
|
|
|
|
|
| with self._subscription_lock:
|
| is_latest_only = channel in self._latest_only_channels
|
| callback = self._subscriptions.get(channel)
|
|
|
| if not callback:
|
| return
|
|
|
| if not is_latest_only:
|
|
|
|
|
|
|
| callback(redis_msg)
|
| return
|
|
|
|
|
|
|
|
|
|
|
|
|
| with self._latch_lock:
|
| busy = self._in_flight.get(channel, False)
|
| if busy:
|
|
|
|
|
| had_pending = channel in self._pending_latest
|
| self._pending_latest[channel] = redis_msg
|
| with self._latch_stats_lock:
|
| self._latch_stats['busy_drops_avoided'] += 1
|
| if had_pending:
|
|
|
| self._latch_stats['messages_overwritten'] += 1
|
| logger.debug(
|
| f"[LATCH] {channel}: consumer busy — pending_latest updated "
|
| f"(ts={timestamp:.4f}, overwrite={'yes' if had_pending else 'no'})"
|
| )
|
| return
|
|
|
| self._in_flight[channel] = True
|
|
|
|
|
|
|
| self._dispatch_latest_and_drain(channel, redis_msg, callback)
|
|
|
| except Exception as e:
|
| logger.error(f"❌ Message handling error: {e}")
|
|
|
| def _dispatch_latest_and_drain(self, channel: str, redis_msg: Any, callback: Callable):
|
| """Fire callback for a latest_only channel and drain any pending slot.
|
|
|
| This implements the tail of the latch model:
|
| 1. Fire callback with the supplied message (marks consumer as busy).
|
| 2. After callback returns, atomically check whether a newer message
|
| arrived and was stored in pending_latest[channel].
|
| 3. If yes, fire the callback with that newest message and repeat.
|
| 4. If no pending message, clear in_flight and return idle.
|
|
|
| The loop guarantees that if N messages arrive while we are busy, exactly
|
| ONE more callback fires (with the freshest of the N), not N callbacks.
|
| All intermediate messages are permanently discarded — intentionally.
|
| """
|
| while True:
|
| try:
|
| callback(redis_msg)
|
| with self._latch_stats_lock:
|
| self._latch_stats['messages_dispatched'] += 1
|
| except Exception as e:
|
| logger.error(f"❌ [LATCH] Callback error on {channel}: {e}")
|
|
|
|
|
| with self._latch_lock:
|
| next_msg = self._pending_latest.pop(channel, None)
|
| if next_msg is None:
|
|
|
| self._in_flight[channel] = False
|
| logger.debug(f"[LATCH] {channel}: callback done, no pending — going idle")
|
| return
|
|
|
| logger.debug(
|
| f"[LATCH] {channel}: callback done, dispatching freshest pending "
|
| f"(ts={next_msg.timestamp:.4f})"
|
| )
|
| redis_msg = next_msg
|
|
|
|
|
| def _handle_subscribe(self, command: SubscribeCommand):
|
| """Handle subscription command.
|
|
|
| If command.latest_only is True, the channel is registered in the latch
|
| model and will receive only the freshest pending message after each
|
| processing cycle (collapse-to-freshest). All other channels remain
|
| lossless queue-style consumers.
|
| """
|
| try:
|
| with self._subscription_lock:
|
| self._subscriptions[command.channel] = command.callback
|
|
|
|
|
|
|
|
|
| if command.latest_only:
|
| with self._latch_lock:
|
| self._latest_only_channels.add(command.channel)
|
| self._in_flight.setdefault(command.channel, False)
|
|
|
| self._pending_latest.pop(command.channel, None)
|
| logger.info(
|
| f"📡 [LATCH] Subscribed to channel (latest_only=True): {command.channel}"
|
| )
|
| else:
|
| logger.info(f"📡 Subscribed to channel: {command.channel}")
|
|
|
| if self._pubsub:
|
| self._pubsub.subscribe(command.channel)
|
|
|
|
|
| if self.config.use_streams and self._redis:
|
| stream_key = f"stream:{command.channel}"
|
| try:
|
|
|
|
|
| self._redis.xgroup_create(
|
| stream_key,
|
| self._stream_consumer_group,
|
| id='$',
|
| mkstream=True
|
| )
|
| self._stream_channels.append(stream_key)
|
| logger.info(f"📡 Stream consumer group created: {stream_key} → {self._stream_consumer_group}")
|
| except redis.ResponseError as e:
|
| if "BUSYGROUP" in str(e):
|
|
|
| if stream_key not in self._stream_channels:
|
| self._stream_channels.append(stream_key)
|
| logger.debug(f"📡 Stream consumer group already exists: {stream_key}")
|
| else:
|
| logger.error(f"❌ Stream group create error for {stream_key}: {e}")
|
| except Exception as e:
|
| logger.error(f"❌ Stream setup error for {stream_key}: {e}")
|
|
|
| except Exception as e:
|
| logger.error(f"❌ Subscribe error for {command.channel}: {e}")
|
|
|
| def _handle_unsubscribe(self, command: UnsubscribeCommand):
|
| """Handle unsubscription command."""
|
| try:
|
| with self._subscription_lock:
|
| if command.channel in self._subscriptions:
|
| del self._subscriptions[command.channel]
|
|
|
|
|
|
|
|
|
| with self._latch_lock:
|
| self._latest_only_channels.discard(command.channel)
|
| self._in_flight.pop(command.channel, None)
|
| self._pending_latest.pop(command.channel, None)
|
|
|
| if self._pubsub:
|
| self._pubsub.unsubscribe(command.channel)
|
| logger.info(f"📡 Unsubscribed from channel: {command.channel}")
|
|
|
| except Exception as e:
|
| logger.error(f"❌ Unsubscribe error for {command.channel}: {e}")
|
|
|
| def _handle_publish(self, command: PublishCommand):
|
| """Handle publish command."""
|
| try:
|
| if not self._redis:
|
| raise ConnectionError("Redis not connected")
|
|
|
|
|
| envelope = {
|
| 'event': command.event_name,
|
| 'data': command.message,
|
| 'timestamp': time.time()
|
| }
|
|
|
|
|
| result = self._redis.publish(command.channel, json.dumps(envelope))
|
|
|
| with self._stats_lock:
|
| self._stats['messages_published'] += 1
|
|
|
|
|
| if command.response_event:
|
| command.result = result
|
| command.response_event.set()
|
|
|
| except Exception as e:
|
| logger.error(f"❌ Publish error for {command.channel}: {e}")
|
|
|
| with self._stats_lock:
|
| self._stats['publish_errors'] += 1
|
|
|
| if command.response_event:
|
| command.result = 0
|
| command.response_event.set()
|
|
|
| def _handle_get_state(self, command: GetStateCommand):
|
| """Handle get state command."""
|
| try:
|
| with self._subscription_lock:
|
| subscriptions = list(self._subscriptions.keys())
|
|
|
| with self._stats_lock:
|
| stats = self._stats.copy()
|
| stats['total_uptime'] = time.time() - self._start_time
|
|
|
|
|
| with self._latch_lock:
|
| latest_only_channels = sorted(self._latest_only_channels)
|
| pending_occupancy = sum(
|
| 1 for ch in self._latest_only_channels if ch in self._pending_latest
|
| )
|
| in_flight_channels = [ch for ch, busy in self._in_flight.items() if busy]
|
|
|
| with self._latch_stats_lock:
|
| latch_stats = self._latch_stats.copy()
|
| latch_stats['pending_slot_occupancy'] = pending_occupancy
|
|
|
| command.result = {
|
| 'connected': self._connected,
|
| 'state': self._state.name.lower(),
|
| 'subscriptions': subscriptions,
|
| 'stats': stats,
|
| 'reconnect_attempts': 0,
|
| 'config': {
|
| 'redis_url': self.config.url,
|
| 'use_streams': self.config.use_streams,
|
| 'socket_timeout': self.config.socket_timeout,
|
| 'is_hf_spaces': IS_HF_SPACES
|
| },
|
|
|
| 'latch_model': {
|
| 'latest_only_enabled': bool(latest_only_channels),
|
| 'latest_only_channels': latest_only_channels,
|
| 'in_flight_channels': in_flight_channels,
|
| 'messages_overwritten': latch_stats['messages_overwritten'],
|
| 'messages_dispatched': latch_stats['messages_dispatched'],
|
| 'busy_drops_avoided': latch_stats['busy_drops_avoided'],
|
| 'pending_slot_occupancy': latch_stats['pending_slot_occupancy'],
|
| }
|
| }
|
|
|
| except Exception as e:
|
| logger.error(f"❌ Get state error: {e}")
|
| command.result = {'error': str(e)}
|
| finally:
|
| command.response_event.set()
|
|
|
| def _resubscribe_all(self):
|
| """Resubscribe to all channels after reconnection."""
|
| try:
|
| with self._subscription_lock:
|
| channels = list(self._subscriptions.keys())
|
|
|
| for channel in channels:
|
| if self._pubsub:
|
| self._pubsub.subscribe(channel)
|
|
|
| logger.info(f"🔄 Resubscribed to {len(channels)} channels")
|
|
|
| except Exception as e:
|
| logger.error(f"❌ Resubscription error: {e}")
|
|
|
|
|
| def subscribe(
|
| self,
|
| channel: str,
|
| callback: Optional[Callable] = None,
|
| event_filter: Optional[str] = None,
|
| latest_only: bool = False,
|
| ):
|
| """Subscribe to a channel.
|
|
|
| Args:
|
| channel: Redis channel name.
|
| callback: Callable invoked with a RedisMessage on each delivery.
|
| event_filter: Optional event name filter (passed through to SubscribeCommand).
|
| latest_only: If True, enables the latest-state latch model for this
|
| channel. While the callback is executing, any new arrivals
|
| overwrite the single pending slot rather than accumulating
|
| a backlog. When the callback finishes, only the freshest
|
| pending message is delivered — all intermediate arrivals are
|
| discarded. Use this for hot-path live-state/feature channels
|
| where QUASAR must never replay stale feature transactions.
|
| Do NOT use for reward, audit, or replay channels — those
|
| require lossless delivery and remain queue-style consumers.
|
| """
|
| command = SubscribeCommand(
|
| channel=channel,
|
| callback=callback,
|
| event_filter=event_filter,
|
| latest_only=latest_only,
|
| )
|
| self._command_queue.put(command)
|
|
|
| def unsubscribe(self, channel: str):
|
| """Unsubscribe from a channel."""
|
| command = UnsubscribeCommand(channel=channel)
|
| self._command_queue.put(command)
|
|
|
| def publish(self, channel: str, message: dict, event_name: str = "message") -> int:
|
| """Publish a message to a channel."""
|
| response_event = threading.Event()
|
| command = PublishCommand(
|
| channel=channel,
|
| message=message,
|
| event_name=event_name,
|
| response_event=response_event
|
| )
|
|
|
| self._command_queue.put(command)
|
|
|
|
|
| if response_event.wait(timeout=5.0):
|
| return command.result or 0
|
| else:
|
| logger.warning(f"⏰ Publish timeout for channel: {channel}")
|
| return 0
|
|
|
| def get_state(self, timeout: float = 2.0) -> dict:
|
| """Get current connection state."""
|
| command = GetStateCommand()
|
| self._command_queue.put(command)
|
|
|
| if command.response_event.wait(timeout=timeout):
|
| return command.result or {}
|
| else:
|
| return {'error': 'State request timeout'}
|
|
|
| def stop(self):
|
| """Stop the connection manager."""
|
| if not self._running:
|
| return
|
|
|
| logger.info("🛑 Stopping DedicatedRedisConnectionManager...")
|
|
|
| self._running = False
|
| self._shutdown_event.set()
|
|
|
|
|
| self._command_queue.put(StopCommand())
|
|
|
|
|
| try:
|
| if self._pubsub:
|
| self._pubsub.unsubscribe()
|
| self._pubsub.close()
|
| except Exception as e:
|
| logger.debug(f"PubSub close error: {e}")
|
|
|
| try:
|
| if self._redis:
|
| self._redis.close()
|
| except Exception as e:
|
| logger.debug(f"Redis close error: {e}")
|
|
|
|
|
| for thread, name in [(self._command_thread, "Command"), (self._listener_thread, "Listener")]:
|
| if thread and thread.is_alive():
|
| thread.join(timeout=2.0)
|
| if thread.is_alive():
|
| logger.warning(f"⏰ {name} thread did not stop gracefully")
|
|
|
| self._state = ConnectionState.CLOSED
|
| self._connected = False
|
|
|
| with self._stats_lock:
|
| self._stats['disconnections'] += 1
|
|
|
| logger.info("✅ DedicatedRedisConnectionManager stopped")
|
|
|
| @property
|
| def connected(self) -> bool:
|
| """Check if connected."""
|
| return self._connected
|
|
|
| @property
|
| def stats(self) -> dict:
|
| """Get statistics."""
|
| with self._stats_lock:
|
| stats = self._stats.copy()
|
| stats['total_uptime'] = time.time() - self._start_time
|
| return stats
|
|
|
| @property
|
| def latch_stats(self) -> dict:
|
| """Snapshot of latest-state latch coalescing counters (thread-safe).
|
|
|
| Returns a dict with:
|
| latest_only_enabled — True if any channel uses latch mode.
|
| messages_overwritten — Arrivals that replaced a waiting pending slot.
|
| messages_dispatched — Callbacks actually fired on latest_only channels.
|
| busy_drops_avoided — Times a new arrival was coalesced (consumer was busy).
|
| pending_slot_occupancy — Current count of occupied pending-latest slots.
|
| """
|
| with self._latch_lock:
|
| pending_occupancy = sum(
|
| 1 for ch in self._latest_only_channels if ch in self._pending_latest
|
| )
|
| latest_only_enabled = bool(self._latest_only_channels)
|
| with self._latch_stats_lock:
|
| snap = self._latch_stats.copy()
|
| snap['latest_only_enabled'] = latest_only_enabled
|
| snap['pending_slot_occupancy'] = pending_occupancy
|
| return snap
|
|
|
| def get_latch_diagnostics(self) -> dict:
|
| """Return detailed latch-model state for monitoring and debugging.
|
|
|
| Exposes all latch internals without going through the command queue,
|
| so it is safe to call from any thread (including QUASAR's main loop)
|
| without incurring dispatch latency.
|
|
|
| Fields:
|
| latest_only_channels — Names of all channels in latch mode.
|
| in_flight_channels — Channels currently executing their callback.
|
| pending_channels — Channels with a waiting (not-yet-dispatched) message.
|
| messages_overwritten — Total arrivals that overwrote an existing pending slot.
|
| messages_dispatched — Total callbacks fired via the latch path.
|
| busy_drops_avoided — Total coalesced arrivals (consumer was busy).
|
| pending_slot_occupancy — Current number of occupied pending slots.
|
| """
|
| with self._latch_lock:
|
| latest_only_channels = sorted(self._latest_only_channels)
|
| in_flight_channels = [ch for ch, busy in self._in_flight.items() if busy]
|
| pending_channels = list(self._pending_latest.keys())
|
| pending_occupancy = len(pending_channels)
|
|
|
| with self._latch_stats_lock:
|
| snap = self._latch_stats.copy()
|
|
|
| snap['latest_only_enabled'] = bool(latest_only_channels)
|
| snap['latest_only_channels'] = latest_only_channels
|
| snap['in_flight_channels'] = in_flight_channels
|
| snap['pending_channels'] = pending_channels
|
| snap['pending_slot_occupancy'] = pending_occupancy
|
| return snap
|
|
|
|
|
|
|
|
|
|
|
|
|
| class RedisAblyChannel:
|
| """
|
| V10.1: HuggingFace Spaces optimized Ably-compatible channel wrapper.
|
| """
|
|
|
| def __init__(self, name: str, redis_client: redis.Redis, pubsub: redis.client.PubSub, use_streams: bool = False):
|
| self.name = name
|
| self._redis = redis_client
|
| self._pubsub = pubsub
|
| self._use_streams = use_streams
|
| self._callbacks: Dict[str, List[Callable]] = {}
|
| self._subscribed = False
|
|
|
| if IS_HF_SPACES:
|
| logger.debug(f"🤗 RedisAblyChannel {name} optimized for HuggingFace Spaces")
|
|
|
| async def publish(self, event_name: str, data: Any) -> int:
|
| """Publish message to channel."""
|
| try:
|
| envelope = {
|
| 'event': event_name,
|
| 'data': data,
|
| 'timestamp': time.time(),
|
| 'channel': self.name
|
| }
|
|
|
| if self._use_streams:
|
|
|
| stream_key = f"stream:{self.name}"
|
|
|
|
|
| stream_data = {
|
| k: json.dumps(v) if isinstance(v, (dict, list)) else str(v)
|
| for k, v in envelope.items()
|
| }
|
| result = self._redis.xadd(stream_key, stream_data, maxlen=10000)
|
| logger.debug(f"📤 Published to stream {stream_key}: {result}")
|
|
|
|
|
| pub_result = self._redis.publish(self.name, json.dumps(envelope))
|
| return pub_result
|
| else:
|
|
|
| result = self._redis.publish(self.name, json.dumps(envelope))
|
| logger.debug(f"📤 Published to channel {self.name}: {result} subscribers")
|
| return result
|
|
|
| except Exception as e:
|
| logger.error(f"❌ Publish error on channel {self.name}: {e}")
|
| return 0
|
|
|
| async def subscribe(self, event_name: str, callback: Callable):
|
| """Subscribe to events on this channel."""
|
| if event_name not in self._callbacks:
|
| self._callbacks[event_name] = []
|
|
|
| self._callbacks[event_name].append(callback)
|
|
|
|
|
| if not self._subscribed:
|
| self._pubsub.subscribe(self.name)
|
| self._subscribed = True
|
| logger.info(f"📡 Subscribed to Redis channel: {self.name}")
|
|
|
| logger.debug(f"📡 Added callback for {self.name}:{event_name}")
|
|
|
| def _dispatch(self, event_name: str, data: Any, timestamp: float = None):
|
| """Dispatch message to callbacks (called by listener thread)."""
|
| if event_name in self._callbacks:
|
| message = RedisMessage(
|
| channel=self.name,
|
| name=event_name,
|
| data=data,
|
| timestamp=timestamp or time.time()
|
| )
|
|
|
| for callback in self._callbacks[event_name]:
|
| try:
|
| callback(message)
|
| except Exception as e:
|
| logger.error(f"❌ Callback error on {self.name}:{event_name}: {e}")
|
|
|
|
|
| class RedisAblyChannelCollection:
|
| """Collection of Redis channels (HuggingFace Spaces optimized)."""
|
|
|
| def __init__(self, redis_client: redis.Redis, pubsub: redis.client.PubSub, use_streams: bool = False):
|
| self._redis = redis_client
|
| self._pubsub = pubsub
|
| self._use_streams = use_streams
|
| self._channels: Dict[str, RedisAblyChannel] = {}
|
|
|
| def get(self, channel_name: str) -> RedisAblyChannel:
|
| """Get or create a channel."""
|
| if channel_name not in self._channels:
|
| self._channels[channel_name] = RedisAblyChannel(
|
| channel_name, self._redis, self._pubsub, self._use_streams
|
| )
|
| return self._channels[channel_name]
|
|
|
|
|
| class RedisConnectionState:
|
| """Ably-compatible connection state object."""
|
|
|
| def __init__(self):
|
| self.state = 'initialized'
|
| self._callbacks: Dict[str, List[Callable]] = {}
|
|
|
| def on(self, event: str, callback: Callable):
|
| """Register event callback."""
|
| if event not in self._callbacks:
|
| self._callbacks[event] = []
|
| self._callbacks[event].append(callback)
|
|
|
| def _trigger(self, event: str, *args):
|
| """Trigger event callbacks."""
|
| if event in self._callbacks:
|
| for callback in self._callbacks[event]:
|
| try:
|
| callback(*args)
|
| except Exception as e:
|
| logger.error(f"❌ Connection event callback error: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| class RedisAblyClient:
|
| """
|
| V10.1: Simplified Redis client with Ably-compatible interface.
|
| HuggingFace Spaces optimized with environment-aware configuration.
|
| """
|
|
|
| def __init__(
|
| self,
|
| redis_url: Optional[str] = None,
|
| key: Optional[str] = None,
|
| password: Optional[str] = None,
|
| use_streams: bool = False,
|
| database: int = 0,
|
| ):
|
| if key:
|
| logger.info("ℹ️ [RedisAblyClient] Ably key ignored (migration compatibility)")
|
|
|
|
|
| if redis_url is None:
|
| if database == REDIS_DB_FEATURES:
|
| redis_url = RedisConfig.for_database(REDIS_DB_FEATURES).url
|
| elif database == REDIS_DB_REWARDS:
|
| redis_url = RedisConfig.for_database(REDIS_DB_REWARDS).url
|
| else:
|
| redis_url = RedisConfig.for_database(database).url
|
|
|
| self._redis_url = redis_url
|
|
|
|
|
| connection_kwargs = {
|
| 'decode_responses': True,
|
| 'socket_timeout': 15 if IS_HF_SPACES else 10,
|
| 'socket_connect_timeout': 15 if IS_HF_SPACES else 10,
|
| 'retry_on_timeout': True,
|
| }
|
|
|
| if password or REDIS_PASSWORD:
|
| connection_kwargs['password'] = password or REDIS_PASSWORD
|
|
|
| self._redis = redis.Redis.from_url(redis_url, **connection_kwargs)
|
|
|
|
|
|
|
|
|
|
|
| pubsub_kwargs = connection_kwargs.copy()
|
| pubsub_kwargs['socket_timeout'] = 30
|
|
|
| self._sub_redis = redis.Redis.from_url(redis_url, **pubsub_kwargs)
|
| self._pubsub = self._sub_redis.pubsub(ignore_subscribe_messages=True)
|
|
|
|
|
| self.channels = RedisAblyChannelCollection(
|
| self._redis, self._pubsub, use_streams
|
| )
|
| self.connection = RedisConnectionState()
|
|
|
|
|
| self._running = True
|
| self._listener_thread = threading.Thread(
|
| target=self._listener_loop,
|
| daemon=True,
|
| name="RedisAblyClientListener-V10.1"
|
| )
|
| self._listener_thread.start()
|
|
|
| self.connection.state = 'connected'
|
| self.connection._trigger('connected')
|
|
|
| if IS_HF_SPACES:
|
| logger.info(f"✅ RedisAblyClient V10.1 (HF Spaces) connected to {redis_url}")
|
| else:
|
| logger.info(f"✅ RedisAblyClient V10.1 connected to {redis_url}")
|
|
|
| def _listener_loop(self):
|
| """
|
| BLOCKING listener using pubsub.listen() - zero polling latency!
|
| HuggingFace Spaces optimized with enhanced error handling.
|
|
|
| ✅ FIX: Added heartbeat via socket_timeout=30 and stall detection.
|
| This is the Rewards.py side — if this listener dies, Rewards.py stops
|
| receiving signals and NO rewards get calculated for ANY consumer.
|
| """
|
| reconnect_attempts = 0
|
| STALL_THRESHOLD = 120
|
| self._last_ably_message_time = time.time()
|
|
|
| while self._running:
|
| try:
|
|
|
| for message in self._pubsub.listen():
|
| if not self._running:
|
| break
|
|
|
| if message['type'] == 'message':
|
| channel_name = message['channel']
|
| raw_data = message['data']
|
| self._last_ably_message_time = time.time()
|
|
|
| try:
|
| envelope = json.loads(raw_data)
|
| event_name = envelope.get('event', 'message')
|
| data = envelope.get('data', envelope)
|
| timestamp = envelope.get('timestamp', time.time())
|
| except (json.JSONDecodeError, TypeError):
|
| event_name = 'message'
|
| data = raw_data
|
| timestamp = time.time()
|
|
|
| if channel_name in self.channels._channels:
|
| self.channels._channels[channel_name]._dispatch(
|
| event_name, data, timestamp
|
| )
|
|
|
|
|
| reconnect_attempts = 0
|
|
|
| except (redis.TimeoutError, TimeoutError):
|
|
|
| silence = time.time() - self._last_ably_message_time
|
|
|
| if silence > STALL_THRESHOLD:
|
| logger.warning(
|
| f"[RedisAblyClient] ⚠️ No messages for {silence:.0f}s — "
|
| f"forcing reconnect (subscription may have silently dropped)"
|
| )
|
|
|
| try:
|
| connection_kwargs = {
|
| 'decode_responses': True,
|
| 'socket_timeout': 30,
|
| 'retry_on_timeout': True,
|
| }
|
| if REDIS_PASSWORD:
|
| connection_kwargs['password'] = REDIS_PASSWORD
|
|
|
| self._sub_redis = redis.Redis.from_url(self._redis_url, **connection_kwargs)
|
| self._pubsub = self._sub_redis.pubsub(ignore_subscribe_messages=True)
|
|
|
|
|
| for ch in self.channels._channels:
|
| self._pubsub.subscribe(ch)
|
|
|
| self._last_ably_message_time = time.time()
|
| logger.info(f"[RedisAblyClient] ✅ Reconnected after silent stall, re-subscribed to {len(self.channels._channels)} channels")
|
| except Exception as e:
|
| logger.error(f"[RedisAblyClient] ❌ Reconnection failed: {e}")
|
| time.sleep(5)
|
|
|
| continue
|
|
|
| except redis.ConnectionError:
|
| logger.warning("[RedisAblyClient] ⚠️ Connection lost, reconnecting...")
|
|
|
|
|
| if reconnect_attempts < 10:
|
| reconnect_attempts += 1
|
|
|
| delay = min(2 ** reconnect_attempts, 60) if IS_HF_SPACES else min(2 ** reconnect_attempts, 30)
|
| time.sleep(delay)
|
|
|
| try:
|
| self._sub_redis.ping()
|
| except Exception:
|
|
|
| connection_kwargs = {
|
| 'decode_responses': True,
|
| 'socket_timeout': 30,
|
| 'retry_on_timeout': True,
|
| }
|
| if REDIS_PASSWORD:
|
| connection_kwargs['password'] = REDIS_PASSWORD
|
|
|
| self._sub_redis = redis.Redis.from_url(self._redis_url, **connection_kwargs)
|
| self._pubsub = self._sub_redis.pubsub(ignore_subscribe_messages=True)
|
|
|
|
|
| for ch in self.channels._channels:
|
| self._pubsub.subscribe(ch)
|
|
|
| self._last_ably_message_time = time.time()
|
|
|
| except Exception as e:
|
| if self._running:
|
| logger.error(f"[RedisAblyClient] ❌ Listener error: {e}")
|
| time.sleep(1 if not IS_HF_SPACES else 2)
|
|
|
| def close(self):
|
| """Close connections."""
|
| self._running = False
|
| try:
|
| self._pubsub.unsubscribe()
|
| self._pubsub.close()
|
| except Exception:
|
| pass
|
| try:
|
| self._redis.close()
|
| self._sub_redis.close()
|
| except Exception:
|
| pass
|
| self.connection.state = 'closed'
|
| logger.info("✅ RedisAblyClient closed")
|
|
|
|
|
|
|
|
|
|
|
|
|
| def diagnose_redis_connection(system, verbose: bool = True) -> Optional[dict]:
|
| """
|
| V10.2: Redis diagnostics with HuggingFace Spaces awareness and latch model reporting.
|
|
|
| Args:
|
| system: Object with .ably_manager attribute
|
| verbose: Print detailed output
|
|
|
| Returns:
|
| Connection state dict (includes 'latch_model' sub-dict)
|
| """
|
| manager = getattr(system, 'ably_manager', None)
|
| if manager is None:
|
| logger.warning("⚠️ No Redis manager found on system")
|
| return None
|
|
|
| if verbose:
|
| print(f"\n{'='*80}")
|
| print(f"📡 REDIS CONNECTION DIAGNOSTICS (V10.2 - HuggingFace Spaces)")
|
| print(f"{'='*80}")
|
| print(f"🤗 HuggingFace Spaces: {IS_HF_SPACES}")
|
| print(f"📋 Redis Config Available: {HAS_REDIS_CONFIG}")
|
|
|
| try:
|
| state = manager.get_state(timeout=5 if IS_HF_SPACES else 2)
|
|
|
| if verbose:
|
| print(f" Connected: {state.get('connected', 'unknown')}")
|
| print(f" State: {state.get('state', 'unknown')}")
|
| print(f" Reconnect attempts: {state.get('reconnect_attempts', 0)}")
|
|
|
| config = state.get('config', {})
|
| print(f" Redis URL: {config.get('redis_url', 'unknown')}")
|
| print(f" Socket timeout: {config.get('socket_timeout', 'unknown')}")
|
|
|
| stats = state.get('stats', {})
|
| print(f"\n 📊 Statistics:")
|
| print(f" Connections: {stats.get('connections', 0)}")
|
| print(f" Disconnections: {stats.get('disconnections', 0)}")
|
| print(f" Reconnections: {stats.get('reconnections', 0)}")
|
| print(f" Messages recv: {stats.get('messages_received', 0)}")
|
| print(f" Messages pub: {stats.get('messages_published', 0)}")
|
| print(f" Publish errors: {stats.get('publish_errors', 0)}")
|
| print(f" Avg latency: {stats.get('avg_latency_ms', 0):.2f}ms")
|
| print(f" Last connected: {stats.get('last_connected', 'never')}")
|
| print(f" Uptime: {stats.get('total_uptime', 0):.0f}s")
|
|
|
|
|
| latch = state.get('latch_model', {})
|
| print(f"\n 🔒 Latest-State Latch Model (V10.2):")
|
| print(f" Enabled: {latch.get('latest_only_enabled', False)}")
|
| lo_channels = latch.get('latest_only_channels', [])
|
| if lo_channels:
|
| print(f" Latest-only channels: {', '.join(lo_channels)}")
|
| else:
|
| print(f" Latest-only channels: (none)")
|
| if_channels = latch.get('in_flight_channels', [])
|
| print(f" In-flight now: {', '.join(if_channels) if if_channels else '(none)'}")
|
| print(f" Pending slot occupancy: {latch.get('pending_slot_occupancy', 0)}")
|
| print(f" Messages dispatched: {latch.get('messages_dispatched', 0)}")
|
| print(f" Messages overwritten: {latch.get('messages_overwritten', 0)}")
|
| print(f" Busy drops avoided: {latch.get('busy_drops_avoided', 0)}")
|
|
|
| subs = state.get('subscriptions', [])
|
| print(f"\n 📌 Subscriptions ({len(subs)}):")
|
| for ch in subs[:10]:
|
| tag = " [latest_only]" if ch in lo_channels else ""
|
| print(f" • {ch}{tag}")
|
| if len(subs) > 10:
|
| print(f" ... and {len(subs) - 10} more")
|
|
|
| print(f"{'='*80}\n")
|
|
|
| return state
|
|
|
| except Exception as e:
|
| if verbose:
|
| print(f" ❌ Diagnostic error: {e}")
|
| print(f"{'='*80}\n")
|
| return {'error': str(e)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| class FeatureBatchGateway:
|
| """
|
| Redis-side batch synchronisation gate.
|
|
|
| Subscribes to N per-agent feature channels via a
|
| DedicatedRedisConnectionManager. Accumulates per-tick contributions
|
| and fires on_batch_ready only when all N agents have arrived for the
|
| same tick_index. Any partial batch for a superseded tick is silently
|
| discarded — the downstream consumer (QSAP / Erasure engine) therefore
|
| always receives a coherent, same-price, all-agents feature set.
|
|
|
| Usage
|
| -----
|
| gateway = FeatureBatchGateway(
|
| manager = redis_manager,
|
| agent_names = ['xs','s','m','l','xl','xxl','5m','10m'],
|
| channel_prefix = 'V75_1S:',
|
| on_batch_ready = lambda batch: asyncio.ensure_future(
|
| system.on_tick_batch_message(batch)
|
| ),
|
| )
|
| gateway.start()
|
| """
|
|
|
| def __init__(
|
| self,
|
| manager,
|
| agent_names: list,
|
| on_batch_ready,
|
| channel_prefix: str = '',
|
| n_required: int = None,
|
| stale_timeout_s: float = 10.0,
|
| ):
|
| self._manager = manager
|
| self._agent_names = list(agent_names)
|
| self._n_required = n_required or len(agent_names)
|
| self._on_batch = on_batch_ready
|
| self._prefix = channel_prefix
|
| self._stale_timeout = stale_timeout_s
|
|
|
|
|
| self._lock = threading.Lock()
|
| self._current_tick = None
|
| self._batch: dict = {}
|
| self._batch_price: float = 0.0
|
| self._batch_ts: float = 0.0
|
|
|
|
|
| self._batches_fired: int = 0
|
| self._partial_discards: int = 0
|
| self._agent_overwrites: int = 0
|
|
|
| self._started = False
|
| logger.info("[FeatureBatchGateway] Created: agents=%s n_required=%d prefix=%r",
|
| self._agent_names, self._n_required, self._prefix)
|
|
|
|
|
|
|
| def start(self) -> None:
|
| """Subscribe to all per-agent channels through the manager."""
|
| if self._started:
|
| logger.warning("[FeatureBatchGateway] Already started — ignoring duplicate start()")
|
| return
|
|
|
| for agent in self._agent_names:
|
| channel = f"{self._prefix}{agent}"
|
| self._manager.subscribe(
|
| channel = channel,
|
| callback = self._make_handler(agent),
|
| latest_only = True,
|
| )
|
| logger.info("[FeatureBatchGateway] Subscribed to channel: %s (latest_only=True)", channel)
|
|
|
| self._started = True
|
| logger.info("[FeatureBatchGateway] Started — waiting for features on %d channels",
|
| len(self._agent_names))
|
|
|
| def stop(self) -> None:
|
| """Unsubscribe from all channels."""
|
| for agent in self._agent_names:
|
| channel = f"{self._prefix}{agent}"
|
| self._manager.unsubscribe(channel)
|
| self._started = False
|
| logger.info("[FeatureBatchGateway] Stopped")
|
|
|
| @property
|
| def diagnostics(self) -> dict:
|
| with self._lock:
|
| return {
|
| 'batches_fired': self._batches_fired,
|
| 'partial_discards': self._partial_discards,
|
| 'agent_overwrites': self._agent_overwrites,
|
| 'current_tick': self._current_tick,
|
| 'agents_in_batch': len(self._batch),
|
| 'n_required': self._n_required,
|
| }
|
|
|
|
|
|
|
| def _make_handler(self, agent_name: str):
|
| """Return a per-agent message handler closed over agent_name."""
|
| def _handler(message):
|
| try:
|
| self._receive(agent_name, message)
|
| except Exception as e:
|
| logger.error("[FeatureBatchGateway] Handler error for %s: %s", agent_name, e)
|
| return _handler
|
|
|
| def _receive(self, agent_name: str, message) -> None:
|
| """
|
| Process one arriving per-agent feature message.
|
|
|
| Called by the DedicatedRedisConnectionManager's listener thread —
|
| must be fast and must not block.
|
| """
|
|
|
| data = getattr(message, 'data', None)
|
| if data is None:
|
| if isinstance(message, dict):
|
| data = message
|
| else:
|
| logger.debug("[FeatureBatchGateway] %s: unrecognised message shape", agent_name)
|
| return
|
|
|
|
|
| tick_index = (
|
| data.get('tick_index')
|
| or (data.get('features') or {}).get('tick_index')
|
| or (data.get('features') or {}).get('tick_count')
|
| )
|
| raw_price = data.get('price')
|
| features = data.get('features', {})
|
|
|
| now_mono = time.monotonic()
|
|
|
| with self._lock:
|
|
|
|
|
|
|
| if (self._current_tick is not None
|
| and (now_mono - self._batch_ts) > self._stale_timeout):
|
| n_stale = len(self._batch)
|
| logger.warning(
|
| "[FeatureBatchGateway] Discarding stale partial batch: "
|
| "tick=%s agents=%d/%d age=%.1fs",
|
| self._current_tick, n_stale, self._n_required,
|
| now_mono - self._batch_ts,
|
| )
|
| self._partial_discards += 1
|
| self._batch = {}
|
| self._current_tick = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| if tick_index is not None and tick_index != self._current_tick:
|
| if self._batch:
|
| logger.info(
|
| "[FeatureBatchGateway] New tick %s arrived — discarding partial "
|
| "batch for tick %s (%d/%d agents). Waitlist cleared.",
|
| tick_index, self._current_tick,
|
| len(self._batch), self._n_required,
|
| )
|
| self._partial_discards += 1
|
| self._batch = {}
|
| self._current_tick = tick_index
|
| self._batch_ts = now_mono
|
| self._batch_price = float(raw_price) if raw_price else 0.0
|
|
|
|
|
| if agent_name in self._batch:
|
| self._agent_overwrites += 1
|
| logger.debug("[FeatureBatchGateway] Agent %s overwrote slot in tick %s",
|
| agent_name, self._current_tick)
|
|
|
| self._batch[agent_name] = {
|
| 'features': features,
|
| 'price': raw_price,
|
| 'tick_index': tick_index,
|
| }
|
|
|
|
|
| if self._batch_price == 0.0 and raw_price:
|
| try:
|
| self._batch_price = float(raw_price)
|
| except (ValueError, TypeError):
|
| pass
|
|
|
| n_now = len(self._batch)
|
| logger.debug("[FeatureBatchGateway] tick=%s agents=%d/%d (+%s)",
|
| self._current_tick, n_now, self._n_required, agent_name)
|
|
|
|
|
| if n_now >= self._n_required:
|
| batch_payload = {
|
| 'tick_index': self._current_tick,
|
| 'price': self._batch_price,
|
| 'timestamp': time.strftime('%H:%M:%S'),
|
| 'agent_count': n_now,
|
| 'agents': {
|
| ag: {'features': v['features'], 'price': v['price']}
|
| for ag, v in self._batch.items()
|
| },
|
| }
|
| self._batches_fired += 1
|
| logger.info(
|
| "[FeatureBatchGateway] ✅ BATCH COMPLETE tick=%s price=%.4f "
|
| "agents=%d (batch #%d)",
|
| self._current_tick, self._batch_price, n_now, self._batches_fired,
|
| )
|
|
|
| self._batch = {}
|
| self._current_tick = None
|
| self._batch_price = 0.0
|
|
|
|
|
|
|
| batch_to_fire = batch_payload
|
|
|
| else:
|
| batch_to_fire = None
|
|
|
|
|
| if batch_to_fire is not None:
|
| try:
|
| self._on_batch(batch_to_fire)
|
| except Exception as e:
|
| logger.error("[FeatureBatchGateway] on_batch_ready raised: %s", e)
|
|
|
|
|
|
|
|
|
|
|
|
|
| if __name__ == "__main__":
|
| import sys
|
|
|
| logging.basicConfig(
|
| level=logging.INFO,
|
| format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
|
| )
|
|
|
| def on_message(msg: RedisMessage):
|
| print(f"📨 Received: {msg}")
|
|
|
| def on_connected():
|
| print("🟢 Connected!")
|
|
|
| def on_disconnected():
|
| print("🔴 Disconnected!")
|
|
|
|
|
| print(f"\n=== Testing DedicatedRedisConnectionManager V10.2 ===")
|
| print(f"🤗 HuggingFace Spaces: {IS_HF_SPACES}")
|
| print(f"📋 Redis Config: {'Available' if HAS_REDIS_CONFIG else 'Using defaults'}")
|
| print()
|
|
|
| manager = DedicatedRedisConnectionManager(
|
| on_connected=on_connected,
|
| on_disconnected=on_disconnected,
|
| on_message=lambda ch, msg: print(f"[Global] {ch}: {msg}"),
|
| database=0
|
| )
|
|
|
| timeout = 10 if IS_HF_SPACES else 5
|
|
|
| if manager.start(timeout=timeout):
|
| print("✅ Manager started successfully")
|
|
|
|
|
| manager.subscribe("reward-channel", callback=on_message)
|
|
|
|
|
|
|
|
|
|
|
| manager.subscribe(
|
| "features-live",
|
| callback=on_message,
|
| latest_only=True,
|
| )
|
|
|
|
|
| time.sleep(1)
|
|
|
| start = time.time()
|
| result = manager.publish(
|
| "features-live",
|
| {"hello": "world", "platform": "HuggingFace Spaces" if IS_HF_SPACES else "local"},
|
| "feature-update"
|
| )
|
| latency = (time.time() - start) * 1000
|
|
|
| print(f"📤 Published to {result} subscribers (latency: {latency:.2f}ms)")
|
|
|
|
|
| time.sleep(2)
|
|
|
|
|
| state = manager.get_state()
|
| print(f"\n📊 Final State:")
|
| print(f" Connected: {state.get('connected')}")
|
| print(f" Messages sent: {state.get('stats', {}).get('messages_published', 0)}")
|
| print(f" Messages received: {state.get('stats', {}).get('messages_received', 0)}")
|
|
|
|
|
| latch = manager.get_latch_diagnostics()
|
| print(f"\n🔒 Latch Model Diagnostics:")
|
| print(f" Latest-only channels: {latch.get('latest_only_channels')}")
|
| print(f" Messages dispatched: {latch.get('messages_dispatched')}")
|
| print(f" Messages overwritten: {latch.get('messages_overwritten')}")
|
| print(f" Busy drops avoided: {latch.get('busy_drops_avoided')}")
|
|
|
| manager.stop()
|
| else:
|
| print("❌ Failed to start manager")
|
| sys.exit(1) |