Enhance caching mechanism and logging configuration
- Updated .gitignore to include additional IDE and OS files, as well as log and web build directories. - Expanded config.sample.yaml to include cache configuration options for memory and Redis. - Modified wallamonitor.py to load cache configuration and initialize ArticleCache. - Refactored QueueManager to utilize ArticleCache for tracking notified articles. - Improved logging setup to dynamically determine log file path based on environment.
This commit is contained in:
157
managers/article_cache.py
Normal file
157
managers/article_cache.py
Normal file
@@ -0,0 +1,157 @@
|
||||
import logging
|
||||
import redis
|
||||
import json
|
||||
from collections import deque
|
||||
|
||||
NOTIFIED_ARTICLE_TTL = 7 * 24 * 60 * 60 # TTL de 7 días en segundos para artículos notificados (solo Redis)
|
||||
DEFAULT_MEMORY_LIMIT = 300 # Límite por defecto de artículos en memoria
|
||||
|
||||
class MemoryArticleCache:
|
||||
"""Maneja el cache de artículos notificados usando memoria (lista con límite)"""
|
||||
|
||||
def __init__(self, limit=DEFAULT_MEMORY_LIMIT):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self._notified_articles = deque(maxlen=limit)
|
||||
self._limit = limit
|
||||
self.logger.info(f"Cache de artículos en memoria inicializado (límite: {limit})")
|
||||
|
||||
def is_article_notified(self, article):
|
||||
"""Verifica si un artículo ya ha sido notificado"""
|
||||
return article in self._notified_articles
|
||||
|
||||
def mark_article_as_notified(self, article):
|
||||
"""Marca un artículo como notificado en memoria"""
|
||||
if article not in self._notified_articles:
|
||||
self._notified_articles.append(article)
|
||||
self.logger.debug(f"Artículo marcado como notificado (total en memoria: {len(self._notified_articles)})")
|
||||
|
||||
def mark_articles_as_notified(self, articles):
|
||||
"""Añade múltiples artículos a la lista de artículos ya notificados en memoria"""
|
||||
article_list = articles if isinstance(articles, list) else [articles]
|
||||
added = 0
|
||||
for article in article_list:
|
||||
if article not in self._notified_articles:
|
||||
self._notified_articles.append(article)
|
||||
added += 1
|
||||
self.logger.debug(f"{added} artículos marcados como notificados (total en memoria: {len(self._notified_articles)}/{self._limit})")
|
||||
|
||||
|
||||
class RedisArticleCache:
|
||||
"""Maneja el cache de artículos notificados usando Redis"""
|
||||
|
||||
def __init__(self, redis_host='localhost', redis_port=6379, redis_db=0, redis_password=None):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# Inicializar conexión Redis
|
||||
try:
|
||||
self._redis_client = redis.Redis(
|
||||
host=redis_host,
|
||||
port=redis_port,
|
||||
db=redis_db,
|
||||
password=redis_password,
|
||||
decode_responses=True,
|
||||
socket_connect_timeout=5,
|
||||
socket_timeout=5
|
||||
)
|
||||
# Verificar conexión
|
||||
self._redis_client.ping()
|
||||
self.logger.info(f"Conectado a Redis en {redis_host}:{redis_port} (db={redis_db})")
|
||||
except (redis.ConnectionError, redis.TimeoutError) as e:
|
||||
self.logger.error(f"Error conectando a Redis: {e}")
|
||||
self.logger.error("Redis no está disponible. El sistema no podrá evitar duplicados sin Redis.")
|
||||
raise
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error inesperado inicializando Redis: {e}")
|
||||
raise
|
||||
|
||||
def _get_article_key(self, article):
|
||||
"""Genera una clave única para un artículo en Redis"""
|
||||
return f"notified:{article.get_platform()}:{article.get_id()}"
|
||||
|
||||
def is_article_notified(self, article):
|
||||
"""Verifica si un artículo ya ha sido notificado"""
|
||||
try:
|
||||
key = self._get_article_key(article)
|
||||
return self._redis_client.exists(key) > 0
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error verificando artículo en Redis: {e}")
|
||||
return False
|
||||
|
||||
def mark_article_as_notified(self, article):
|
||||
"""Marca un artículo como notificado en Redis con TTL, guardando toda la información del artículo"""
|
||||
try:
|
||||
key = self._get_article_key(article)
|
||||
# Guardar toda la información del artículo como JSON
|
||||
article_data = {
|
||||
'id': article.get_id(),
|
||||
'title': article.get_title(),
|
||||
'description': article._description, # Acceder al campo privado para obtener la descripción completa
|
||||
'price': article.get_price(),
|
||||
'currency': article.get_currency(),
|
||||
'location': article.get_location(),
|
||||
'allows_shipping': article._allows_shipping, # Acceder al campo privado para obtener el valor booleano
|
||||
'url': article.get_url(),
|
||||
'images': article.get_images(),
|
||||
'modified_at': article.get_modified_at(),
|
||||
'platform': article.get_platform(),
|
||||
}
|
||||
self._redis_client.setex(key, NOTIFIED_ARTICLE_TTL, json.dumps(article_data))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error marcando artículo como notificado en Redis: {e}")
|
||||
|
||||
def mark_articles_as_notified(self, articles):
|
||||
"""Añade múltiples artículos a la lista de artículos ya notificados en Redis"""
|
||||
article_list = articles if isinstance(articles, list) else [articles]
|
||||
|
||||
try:
|
||||
# Usar pipeline para mejor rendimiento al añadir múltiples artículos
|
||||
pipe = self._redis_client.pipeline()
|
||||
for article in article_list:
|
||||
key = self._get_article_key(article)
|
||||
# Guardar toda la información del artículo como JSON
|
||||
article_data = {
|
||||
'id': article.get_id(),
|
||||
'title': article.get_title(),
|
||||
'description': article._description, # Acceder al campo privado para obtener la descripción completa
|
||||
'price': article.get_price(),
|
||||
'currency': article.get_currency(),
|
||||
'location': article.get_location(),
|
||||
'allows_shipping': article._allows_shipping, # Acceder al campo privado para obtener el valor booleano
|
||||
'url': article.get_url(),
|
||||
'images': article.get_images(),
|
||||
'modified_at': article.get_modified_at(),
|
||||
'platform': article.get_platform(),
|
||||
}
|
||||
pipe.setex(key, NOTIFIED_ARTICLE_TTL, json.dumps(article_data))
|
||||
pipe.execute()
|
||||
self.logger.debug(f"{len(article_list)} artículos marcados como notificados en Redis")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error añadiendo artículos a Redis: {e}")
|
||||
|
||||
|
||||
def create_article_cache(cache_type='memory', **kwargs):
|
||||
"""
|
||||
Factory function para crear el cache de artículos apropiado.
|
||||
|
||||
Args:
|
||||
cache_type: 'memory' o 'redis'
|
||||
**kwargs: Argumentos adicionales según el tipo de cache:
|
||||
- Para 'memory': limit (opcional, default=300)
|
||||
- Para 'redis': redis_host, redis_port, redis_db, redis_password
|
||||
|
||||
Returns:
|
||||
MemoryArticleCache o RedisArticleCache
|
||||
"""
|
||||
if cache_type == 'memory':
|
||||
limit = kwargs.get('limit', DEFAULT_MEMORY_LIMIT)
|
||||
return MemoryArticleCache(limit=limit)
|
||||
elif cache_type == 'redis':
|
||||
return RedisArticleCache(
|
||||
redis_host=kwargs.get('redis_host', 'localhost'),
|
||||
redis_port=kwargs.get('redis_port', 6379),
|
||||
redis_db=kwargs.get('redis_db', 0),
|
||||
redis_password=kwargs.get('redis_password')
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Tipo de cache desconocido: {cache_type}. Debe ser 'memory' o 'redis'")
|
||||
|
||||
@@ -5,15 +5,14 @@ import logging
|
||||
from managers.telegram_manager import TelegramManager
|
||||
|
||||
MESSAGE_DELAY = 3.0 # Tiempo de espera entre mensajes en segundos
|
||||
NOTIFIED_ARTICLES_LIMIT = 300 # Límite de artículos notificados a mantener en memoria
|
||||
RETRY_TIMES = 3
|
||||
|
||||
class QueueManager:
|
||||
def __init__(self):
|
||||
def __init__(self, article_cache):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self._queue = queue.Queue() # Cola thread-safe
|
||||
self._notified_articles = []
|
||||
self._telegram_manager = TelegramManager()
|
||||
self._article_cache = article_cache
|
||||
self._running = True
|
||||
|
||||
# Iniciar el thread de procesamiento
|
||||
@@ -22,7 +21,7 @@ class QueueManager:
|
||||
|
||||
def add_to_queue(self, article, search_name=None, thread_id=None, retry_times=RETRY_TIMES):
|
||||
# Verificar si el artículo ya ha sido enviado
|
||||
if article in self._notified_articles:
|
||||
if self._article_cache.is_article_notified(article):
|
||||
return
|
||||
|
||||
if search_name is None:
|
||||
@@ -30,14 +29,11 @@ class QueueManager:
|
||||
self._queue.put((search_name, article, thread_id, retry_times))
|
||||
self.logger.debug(f"Artículo añadido a la cola: {article.get_title()}")
|
||||
|
||||
self.add_to_notified_articles(article)
|
||||
self._article_cache.mark_article_as_notified(article)
|
||||
|
||||
def add_to_notified_articles(self, articles):
|
||||
"""Añade artículos a la lista de artículos ya notificados"""
|
||||
if isinstance(articles, list):
|
||||
self._notified_articles.extend(articles)
|
||||
else:
|
||||
self._notified_articles.append(articles)
|
||||
self._article_cache.mark_articles_as_notified(articles)
|
||||
|
||||
def _process_queue(self):
|
||||
self.logger.info("Procesador de cola: Iniciado")
|
||||
|
||||
@@ -210,7 +210,8 @@ class TelegramManager:
|
||||
[InlineKeyboardButton("✅ En favoritos", callback_data=f"already_fav_{article_id}")],
|
||||
[InlineKeyboardButton("🗑️ Quitar de favoritos", callback_data=f"unfav_{article_id}")]
|
||||
]
|
||||
await query.edit_message_text(
|
||||
# Como el mensaje original es una foto con caption, usar edit_message_reply_markup
|
||||
await query.edit_message_reply_markup(
|
||||
reply_markup=InlineKeyboardMarkup(new_keyboard)
|
||||
)
|
||||
|
||||
@@ -281,8 +282,8 @@ class TelegramManager:
|
||||
keyboard = [
|
||||
[InlineKeyboardButton("⭐ Añadir a favoritos", callback_data=f"fav_{article_id}_unknown")]
|
||||
]
|
||||
await query.edit_message_text(
|
||||
text="💾 Acciones",
|
||||
# Como el mensaje original es una foto con caption, usar edit_message_reply_markup
|
||||
await query.edit_message_reply_markup(
|
||||
reply_markup=InlineKeyboardMarkup(keyboard)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import logging
|
||||
|
||||
class WorkerConditions:
|
||||
def __init__(self, item_monitoring, general_args):
|
||||
self._item_monitoring = item_monitoring
|
||||
self._general_args = general_args
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def _has_words(self, text, word_list):
|
||||
return any(word in text for word in word_list)
|
||||
|
||||
Reference in New Issue
Block a user