MukeshKapoor25 commited on
Commit
e6cdbd6
·
1 Parent(s): c17a7bd

feat(postgres): add ssl support and connection retry logic

Browse files

Add SSL configuration options for PostgreSQL connections with different modes (disable/require/verify-full).
Implement connection retry logic with exponential backoff for database initialization.
Add new configuration parameters for connection retries and SSL settings.

Files changed (3) hide show
  1. app/core/config.py +7 -0
  2. app/postgres.py +21 -0
  3. app/sql.py +74 -17
app/core/config.py CHANGED
@@ -27,6 +27,13 @@ class Settings(BaseSettings):
27
  POSTGRES_PASSWORD: str = os.getenv("POSTGRES_PASSWORD", "")
28
  POSTGRES_MIN_POOL_SIZE: int = int(os.getenv("POSTGRES_MIN_POOL_SIZE", "5"))
29
  POSTGRES_MAX_POOL_SIZE: int = int(os.getenv("POSTGRES_MAX_POOL_SIZE", "20"))
 
 
 
 
 
 
 
30
 
31
  @property
32
  def POSTGRES_URI(self) -> str:
 
27
  POSTGRES_PASSWORD: str = os.getenv("POSTGRES_PASSWORD", "")
28
  POSTGRES_MIN_POOL_SIZE: int = int(os.getenv("POSTGRES_MIN_POOL_SIZE", "5"))
29
  POSTGRES_MAX_POOL_SIZE: int = int(os.getenv("POSTGRES_MAX_POOL_SIZE", "20"))
30
+ POSTGRES_CONNECT_MAX_RETRIES: int = int(os.getenv("POSTGRES_CONNECT_MAX_RETRIES", "20"))
31
+ POSTGRES_CONNECT_INITIAL_DELAY_MS: int = int(os.getenv("POSTGRES_CONNECT_INITIAL_DELAY_MS", "500"))
32
+ POSTGRES_CONNECT_BACKOFF_MULTIPLIER: float = float(os.getenv("POSTGRES_CONNECT_BACKOFF_MULTIPLIER", "1.5"))
33
+ POSTGRES_SSL_MODE: str = os.getenv("POSTGRES_SSL_MODE", "disable") # disable | require | verify-full
34
+ POSTGRES_SSL_ROOT_CERT: Optional[str] = os.getenv("POSTGRES_SSL_ROOT_CERT")
35
+ POSTGRES_SSL_CERT: Optional[str] = os.getenv("POSTGRES_SSL_CERT")
36
+ POSTGRES_SSL_KEY: Optional[str] = os.getenv("POSTGRES_SSL_KEY")
37
 
38
  @property
39
  def POSTGRES_URI(self) -> str:
app/postgres.py CHANGED
@@ -3,6 +3,7 @@ PostgreSQL connection pool management.
3
  Provides async connection pool for PostgreSQL operations.
4
  """
5
  import asyncpg
 
6
  from typing import Optional, Dict, Any
7
  from insightfy_utils.logging import get_logger
8
  from app.core.config import settings
@@ -51,6 +52,25 @@ class PostgreSQLConnectionPool:
51
  "max_pool_size": settings.POSTGRES_MAX_POOL_SIZE
52
  })
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  # Create connection pool
55
  cls._pool = await asyncpg.create_pool(
56
  host=settings.POSTGRES_HOST,
@@ -62,6 +82,7 @@ class PostgreSQLConnectionPool:
62
  max_size=settings.POSTGRES_MAX_POOL_SIZE,
63
  command_timeout=30.0,
64
  timeout=30.0,
 
65
  )
66
 
67
  # Test connection by acquiring and releasing
 
3
  Provides async connection pool for PostgreSQL operations.
4
  """
5
  import asyncpg
6
+ import ssl
7
  from typing import Optional, Dict, Any
8
  from insightfy_utils.logging import get_logger
9
  from app.core.config import settings
 
52
  "max_pool_size": settings.POSTGRES_MAX_POOL_SIZE
53
  })
54
 
55
+ # Optional SSL context
56
+ ssl_context = None
57
+ mode = (settings.POSTGRES_SSL_MODE or "disable").lower()
58
+ if mode != "disable":
59
+ if mode == "verify-full":
60
+ ssl_context = ssl.create_default_context(cafile=settings.POSTGRES_SSL_ROOT_CERT) if settings.POSTGRES_SSL_ROOT_CERT else ssl.create_default_context()
61
+ if settings.POSTGRES_SSL_CERT and settings.POSTGRES_SSL_KEY:
62
+ try:
63
+ ssl_context.load_cert_chain(certfile=settings.POSTGRES_SSL_CERT, keyfile=settings.POSTGRES_SSL_KEY)
64
+ except Exception as e:
65
+ logger.warning("Failed to load client SSL cert/key for PostgreSQL pool", exc_info=e)
66
+ ssl_context.check_hostname = True
67
+ ssl_context.verify_mode = ssl.CERT_REQUIRED
68
+ else:
69
+ ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
70
+ ssl_context.check_hostname = False
71
+ ssl_context.verify_mode = ssl.CERT_NONE
72
+ logger.info("PostgreSQL pool SSL enabled", extra={"ssl_mode": settings.POSTGRES_SSL_MODE})
73
+
74
  # Create connection pool
75
  cls._pool = await asyncpg.create_pool(
76
  host=settings.POSTGRES_HOST,
 
82
  max_size=settings.POSTGRES_MAX_POOL_SIZE,
83
  command_timeout=30.0,
84
  timeout=30.0,
85
+ ssl=ssl_context,
86
  )
87
 
88
  # Test connection by acquiring and releasing
app/sql.py CHANGED
@@ -3,6 +3,7 @@ PostgreSQL database connection and session management for SCM microservice.
3
  Following TMS pattern with SQLAlchemy async engine.
4
  """
5
  import logging
 
6
  from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
7
  from sqlalchemy.orm import sessionmaker
8
  from sqlalchemy import MetaData, text
@@ -19,6 +20,36 @@ if not DATABASE_URI:
19
 
20
  logger.info("Using PostgreSQL DATABASE_URL", extra={"url": DATABASE_URI.split('@')[0] + '@***'})
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  # Create async engine with connection pool settings
23
  async_engine = create_async_engine(
24
  DATABASE_URI,
@@ -29,14 +60,7 @@ async_engine = create_async_engine(
29
  pool_timeout=30,
30
  pool_recycle=3600,
31
  pool_pre_ping=True,
32
- connect_args={
33
- "server_settings": {
34
- "application_name": "cuatrolabs-scm-ms",
35
- "jit": "off"
36
- },
37
- "command_timeout": 60,
38
- "statement_cache_size": 0
39
- }
40
  )
41
 
42
  # Create async session factory
@@ -57,14 +81,47 @@ logger.info("PostgreSQL configuration loaded successfully")
57
 
58
  async def connect_to_database() -> None:
59
  """Initialize database connection when the application starts."""
60
- try:
61
- # Test the connection
62
- async with async_engine.begin() as conn:
63
- await conn.execute(text("SELECT 1"))
64
- logger.info("Successfully connected to PostgreSQL database")
65
- except Exception as e:
66
- logger.exception("Error connecting to PostgreSQL database")
67
- raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
  async def disconnect_from_database() -> None:
70
  """Close database connection when the application shuts down."""
@@ -84,4 +141,4 @@ async def create_tables() -> None:
84
  logger.info("Database tables created successfully")
85
  except Exception as e:
86
  logger.exception("Error creating database tables")
87
- raise
 
3
  Following TMS pattern with SQLAlchemy async engine.
4
  """
5
  import logging
6
+ import ssl
7
  from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
8
  from sqlalchemy.orm import sessionmaker
9
  from sqlalchemy import MetaData, text
 
20
 
21
  logger.info("Using PostgreSQL DATABASE_URL", extra={"url": DATABASE_URI.split('@')[0] + '@***'})
22
 
23
+ # Build connect args including optional SSL
24
+ CONNECT_ARGS = {
25
+ "server_settings": {
26
+ "application_name": "cuatrolabs-scm-ms",
27
+ "jit": "off"
28
+ },
29
+ "command_timeout": 60,
30
+ "statement_cache_size": 0
31
+ }
32
+
33
+ mode = (settings.POSTGRES_SSL_MODE or "disable").lower()
34
+ if mode != "disable":
35
+ ssl_context: ssl.SSLContext
36
+ if mode == "verify-full":
37
+ ssl_context = ssl.create_default_context(cafile=settings.POSTGRES_SSL_ROOT_CERT) if settings.POSTGRES_SSL_ROOT_CERT else ssl.create_default_context()
38
+ if settings.POSTGRES_SSL_CERT and settings.POSTGRES_SSL_KEY:
39
+ try:
40
+ ssl_context.load_cert_chain(certfile=settings.POSTGRES_SSL_CERT, keyfile=settings.POSTGRES_SSL_KEY)
41
+ except Exception as e:
42
+ logger.warning("Failed to load client SSL cert/key for PostgreSQL", exc_info=e)
43
+ ssl_context.check_hostname = True
44
+ ssl_context.verify_mode = ssl.CERT_REQUIRED
45
+ else:
46
+ # sslmode=require: encrypt but don't verify server cert
47
+ ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
48
+ ssl_context.check_hostname = False
49
+ ssl_context.verify_mode = ssl.CERT_NONE
50
+ CONNECT_ARGS["ssl"] = ssl_context
51
+ logger.info("PostgreSQL SSL enabled", extra={"ssl_mode": settings.POSTGRES_SSL_MODE})
52
+
53
  # Create async engine with connection pool settings
54
  async_engine = create_async_engine(
55
  DATABASE_URI,
 
60
  pool_timeout=30,
61
  pool_recycle=3600,
62
  pool_pre_ping=True,
63
+ connect_args=CONNECT_ARGS
 
 
 
 
 
 
 
64
  )
65
 
66
  # Create async session factory
 
81
 
82
  async def connect_to_database() -> None:
83
  """Initialize database connection when the application starts."""
84
+ import asyncio
85
+ attempts = 0
86
+ max_attempts = settings.POSTGRES_CONNECT_MAX_RETRIES
87
+ delay = settings.POSTGRES_CONNECT_INITIAL_DELAY_MS / 1000.0
88
+ last_error = None
89
+ while attempts < max_attempts:
90
+ try:
91
+ async with async_engine.begin() as conn:
92
+ await conn.execute(text("SELECT 1"))
93
+ logger.info("Successfully connected to PostgreSQL database", extra={
94
+ "host": settings.POSTGRES_HOST,
95
+ "port": settings.POSTGRES_PORT,
96
+ "database": settings.POSTGRES_DB
97
+ })
98
+ return
99
+ except Exception as e:
100
+ last_error = e
101
+ attempts += 1
102
+ logger.warning(
103
+ "PostgreSQL connection attempt failed",
104
+ extra={
105
+ "attempt": attempts,
106
+ "max_attempts": max_attempts,
107
+ "retry_delay_ms": int(delay * 1000),
108
+ "host": settings.POSTGRES_HOST,
109
+ "port": settings.POSTGRES_PORT,
110
+ "database": settings.POSTGRES_DB
111
+ }
112
+ )
113
+ await asyncio.sleep(delay)
114
+ delay = min(delay * settings.POSTGRES_CONNECT_BACKOFF_MULTIPLIER, 30.0)
115
+ logger.error(
116
+ "Failed to connect to PostgreSQL after retries",
117
+ exc_info=last_error,
118
+ extra={
119
+ "host": settings.POSTGRES_HOST,
120
+ "port": settings.POSTGRES_PORT,
121
+ "database": settings.POSTGRES_DB
122
+ }
123
+ )
124
+ raise last_error
125
 
126
  async def disconnect_from_database() -> None:
127
  """Close database connection when the application shuts down."""
 
141
  logger.info("Database tables created successfully")
142
  except Exception as e:
143
  logger.exception("Error creating database tables")
144
+ raise