Skip to content

Commit eeb1a92

Browse files
jneightJavier Cordero
andauthored
working on psycopg3 (#81)
* refactor some code * ruff formatting * split common code for psycopg2 and psycopg3 * Removed python2 code * split code for pool * added psycopg3 pool * split tests for psycopg * updated readme --------- Co-authored-by: Javier Cordero <github@j2i.me>
1 parent d8a6130 commit eeb1a92

File tree

16 files changed

+348
-221
lines changed

16 files changed

+348
-221
lines changed
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: CI
1+
name: CI-psycopg2
22
'on':
33
push:
44
branches:
@@ -60,6 +60,7 @@ jobs:
6060
- uses: actions/checkout@v4
6161
- run: pip install django==${{ matrix.django-version}}
6262
- run: pip install psycopg2
63+
- run: pip install psycogreen
6364
- run: pip install gevent
6465
- run: python setup.py -q install
65-
- run: python runtests.py
66+
- run: python runtests_psycopg2.py

.github/workflows/ci_psycopg3.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: CI-psycopg3
2+
'on':
3+
push:
4+
branches:
5+
- master
6+
pull_request:
7+
branches:
8+
- master
9+
jobs:
10+
build:
11+
env:
12+
POSTGRES_USER: postgres
13+
PGPASSWORD: postgres
14+
runs-on: '${{ matrix.os }}'
15+
strategy:
16+
matrix:
17+
include:
18+
- os: ubuntu-latest
19+
python-version: '3.8'
20+
django-version: '4.2.9'
21+
- os: ubuntu-latest
22+
python-version: '3.9'
23+
django-version: '4.2.9'
24+
- os: ubuntu-latest
25+
python-version: '3.10'
26+
django-version: '4.2.9'
27+
- os: ubuntu-latest
28+
python-version: '3.11'
29+
django-version: '4.2.9'
30+
- os: ubuntu-latest
31+
python-version: '3.12'
32+
django-version: '4.2.9'
33+
services:
34+
postgres:
35+
image: postgres
36+
env:
37+
POSTGRES_USER: postgres
38+
POSTGRES_PASSWORD: postgres
39+
options: >-
40+
--health-cmd pg_isready
41+
--health-interval 10s
42+
--health-timeout 5s
43+
--health-retries 5
44+
ports:
45+
- 5432:5432
46+
steps:
47+
- name: 'Set up Python ${{ matrix.python-version }}'
48+
uses: actions/setup-python@v5
49+
with:
50+
python-version: '${{ matrix.python-version }}'
51+
- uses: actions/checkout@v4
52+
- run: pip install django==${{ matrix.django-version}}
53+
- run: pip install psycopg[binary]
54+
- run: pip install gevent
55+
- run: python setup.py -q install
56+
- run: python runtests_psycopg3.py

README.md

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,46 +9,54 @@ django-db-geventpool
99

1010
Another DB pool using gevent for PostgreSQL DB.
1111

12-
If **gevent** is not installed, the pool will use **eventlet** as fallback.
12+
13+
psycopg3
14+
---------
15+
16+
Django, since 4.2, supports psycopg3. One of the advantages is that gevent is supported without needing extra patches, just install the package
17+
18+
```
19+
$ pip install psycopg[binary]
20+
```
21+
1322

1423
psycopg2
1524
--------
1625

17-
django-db-geventpool requires psycopg2:
26+
If **gevent** is not installed, the pool will use **eventlet** as fallback.
1827

1928
- `psycopg2>=2.5.1` for CPython 2 and 3 (or
2029
[psycopg2-binary](https://pypi.org/project/psycopg2-binary/)---see
2130
[notes in the psycopg2 2.7.4
2231
release](http://initd.org/psycopg/articles/2018/02/08/psycopg-274-released/))
2332
- `psycopg2cffi>=2.7` for PyPy
2433

25-
Patch psycopg2
26-
--------------
34+
Patch psycopg2
35+
--------------
2736

28-
Before using the pool, psycopg2 must be patched with psycogreen, if you
29-
are using [gunicorn webserver](http://www.gunicorn.org/), a good place
30-
is the
31-
[post\_fork()](http://docs.gunicorn.org/en/latest/settings.html#post-fork)
32-
function at the config file:
37+
Before using the pool, psycopg2 must be patched with psycogreen, if you
38+
are using [gunicorn webserver](http://www.gunicorn.org/), a good place
39+
is the
40+
[post\_fork()](http://docs.gunicorn.org/en/latest/settings.html#post-fork)
41+
function at the config file:
3342

34-
``` {.python}
35-
from psycogreen.gevent import patch_psycopg # use this if you use gevent workers
36-
from psycogreen.eventlet import patch_psycopg # use this if you use eventlet workers
43+
``` {.python}
44+
from psycogreen.gevent import patch_psycopg # use this if you use gevent workers
45+
from psycogreen.eventlet import patch_psycopg # use this if you use eventlet workers
3746
38-
def post_fork(server, worker):
39-
patch_psycopg()
40-
worker.log.info("Made Psycopg2 Green")
41-
```
47+
def post_fork(server, worker):
48+
patch_psycopg()
49+
worker.log.info("Made Psycopg2 Green")
50+
```
4251
4352
Settings
4453
--------
4554
46-
> -
47-
>
48-
> Set *ENGINE* in your database settings to:
55+
> - Set *ENGINE* in your database settings to:
4956
>
50-
> : - *\'django\_db\_geventpool.backends.postgresql\_psycopg2\'*
51-
> - For postgis: *\'django\_db\_geventpool.backends.postgis\'*
57+
> - For psycopg3: 'django_db_geventpool.backends.postgresql_psycopg3'
58+
> - For psycopg2: 'django_db_geventpool.backends.postgresql_psycopg2'
59+
> - For postgis: 'django_db_geventpool.backends.postgis'
5260
>
5361
> - Add *MAX\_CONNS* to *OPTIONS* to set the maximun number of
5462
> connections allowed to database (default=4)
@@ -64,7 +72,7 @@ Settings
6472
``` {.python}
6573
DATABASES = {
6674
'default': {
67-
'ENGINE': 'django_db_geventpool.backends.postgresql_psycopg2',
75+
'ENGINE': 'django_db_geventpool.backends.postgresql_psycopg',
6876
'NAME': 'db',
6977
'USER': 'postgres',
7078
'PASSWORD': 'postgres',
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import logging
2+
import sys
3+
4+
try:
5+
from gevent.lock import Semaphore
6+
except ImportError:
7+
from eventlet.semaphore import Semaphore
8+
9+
from .creation import DatabaseCreation
10+
11+
logger = logging.getLogger("django.geventpool")
12+
13+
connection_pools = {}
14+
connection_pools_lock = Semaphore(value=1)
15+
16+
17+
class DatabaseWrapperMixin(object):
18+
pool_class = None
19+
creation_class = DatabaseCreation
20+
INTRANS = None
21+
22+
def __init__(self, *args, **kwargs):
23+
self._pool = None
24+
super().__init__(*args, **kwargs)
25+
self.creation = self.creation_class(self)
26+
27+
@property
28+
def pool(self):
29+
if self._pool is not None:
30+
return self._pool
31+
with connection_pools_lock:
32+
if self.alias not in connection_pools:
33+
self._pool = self.pool_class(**self.get_connection_params())
34+
connection_pools[self.alias] = self._pool
35+
else:
36+
self._pool = connection_pools[self.alias]
37+
return self._pool
38+
39+
def get_new_connection(self, conn_params: dict):
40+
if self.connection is None:
41+
self.connection = self.pool.get()
42+
self.closed_in_transaction = False
43+
return self.connection
44+
45+
def get_connection_params(self) -> dict:
46+
conn_params = super().get_connection_params()
47+
for attr in ["MAX_CONNS", "REUSE_CONNS"]:
48+
if attr in self.settings_dict["OPTIONS"]:
49+
conn_params[attr] = self.settings_dict["OPTIONS"][attr]
50+
return conn_params
51+
52+
def close(self):
53+
self.validate_thread_sharing()
54+
if self.closed_in_transaction or self.connection is None:
55+
return # no need to close anything
56+
try:
57+
self._close()
58+
except:
59+
# In some cases (database restart, network connection lost etc...)
60+
# the connection to the database is lost without giving Django a
61+
# notification. If we don't set self.connection to None, the error
62+
# will occur at every request.
63+
self.connection = None
64+
logger.warning(
65+
"psycopg2 error while closing the connection.", exc_info=sys.exc_info()
66+
)
67+
raise
68+
finally:
69+
self.set_clean()
70+
71+
def close_if_unusable_or_obsolete(self):
72+
# Always close the connection because it's not (usually) really being closed.
73+
self.close()
74+
75+
def _close(self):
76+
if self.connection.closed:
77+
self.pool.closeall()
78+
else:
79+
if self.connection.info.transaction_status == self.INTRANS:
80+
self.connection.rollback()
81+
self.connection.autocommit = True
82+
with self.wrap_database_errors:
83+
self.pool.put(self.connection)
84+
self.connection = None
85+
86+
def closeall(self):
87+
for pool in connection_pools.values():
88+
pool.closeall()
89+
90+
def set_clean(self):
91+
if self.in_atomic_block:
92+
self.closed_in_transaction = True
93+
self.needs_rollback = True

django_db_geventpool/backends/postgresql_psycopg2/creation.py renamed to django_db_geventpool/backends/creation.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
# coding=utf-8
2-
3-
from django.db.backends.postgresql.creation import DatabaseCreation as OriginalDatabaseCreation
1+
from django.db.backends.postgresql.creation import (
2+
DatabaseCreation as OriginalDatabaseCreation,
3+
)
44

55

66
class DatabaseCreationMixin(object):
77
def _create_test_db(self, verbosity, autoclobber, keepdb=False):
88
self.connection.closeall()
9-
return super(DatabaseCreationMixin, self)._create_test_db(verbosity, autoclobber, keepdb)
9+
return super()._create_test_db(verbosity, autoclobber, keepdb)
1010

1111
def _destroy_test_db(self, test_database_name, verbosity):
1212
self.connection.closeall()
13-
return super(DatabaseCreationMixin, self)._destroy_test_db(test_database_name, verbosity)
13+
return super()._destroy_test_db(test_database_name, verbosity)
1414

1515

1616
class DatabaseCreation(DatabaseCreationMixin, OriginalDatabaseCreation):

django_db_geventpool/backends/postgresql_psycopg2/psycopg2_pool.py renamed to django_db_geventpool/backends/pool.py

Lines changed: 6 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
# coding=utf-8
2-
31
# this file is a modified version of the psycopg2 used at gevent examples
42
# to be compatible with django, also checks if
53
# DB connection is closed and reopen it:
64
# https://github.com/surfly/gevent/blob/master/examples/psycopg2_pool.py
75
import logging
8-
import sys
96
import weakref
10-
logger = logging.getLogger('django.geventpool')
7+
8+
logger = logging.getLogger("django.geventpool")
119

1210
try:
1311
from gevent import queue
@@ -16,27 +14,11 @@
1614
from eventlet import queue
1715
from ...utils import NullContextRLock as RLock
1816

19-
try:
20-
from psycopg2 import connect, DatabaseError
21-
import psycopg2.extras
22-
except ImportError as e:
23-
from django.core.exceptions import ImproperlyConfigured
24-
raise ImproperlyConfigured("Error loading psycopg2 module: %s" % e)
25-
26-
if sys.version_info[0] >= 3:
27-
integer_types = int,
28-
else:
29-
import __builtin__
30-
integer_types = int, __builtin__.long
31-
3217

33-
class DatabaseConnectionPool(object):
34-
def __init__(self, maxsize=100, reuse=100):
35-
if not isinstance(maxsize, integer_types):
36-
raise TypeError('Expected integer, got %r' % (maxsize,))
37-
if not isinstance(reuse, integer_types):
38-
raise TypeError('Expected integer, got %r' % (reuse,))
18+
class DatabaseConnectionPool:
19+
DBERROR = None
3920

21+
def __init__(self, maxsize: int = 100, reuse: int = 100):
4022
# Use a WeakSet here so, even if we fail to discard the connection
4123
# when it is being closed, or it is closed outside of here, the item
4224
# will be removed automatically
@@ -61,7 +43,7 @@ def get(self):
6143
# check connection is still valid
6244
self.check_usable(conn)
6345
logger.debug("DB connection reused")
64-
except DatabaseError:
46+
except self.DBERROR:
6547
logger.debug("DB connection was closed, creating a new one")
6648
conn = None
6749
except queue.Empty:
@@ -100,24 +82,3 @@ def closeall(self):
10082
self._conns.discard(conn)
10183

10284
logger.debug("DB connections all closed")
103-
104-
105-
class PostgresConnectionPool(DatabaseConnectionPool):
106-
def __init__(self, *args, **kwargs):
107-
self.connect = kwargs.pop('connect', connect)
108-
self.connection = None
109-
maxsize = kwargs.pop('MAX_CONNS', 4)
110-
reuse = kwargs.pop('REUSE_CONNS', maxsize)
111-
self.args = args
112-
self.kwargs = kwargs
113-
super(PostgresConnectionPool, self).__init__(maxsize, reuse)
114-
115-
def create_connection(self):
116-
conn = self.connect(*self.args, **self.kwargs)
117-
# set correct encoding
118-
conn.set_client_encoding('UTF8')
119-
psycopg2.extras.register_default_jsonb(conn_or_curs=conn, loads=lambda x: x)
120-
return conn
121-
122-
def check_usable(self, connection):
123-
connection.cursor().execute('SELECT 1')

django_db_geventpool/backends/postgis/base.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
# coding=utf-8
2-
3-
from django.contrib.gis.db.backends.postgis.base import DatabaseWrapper as OriginalDatabaseWrapper
4-
5-
from django_db_geventpool.backends.postgresql_psycopg2.base import DatabaseWrapperMixin
1+
from django.contrib.gis.db.backends.postgis.base import (
2+
DatabaseWrapper as OriginalDatabaseWrapper,
3+
)
4+
5+
try:
6+
import psycopg # noqa
7+
from ..postgresql_psycopg3.base import DatabaseWrapperMixin
8+
except ImportError:
9+
# fallback to psycopg3
10+
from ..postgresql_psycopg2.base import DatabaseWrapperMixin
611

712

813
class DatabaseWrapper(DatabaseWrapperMixin, OriginalDatabaseWrapper):

0 commit comments

Comments
 (0)