From 3fce0e993b304f159092c39f1aa3c6bfd060ca9b Mon Sep 17 00:00:00 2001 From: Aish-kul16 Date: Wed, 20 May 2026 14:56:03 +0530 Subject: [PATCH] fix: Implement thread-safe singleton Neo4j client with graceful shutdown - Added thread-safe get_neo4j_client() function using double-check locking pattern - Added close_neo4j_client() for graceful cleanup - Integrated shutdown event in FastAPI app lifecycle - Prevents race conditions and connection pool exhaustion under concurrent load Fixes #17 --- src/api/app.py | 7 ++++- src/db/client.py | 66 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/src/api/app.py b/src/api/app.py index 10f77df..c893bdf 100644 --- a/src/api/app.py +++ b/src/api/app.py @@ -16,6 +16,7 @@ from api.middleware.firebase_auth import FirebaseAuthMiddleware from api.routers import auth, ingestion, languages, query, rag, repository, support, webhook from common.firebase_service import firebase_service # noqa: F401 — init on import +from db.client import close_neo4j_client # Add this import logger = logging.getLogger(__name__) @@ -43,7 +44,10 @@ async def lifespan(app: FastAPI): """ # Startup yield - # Shutdown - cleanup will be handled by new system + # Shutdown - cleanup resources + logger.info("Shutting down application...") + close_neo4j_client() + logger.info("Neo4j client closed") # Create FastAPI application @@ -114,3 +118,4 @@ def run_server(): if __name__ == "__main__": run_server() + diff --git a/src/db/client.py b/src/db/client.py index 4184d86..614da56 100644 --- a/src/db/client.py +++ b/src/db/client.py @@ -1,6 +1,7 @@ import logging import os import time +from threading import Lock from typing import Any, Dict, List, Optional, Tuple from neo4j import GraphDatabase @@ -123,22 +124,59 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.close() +# Global singleton instance with thread-safety +_client_instance = None +_client_lock = Lock() + + def get_neo4j_client() -> "Neo4jClient": - """Build a Neo4jClient from environment variables. + """Build a Neo4jClient from environment variables (singleton pattern). Reads NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD, and NEO4J_DATABASE. Raises ValueError if required variables are missing. + + Returns a single shared instance across all requests (thread-safe). + Uses double-check locking pattern for efficient concurrent access. + """ + global _client_instance + + # Fast path - no lock needed + if _client_instance is not None: + return _client_instance + + # Lock only when needed + with _client_lock: + # Double-check pattern + if _client_instance is not None: + return _client_instance + + uri = os.getenv("NEO4J_URI", "") + user = os.getenv("NEO4J_USERNAME", "neo4j") + password = os.getenv("NEO4J_PASSWORD", "") + database = os.getenv("NEO4J_DATABASE", "neo4j") + + if not uri: + raise ValueError("NEO4J_URI environment variable is required") + if not user: + raise ValueError("NEO4J_USERNAME environment variable is required") + if not password: + raise ValueError("NEO4J_PASSWORD environment variable is required") + + _client_instance = Neo4jClient(uri=uri, user=user, password=password, database=database) + logger.info("Neo4j client initialized (singleton)") + return _client_instance + + +def close_neo4j_client() -> None: + """Close the Neo4j client and cleanup resources. + + Call this during application shutdown to properly close database connections. """ - uri = os.getenv("NEO4J_URI", "") - user = os.getenv("NEO4J_USERNAME", "neo4j") - password = os.getenv("NEO4J_PASSWORD", "") - database = os.getenv("NEO4J_DATABASE", "neo4j") - - if not uri: - raise ValueError("NEO4J_URI environment variable is required") - if not user: - raise ValueError("NEO4J_USERNAME environment variable is required") - if not password: - raise ValueError("NEO4J_PASSWORD environment variable is required") - - return Neo4jClient(uri=uri, user=user, password=password, database=database) + global _client_instance + with _client_lock: + if _client_instance is not None: + _client_instance.close() + _client_instance = None + logger.info("Neo4j client closed") + + \ No newline at end of file