diff --git a/README.md b/README.md index 1f6d54c..f4cfc6a 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,8 @@ A FastAPI application with Docker and docker-compose support. BOXAPI_LOG_DIR=/app/logs/boxapi BOXAPI_LOG_LEVEL=all # or "error" for production - # Redis for pagination (optional but recommended) - REDIS_URL=redis://localhost:6379/0 + # Redis for pagination (optional but recommended) + REDIS_URL=redis://redis:6379/0 # Use 'redis' hostname in Docker ``` 2. **Build and start the application:** @@ -191,6 +191,7 @@ The application uses environment variables for configuration. Copy `env.example` ### Redis Configuration (for pagination) - `REDIS_URL`: Redis connection URL (default: `redis://localhost:6379/0`) +- **Note**: Redis is optional but recommended for pagination. If Redis is not available, the system will work without pagination (posts may be duplicated on subsequent calls). ### Database Configuration - `POSTGRES_DB`: Database name (default: `fastapi_db`) @@ -290,6 +291,13 @@ The application includes health checks that can be accessed at: - Check if PostgreSQL container is running: `docker-compose ps` - Check database logs: `docker-compose logs postgres` +5. **Redis connection issues:** + - Redis is optional - the system will work without it (posts may be duplicated) + - If you want Redis: `docker-compose up redis` to start Redis service + - Check Redis logs: `docker-compose logs redis` + - For Docker: use `REDIS_URL=redis://redis:6379/0` + - For local development: use `REDIS_URL=redis://localhost:6379/0` + ## Contributing 1. Fork the repository diff --git a/docker-compose.yml b/docker-compose.yml index 2cd609a..34cea7f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,8 @@ services: depends_on: postgres: condition: service_healthy + redis: + condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 30s @@ -45,9 +47,24 @@ services: networks: - fastapi-network + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - fastapi-network + networks: fastapi-network: driver: bridge volumes: - postgres_data: \ No newline at end of file + postgres_data: + redis_data: \ No newline at end of file diff --git a/utils/seen_store.py b/utils/seen_store.py index 0685584..7acfd57 100644 --- a/utils/seen_store.py +++ b/utils/seen_store.py @@ -1,22 +1,64 @@ # utils/seen_store.py import os, redis -from typing import Iterable +from typing import Iterable, Optional +import logging + +logger = logging.getLogger(__name__) REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/0") -r = redis.from_url(REDIS_URL, decode_responses=True) +_redis_client: Optional[redis.Redis] = None + +def _get_redis_client() -> Optional[redis.Redis]: + """Get Redis client with error handling""" + global _redis_client + if _redis_client is not None: + return _redis_client + + try: + _redis_client = redis.from_url(REDIS_URL, decode_responses=True) + # Test the connection + _redis_client.ping() + logger.info("✅ Redis connection established") + return _redis_client + except Exception as e: + logger.warning(f"⚠️ Redis connection failed: {e}") + logger.warning("📝 Pagination will be disabled - posts may be duplicated") + _redis_client = None + return None def seen_key(username: str) -> str: return f"ig:seen:{username}" def mark_seen(username: str, ids: Iterable[str]) -> None: - if not ids: return - r.sadd(seen_key(username), *ids) + """Mark posts as seen (Redis optional)""" + if not ids: + return + + r = _get_redis_client() + if r is None: + return # Skip if Redis is not available + + try: + r.sadd(seen_key(username), *ids) + except Exception as e: + logger.warning(f"⚠️ Failed to mark posts as seen: {e}") def filter_unseen(username: str, ids: Iterable[str]) -> list[str]: - # pipeline for speed - key = seen_key(username) - pipe = r.pipeline() - for i in ids: - pipe.sismember(key, i) - flags = pipe.execute() - return [i for i, seen in zip(ids, flags) if not seen] \ No newline at end of file + """Filter out already seen posts (Redis optional)""" + r = _get_redis_client() + if r is None: + # If Redis is not available, return all IDs (no filtering) + return list(ids) + + try: + # pipeline for speed + key = seen_key(username) + pipe = r.pipeline() + for i in ids: + pipe.sismember(key, i) + flags = pipe.execute() + return [i for i, seen in zip(ids, flags) if not seen] + except Exception as e: + logger.warning(f"⚠️ Failed to filter unseen posts: {e}") + # Fallback: return all IDs if Redis fails + return list(ids) \ No newline at end of file