44# This source code is licensed under the terms described in the LICENSE file in
55# the root directory of this source tree.
66
7+ from collections .abc import Callable , Coroutine
78from datetime import datetime
9+ from typing import TypeVar
810
911import asyncpg # type: ignore[import-untyped]
1012
1517
1618log = get_logger (name = __name__ , category = "providers::utils" )
1719
20+ T = TypeVar ("T" )
21+
1822
1923class PostgresKVStoreImpl (KVStore ):
2024 """PostgreSQL-backed key-value store implementation."""
2125
2226 def __init__ (self , config : PostgresKVStoreConfig ):
2327 self .config = config
28+ self ._pool : asyncpg .Pool | None = None
2429 self ._table_created = False
2530
2631 async def initialize (self ) -> None :
@@ -35,22 +40,25 @@ def _build_ssl(self) -> object:
3540 return self .config .ssl_mode
3641 return None
3742
38- async def _connect (self ) -> asyncpg .Connection :
39- try :
40- conn = await asyncpg .connect (
41- host = self .config .host ,
42- port = int (self .config .port ),
43- database = self .config .db ,
44- user = self .config .user ,
45- password = self .config .password ,
46- ssl = self ._build_ssl (),
47- )
48- except Exception as e :
49- log .exception ("Could not connect to PostgreSQL database server" )
50- raise RuntimeError ("Could not connect to PostgreSQL database server" ) from e
43+ async def _acquire (self ) -> asyncpg .Pool :
44+ if self ._pool is None :
45+ try :
46+ self ._pool = await asyncpg .create_pool (
47+ host = self .config .host ,
48+ port = int (self .config .port ),
49+ database = self .config .db ,
50+ user = self .config .user ,
51+ password = self .config .password ,
52+ ssl = self ._build_ssl (),
53+ min_size = self .config .pool_size ,
54+ max_size = self .config .pool_size + self .config .max_overflow ,
55+ )
56+ except Exception as e :
57+ log .exception ("Could not connect to PostgreSQL database server" )
58+ raise RuntimeError ("Could not connect to PostgreSQL database server" ) from e
5159
5260 if not self ._table_created :
53- try :
61+ async with self . _pool . acquire () as conn :
5462 await conn .execute (
5563 f"""
5664 CREATE TABLE IF NOT EXISTS { self .config .table_name } (
@@ -61,10 +69,20 @@ async def _connect(self) -> asyncpg.Connection:
6169 """
6270 )
6371 self ._table_created = True
64- except Exception :
65- await conn .close ()
66- raise
67- return conn
72+
73+ return self ._pool
74+
75+ async def _execute_with_retry (self , fn : Callable [[asyncpg .Connection ], Coroutine [None , None , T ]]) -> T :
76+ """Execute fn with a pooled connection, retrying once on connection error."""
77+ pool = await self ._acquire ()
78+ try :
79+ async with pool .acquire () as conn :
80+ return await fn (conn )
81+ except (asyncpg .exceptions .ConnectionDoesNotExistError , OSError ):
82+ log .warning ("PostgreSQL connection lost, expiring pool connections" )
83+ await pool .expire_connections ()
84+ async with pool .acquire () as conn :
85+ return await fn (conn )
6886
6987 def _namespaced_key (self , key : str ) -> str :
7088 if not self .config .namespace :
@@ -78,8 +96,8 @@ def _strip_namespace(self, key: str) -> str:
7896
7997 async def set (self , key : str , value : str , expiration : datetime | None = None ) -> None :
8098 key = self ._namespaced_key (key )
81- conn = await self . _connect ()
82- try :
99+
100+ async def _do ( conn : asyncpg . Connection ) -> None :
83101 await conn .execute (
84102 f"""
85103 INSERT INTO { self .config .table_name } (key, value, expiration)
@@ -91,13 +109,13 @@ async def set(self, key: str, value: str, expiration: datetime | None = None) ->
91109 value ,
92110 expiration ,
93111 )
94- finally :
95- await conn . close ( )
112+
113+ await self . _execute_with_retry ( _do )
96114
97115 async def get (self , key : str ) -> str | None :
98116 key = self ._namespaced_key (key )
99- conn = await self . _connect ()
100- try :
117+
118+ async def _do ( conn : asyncpg . Connection ) -> str | None :
101119 row = await conn .fetchrow (
102120 f"""
103121 SELECT value FROM { self .config .table_name }
@@ -107,26 +125,25 @@ async def get(self, key: str) -> str | None:
107125 key ,
108126 )
109127 return row ["value" ] if row else None
110- finally :
111- await conn . close ( )
128+
129+ return await self . _execute_with_retry ( _do )
112130
113131 async def delete (self , key : str ) -> None :
114132 key = self ._namespaced_key (key )
115- conn = await self . _connect ()
116- try :
133+
134+ async def _do ( conn : asyncpg . Connection ) -> None :
117135 await conn .execute (
118136 f"DELETE FROM { self .config .table_name } WHERE key = $1" ,
119137 key ,
120138 )
121- finally :
122- await conn . close ( )
139+
140+ await self . _execute_with_retry ( _do )
123141
124142 async def values_in_range (self , start_key : str , end_key : str ) -> list [str ]:
125143 start_key = self ._namespaced_key (start_key )
126144 end_key = self ._namespaced_key (end_key )
127145
128- conn = await self ._connect ()
129- try :
146+ async def _do (conn : asyncpg .Connection ) -> list [str ]:
130147 rows = await conn .fetch (
131148 f"""
132149 SELECT value FROM { self .config .table_name }
@@ -138,15 +155,14 @@ async def values_in_range(self, start_key: str, end_key: str) -> list[str]:
138155 end_key ,
139156 )
140157 return [row ["value" ] for row in rows ]
141- finally :
142- await conn . close ( )
158+
159+ return await self . _execute_with_retry ( _do )
143160
144161 async def keys_in_range (self , start_key : str , end_key : str ) -> list [str ]:
145162 start_key = self ._namespaced_key (start_key )
146163 end_key = self ._namespaced_key (end_key )
147164
148- conn = await self ._connect ()
149- try :
165+ async def _do (conn : asyncpg .Connection ) -> list [str ]:
150166 rows = await conn .fetch (
151167 f"""
152168 SELECT key FROM { self .config .table_name }
@@ -158,8 +174,10 @@ async def keys_in_range(self, start_key: str, end_key: str) -> list[str]:
158174 end_key ,
159175 )
160176 return [self ._strip_namespace (row ["key" ]) for row in rows ]
161- finally :
162- await conn . close ( )
177+
178+ return await self . _execute_with_retry ( _do )
163179
164180 async def shutdown (self ) -> None :
165- pass
181+ if self ._pool :
182+ await self ._pool .close ()
183+ self ._pool = None
0 commit comments