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+ import asyncio
8+ from collections .abc import Callable , Coroutine
79from datetime import datetime
10+ from typing import TypeVar
811
912import asyncpg # type: ignore[import-untyped]
1013
1518
1619log = get_logger (name = __name__ , category = "providers::utils" )
1720
21+ T = TypeVar ("T" )
22+
1823
1924class PostgresKVStoreImpl (KVStore ):
2025 """PostgreSQL-backed key-value store implementation."""
2126
2227 def __init__ (self , config : PostgresKVStoreConfig ):
2328 self .config = config
29+ self ._pool : asyncpg .Pool | None = None
30+ self ._loop : asyncio .AbstractEventLoop | None = None
2431 self ._table_created = False
2532
2633 async def initialize (self ) -> None :
@@ -35,22 +42,34 @@ def _build_ssl(self) -> object:
3542 return self .config .ssl_mode
3643 return None
3744
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
45+ async def _acquire (self ) -> asyncpg .Pool :
46+ loop = asyncio .get_running_loop ()
47+ if self ._pool is not None and self ._loop is not loop :
48+ # Pool was created in a different event loop (e.g., during init in a
49+ # temporary asyncio.run() loop). Discard it -- the old connections are
50+ # already dead since that loop is closed.
51+ self ._pool = None
52+ self ._table_created = False
5153
52- if not self ._table_created :
54+ if self ._pool is None :
5355 try :
56+ self ._pool = await asyncpg .create_pool (
57+ host = self .config .host ,
58+ port = int (self .config .port ),
59+ database = self .config .db ,
60+ user = self .config .user ,
61+ password = self .config .password ,
62+ ssl = self ._build_ssl (),
63+ min_size = self .config .pool_size ,
64+ max_size = self .config .pool_size + self .config .max_overflow ,
65+ )
66+ self ._loop = loop
67+ except Exception as e :
68+ log .exception ("Could not connect to PostgreSQL database server" )
69+ raise RuntimeError ("Could not connect to PostgreSQL database server" ) from e
70+
71+ if not self ._table_created :
72+ async with self ._pool .acquire () as conn :
5473 await conn .execute (
5574 f"""
5675 CREATE TABLE IF NOT EXISTS { self .config .table_name } (
@@ -61,10 +80,25 @@ async def _connect(self) -> asyncpg.Connection:
6180 """
6281 )
6382 self ._table_created = True
64- except Exception :
65- await conn .close ()
66- raise
67- return conn
83+
84+ return self ._pool
85+
86+ async def _execute_with_retry (self , fn : Callable [[asyncpg .Connection ], Coroutine [None , None , T ]]) -> T :
87+ """Execute fn with a pooled connection, retrying once on connection error."""
88+ pool = await self ._acquire ()
89+ try :
90+ async with pool .acquire () as conn :
91+ return await fn (conn )
92+ except (
93+ asyncpg .exceptions .ConnectionDoesNotExistError ,
94+ asyncpg .exceptions .InterfaceError ,
95+ OSError ,
96+ RuntimeError ,
97+ ):
98+ log .warning ("PostgreSQL connection lost, expiring pool connections" )
99+ await pool .expire_connections ()
100+ async with pool .acquire () as conn :
101+ return await fn (conn )
68102
69103 def _namespaced_key (self , key : str ) -> str :
70104 if not self .config .namespace :
@@ -78,8 +112,8 @@ def _strip_namespace(self, key: str) -> str:
78112
79113 async def set (self , key : str , value : str , expiration : datetime | None = None ) -> None :
80114 key = self ._namespaced_key (key )
81- conn = await self . _connect ()
82- try :
115+
116+ async def _do ( conn : asyncpg . Connection ) -> None :
83117 await conn .execute (
84118 f"""
85119 INSERT INTO { self .config .table_name } (key, value, expiration)
@@ -91,13 +125,13 @@ async def set(self, key: str, value: str, expiration: datetime | None = None) ->
91125 value ,
92126 expiration ,
93127 )
94- finally :
95- await conn . close ( )
128+
129+ await self . _execute_with_retry ( _do )
96130
97131 async def get (self , key : str ) -> str | None :
98132 key = self ._namespaced_key (key )
99- conn = await self . _connect ()
100- try :
133+
134+ async def _do ( conn : asyncpg . Connection ) -> str | None :
101135 row = await conn .fetchrow (
102136 f"""
103137 SELECT value FROM { self .config .table_name }
@@ -107,26 +141,25 @@ async def get(self, key: str) -> str | None:
107141 key ,
108142 )
109143 return row ["value" ] if row else None
110- finally :
111- await conn . close ( )
144+
145+ return await self . _execute_with_retry ( _do )
112146
113147 async def delete (self , key : str ) -> None :
114148 key = self ._namespaced_key (key )
115- conn = await self . _connect ()
116- try :
149+
150+ async def _do ( conn : asyncpg . Connection ) -> None :
117151 await conn .execute (
118152 f"DELETE FROM { self .config .table_name } WHERE key = $1" ,
119153 key ,
120154 )
121- finally :
122- await conn . close ( )
155+
156+ await self . _execute_with_retry ( _do )
123157
124158 async def values_in_range (self , start_key : str , end_key : str ) -> list [str ]:
125159 start_key = self ._namespaced_key (start_key )
126160 end_key = self ._namespaced_key (end_key )
127161
128- conn = await self ._connect ()
129- try :
162+ async def _do (conn : asyncpg .Connection ) -> list [str ]:
130163 rows = await conn .fetch (
131164 f"""
132165 SELECT value FROM { self .config .table_name }
@@ -138,15 +171,14 @@ async def values_in_range(self, start_key: str, end_key: str) -> list[str]:
138171 end_key ,
139172 )
140173 return [row ["value" ] for row in rows ]
141- finally :
142- await conn . close ( )
174+
175+ return await self . _execute_with_retry ( _do )
143176
144177 async def keys_in_range (self , start_key : str , end_key : str ) -> list [str ]:
145178 start_key = self ._namespaced_key (start_key )
146179 end_key = self ._namespaced_key (end_key )
147180
148- conn = await self ._connect ()
149- try :
181+ async def _do (conn : asyncpg .Connection ) -> list [str ]:
150182 rows = await conn .fetch (
151183 f"""
152184 SELECT key FROM { self .config .table_name }
@@ -158,8 +190,10 @@ async def keys_in_range(self, start_key: str, end_key: str) -> list[str]:
158190 end_key ,
159191 )
160192 return [self ._strip_namespace (row ["key" ]) for row in rows ]
161- finally :
162- await conn . close ( )
193+
194+ return await self . _execute_with_retry ( _do )
163195
164196 async def shutdown (self ) -> None :
165- pass
197+ if self ._pool :
198+ await self ._pool .close ()
199+ self ._pool = None
0 commit comments