7
7
import psycopg2 .sql
8
8
from psycopg2 .extras import Json , RealDictCursor
9
9
10
- from procrastinate import connector , sql , utils
10
+ from procrastinate import connector , exceptions , sql , utils
11
11
12
12
logger = logging .getLogger (__name__ )
13
13
18
18
class PostgresConnector (connector .BaseConnector ):
19
19
def __init__ (
20
20
self ,
21
- pool : aiopg . Pool ,
21
+ * ,
22
22
json_dumps : Optional [Callable ] = None ,
23
23
json_loads : Optional [Callable ] = None ,
24
+ ** kwargs : Any ,
24
25
):
25
26
"""
26
- The pool connections are expected to have jsonb adapters.
27
+ Create a PostgreSQL connector. The connector uses an :py:func:`aiopg.Pool`,
28
+ which is either created automatically upon first use, or set through the
29
+ py:func:`PostgresConnector.set_pool` method.
27
30
28
- See parameter details in :py:func:`PostgresConnector.create_with_pool`.
31
+ All other arguments than ``json_dumps`` and ``json_loads`` are passed to
32
+ ``aiopg.create_pool`` (see aiopg documentation__), with default values that
33
+ may differ from those of ``aiopg`` (see the list of parameters below).
29
34
30
- Parameters
31
- ----------
32
- pool:
33
- An aiopg pool, either externally configured or passed by
34
- :py:func:`PostgresConnector.create_with_pool`.
35
-
36
- """
37
- self ._pool = pool
38
- self .json_dumps = json_dumps
39
- self .json_loads = json_loads
40
-
41
- async def close_async (self ) -> None :
42
- """
43
- Closes the pool and awaits all connections to be released.
44
- """
45
- self ._pool .close ()
46
- await self ._pool .wait_closed ()
47
-
48
- def _wrap_json (self , arguments : Dict [str , Any ]):
49
- return {
50
- key : Json (value , dumps = self .json_dumps )
51
- if isinstance (value , dict )
52
- else value
53
- for key , value in arguments .items ()
54
- }
55
-
56
- @classmethod
57
- async def create_with_pool_async (
58
- cls ,
59
- json_dumps : Optional [Callable ] = None ,
60
- json_loads : Optional [Callable ] = None ,
61
- ** kwargs ,
62
- ) -> aiopg .Pool :
63
- """
64
- Creates a connector, and its connection pool, using the provided parameters.
65
- All additional parameters will be used to create a
66
- :py:func:`aiopg.Pool` (see the documentation__), sometimes with a different
67
- default value.
68
-
69
- When using this method, you explicitely take the responsibility for opening the
70
- pool. It's your responsibility to call
71
- :py:func:`procrastinate.PostgresConnector.close` or
72
- :py:func:`procrastinate.PostgresConnector.close_async` to close connections
73
- when your process ends.
74
-
75
- .. __: https://aiopg.readthedocs.io/en/stable/core.html#aiopg.create_pool
76
35
.. _psycopg2 doc: https://www.psycopg.org/docs/extras.html#json-adaptation
36
+ .. __: https://aiopg.readthedocs.io/en/stable/core.html#aiopg.create_pool
77
37
38
+ Parameters
39
+ ----------
78
40
json_dumps:
79
41
The JSON dumps function to use for serializing job arguments. Defaults to
80
42
the function used by psycopg2. See the `psycopg2 doc`_.
81
43
json_loads:
82
44
The JSON loads function to use for deserializing job arguments. Defaults
83
- to the function used by psycopg2. See the `psycopg2 doc`_. Unused if pool
84
- is passed.
45
+ to the function used by psycopg2. See the `psycopg2 doc`_. Unused if the
46
+ pool is externally created and set into the connector through the
47
+ :py:func:`PostgresConnector.set_pool` method.
85
48
dsn (Optional[str]):
86
49
Passed to aiopg. Default is "" instead of None, which means if no argument
87
50
is passed, it will connect to localhost:5432 instead of a Unix-domain
@@ -102,32 +65,82 @@ async def create_with_pool_async(
102
65
maxsize (int):
103
66
Passed to aiopg. Cannot be lower than 2, otherwise worker won't be
104
67
functionning normally (one connection for listen/notify, one for executing
105
- tasks)
68
+ tasks).
69
+ minsize (int):
70
+ Passed to aiopg. Initial connections are not opened when the connector
71
+ is created, but at first use of the pool.
106
72
"""
107
- base_on_connect = kwargs .pop ("on_connect" , None )
73
+ self ._pool : Optional [aiopg .Pool ] = None
74
+ self .json_dumps = json_dumps
75
+ self .json_loads = json_loads
76
+ self ._pool_args = self ._adapt_pool_args (kwargs , json_loads )
77
+ self ._lock = asyncio .Lock ()
78
+
79
+ @staticmethod
80
+ def _adapt_pool_args (
81
+ pool_args : Dict [str , Any ], json_loads : Optional [Callable ]
82
+ ) -> Dict [str , Any ]:
83
+ """
84
+ Adapt the pool args for ``aiopg``, using sensible defaults for Procrastinate.
85
+ """
86
+ base_on_connect = pool_args .pop ("on_connect" , None )
108
87
109
88
async def on_connect (connection ):
110
89
if base_on_connect :
111
90
await base_on_connect (connection )
112
91
if json_loads :
113
92
psycopg2 .extras .register_default_jsonb (connection .raw , loads = json_loads )
114
93
115
- defaults = {
94
+ final_args = {
116
95
"dsn" : "" ,
117
96
"enable_json" : False ,
118
97
"enable_hstore" : False ,
119
98
"enable_uuid" : False ,
120
99
"on_connect" : on_connect ,
121
100
"cursor_factory" : RealDictCursor ,
122
101
}
123
- if "maxsize" in kwargs :
124
- kwargs ["maxsize" ] = max (2 , kwargs ["maxsize" ])
102
+ if "maxsize" in pool_args :
103
+ pool_args ["maxsize" ] = max (2 , pool_args ["maxsize" ])
125
104
126
- defaults .update (kwargs )
105
+ final_args .update (pool_args )
106
+ return final_args
127
107
128
- pool = await aiopg .create_pool (** defaults )
108
+ async def close_async (self ) -> None :
109
+ """
110
+ Close the pool and awaits all connections to be released.
111
+ """
112
+ if self ._pool :
113
+ self ._pool .close ()
114
+ await self ._pool .wait_closed ()
115
+ self ._pool = None
129
116
130
- return cls (pool = pool , json_dumps = json_dumps , json_loads = json_loads )
117
+ def _wrap_json (self , arguments : Dict [str , Any ]):
118
+ return {
119
+ key : Json (value , dumps = self .json_dumps )
120
+ if isinstance (value , dict )
121
+ else value
122
+ for key , value in arguments .items ()
123
+ }
124
+
125
+ @staticmethod
126
+ async def _create_pool (pool_args : Dict [str , Any ]) -> aiopg .Pool :
127
+ return await aiopg .create_pool (** pool_args )
128
+
129
+ def set_pool (self , pool : aiopg .Pool ) -> None :
130
+ """
131
+ Set the connection pool. Raises an exception if the pool is already set.
132
+ """
133
+ if self ._pool :
134
+ raise exceptions .PoolAlreadySet
135
+ self ._pool = pool
136
+
137
+ async def _get_pool (self ) -> aiopg .Pool :
138
+ if self ._pool :
139
+ return self ._pool
140
+ async with self ._lock :
141
+ if not self ._pool :
142
+ self .set_pool (await self ._create_pool (self ._pool_args ))
143
+ return self ._pool
131
144
132
145
# Pools and single connections do not exactly share their cursor API:
133
146
# - connection.cursor() is an async context manager (async with)
@@ -136,7 +149,8 @@ async def on_connect(connection):
136
149
# a pool or from a connection
137
150
138
151
async def execute_query (self , query : str , ** arguments : Any ) -> None :
139
- with await self ._pool .cursor () as cursor :
152
+ pool = await self ._get_pool ()
153
+ with await pool .cursor () as cursor :
140
154
await cursor .execute (query , self ._wrap_json (arguments ))
141
155
142
156
async def _execute_query_connection (
@@ -146,16 +160,17 @@ async def _execute_query_connection(
146
160
await cursor .execute (query , self ._wrap_json (arguments ))
147
161
148
162
async def execute_query_one (self , query : str , ** arguments : Any ) -> Dict [str , Any ]:
149
- with await self ._pool .cursor () as cursor :
163
+ pool = await self ._get_pool ()
164
+ with await pool .cursor () as cursor :
150
165
await cursor .execute (query , self ._wrap_json (arguments ))
151
166
152
167
return await cursor .fetchone ()
153
168
154
169
async def execute_query_all (
155
170
self , query : str , ** arguments : Any
156
171
) -> List [Dict [str , Any ]]:
157
-
158
- with await self . _pool .cursor () as cursor :
172
+ pool = await self . _get_pool ()
173
+ with await pool .cursor () as cursor :
159
174
await cursor .execute (query , self ._wrap_json (arguments ))
160
175
161
176
return await cursor .fetchall ()
@@ -171,10 +186,11 @@ def make_dynamic_query(self, query: str, **identifiers: str) -> str:
171
186
async def listen_notify (
172
187
self , event : asyncio .Event , channels : Iterable [str ]
173
188
) -> NoReturn :
189
+ pool = await self ._get_pool ()
174
190
# We need to acquire a dedicated connection, and use the listen
175
191
# query
176
192
while True :
177
- async with self . _pool .acquire () as connection :
193
+ async with pool .acquire () as connection :
178
194
for channel_name in channels :
179
195
await self ._execute_query_connection (
180
196
connection = connection ,
@@ -215,4 +231,4 @@ def PostgresJobStore(*args, **kwargs):
215
231
)
216
232
logger .warning (f"Deprecation Warning: { message } " )
217
233
warnings .warn (DeprecationWarning (message ))
218
- return PostgresConnector . create_with_pool ( * args , ** kwargs )
234
+ return PostgresConnector ( ** kwargs )
0 commit comments