Skip to content

Commit 3c0974e

Browse files
Implemented support for max_lifetime_session in thin mode (#410).
1 parent 02a06bf commit 3c0974e

File tree

9 files changed

+147
-53
lines changed

9 files changed

+147
-53
lines changed

doc/src/api_manual/pool_params.rst

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,6 @@ PoolParams Attributes
136136
attribute is *0*, then the connections may remain in the pool indefinitely.
137137
The default value is *0* seconds.
138138

139-
This attribute is only supported in python-oracledb Thick mode.
140-
141139
.. attribute:: PoolParams.max_sessions_per_shard
142140

143141
This read-only attribute is an integer that determines the maximum number

doc/src/release_notes.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Thin Mode Changes
1919

2020
#) Added namespace package :ref:`oracledb.plugins <plugins>` for plugins that
2121
can be used to extend the capability of python-oracledb.
22+
#) Added support for property :attr:`ConnectionPool.max_lifetime_session`
23+
(`issue 410 <https://github.com/oracle/python-oracledb/issues/410>`__).
2224
#) Perform TLS server matching in python-oracledb instead of the Python SSL
2325
library to allow alternate names to be checked
2426
(`issue 415 <https://github.com/oracle/python-oracledb/issues/415>`__).

doc/src/user_guide/connection_handling.rst

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2079,17 +2079,16 @@ application) and are unused for longer than the pool creation attribute
20792079
seconds signifying an infinite time and meaning idle connections will never be
20802080
closed.
20812081

2082-
In python-oracledb Thick mode, the pool creation parameter
2083-
``max_lifetime_session`` also allows pools to shrink. This parameter bounds
2084-
the total length of time that a connection can exist starting from the time the
2085-
pool created it. If a connection was created ``max_lifetime_session`` or
2086-
longer seconds ago, then it will be closed when it is idle in the pool. In the
2087-
case when ``timeout`` and ``max_lifetime_session`` are both set, the connection
2088-
will be terminated if either the idle timeout happens or the max lifetime
2089-
setting is exceeded. Note that when using python-oracledb in Thick mode with
2090-
Oracle Client libraries prior to 21c, pool shrinkage is only initiated when the
2091-
pool is accessed so pools in fully dormant applications will not shrink until
2092-
the application is next used.
2082+
The pool creation parameter ``max_lifetime_session`` also allows pools to
2083+
shrink. This parameter bounds the total length of time that a connection can
2084+
exist starting from the time the pool created it. If a connection was created
2085+
``max_lifetime_session`` or longer seconds ago, then it will be closed when it
2086+
is idle in the pool. In the case when ``timeout`` and ``max_lifetime_session``
2087+
are both set, the connection will be terminated if either the idle timeout
2088+
happens or the max lifetime setting is exceeded. Note that when using
2089+
python-oracledb in Thick mode with Oracle Client libraries prior to 21c, pool
2090+
shrinkage is only initiated when the pool is accessed so pools in fully dormant
2091+
applications will not shrink until the application is next used.
20932092

20942093
For pools created with :ref:`external authentication <extauth>`, with
20952094
:ref:`homogeneous <connpooltypes>` set to False, or when using :ref:`drcp`,

src/oracledb/impl/thin/connection.pyx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#------------------------------------------------------------------------------
2-
# Copyright (c) 2020, 2024, Oracle and/or its affiliates.
2+
# Copyright (c) 2020, 2025, Oracle and/or its affiliates.
33
#
44
# This software is dual-licensed to you under the Universal Permissive License
55
# (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
@@ -61,7 +61,8 @@ cdef class BaseThinConnImpl(BaseConnImpl):
6161
str _service_name
6262
bint _drcp_enabled
6363
bint _drcp_establish_session
64-
double _time_in_pool
64+
double _time_created
65+
double _time_returned
6566
list _temp_lobs_to_close
6667
uint32_t _temp_lobs_total_size
6768
uint32_t _call_timeout

src/oracledb/impl/thin/pool.pyx

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#------------------------------------------------------------------------------
2-
# Copyright (c) 2020, 2024, Oracle and/or its affiliates.
2+
# Copyright (c) 2020, 2025, Oracle and/or its affiliates.
33
#
44
# This software is dual-licensed to you under the Universal Permissive License
55
# (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
@@ -70,6 +70,7 @@ cdef class BaseThinPoolImpl(BasePoolImpl):
7070
self.set_wait_timeout(params.wait_timeout)
7171
self.set_timeout(params.timeout)
7272
self._stmt_cache_size = params.stmtcachesize
73+
self._max_lifetime_session = params.max_lifetime_session
7374
self._ping_interval = params.ping_interval
7475
self._ping_timeout = params.ping_timeout
7576
self._free_new_conn_impls = []
@@ -154,6 +155,7 @@ cdef class BaseThinPoolImpl(BasePoolImpl):
154155
if conn_impl._protocol._transport is not None:
155156
self._conn_impls_to_drop.append(conn_impl)
156157
self._notify_bg_task()
158+
self._ensure_min_connections()
157159

158160
cdef int _drop_conn_impls_helper(self, list conn_impls_to_drop) except -1:
159161
"""
@@ -167,6 +169,16 @@ cdef class BaseThinPoolImpl(BasePoolImpl):
167169
except:
168170
pass
169171

172+
cdef int _ensure_min_connections(self) except -1:
173+
"""
174+
Ensure that the minimum number of connections in the pool is
175+
maintained.
176+
"""
177+
if self._open_count < self.min:
178+
self._num_to_create = max(self._num_to_create,
179+
self.min - self._open_count)
180+
self._notify_bg_task()
181+
170182
cdef PooledConnRequest _get_next_request(self):
171183
"""
172184
Get the next request to process.
@@ -246,13 +258,15 @@ cdef class BaseThinPoolImpl(BasePoolImpl):
246258
"""
247259
Called before the connection is connected. The connection class and
248260
pool attributes are updated and the TLS session is stored on the
249-
transport for reuse.
261+
transport for reuse. The timestamps are also retained for later use.
250262
"""
251263
if params is not None:
252264
conn_impl._cclass = params._default_description.cclass
253265
else:
254266
conn_impl._cclass = self.connect_params._default_description.cclass
255267
conn_impl._pool = self
268+
conn_impl._time_created = time.monotonic()
269+
conn_impl._time_returned = conn_impl._time_created
256270

257271
def _process_timeout(self):
258272
"""
@@ -280,25 +294,35 @@ cdef class BaseThinPoolImpl(BasePoolImpl):
280294
bint is_open = conn_impl._protocol._transport is not None
281295
BaseThinDbObjectTypeCache type_cache
282296
PooledConnRequest request
297+
double tstamp
283298
int cache_num
284299
self._busy_conn_impls.remove(conn_impl)
285300
if conn_impl._dbobject_type_cache_num > 0:
286301
cache_num = conn_impl._dbobject_type_cache_num
287302
type_cache = get_dbobject_type_cache(cache_num)
288303
type_cache._clear_cursors()
304+
if not is_open:
305+
self._open_count -= 1
306+
self._ensure_min_connections()
289307
if conn_impl._is_pool_extra:
290308
conn_impl._is_pool_extra = False
291309
if is_open and self._open_count >= self.max:
292310
if self._free_new_conn_impls and self._open_count == self.max:
293311
self._drop_conn_impl(self._free_new_conn_impls.pop(0))
294312
else:
313+
self._open_count -= 1
295314
self._drop_conn_impl(conn_impl)
296315
is_open = False
297-
if not is_open:
298-
self._open_count -= 1
299-
else:
316+
if is_open:
300317
conn_impl.warning = None
301-
conn_impl._time_in_pool = time.monotonic()
318+
conn_impl._time_returned = time.monotonic()
319+
if self._max_lifetime_session != 0:
320+
tstamp = conn_impl._time_created + self._max_lifetime_session
321+
if conn_impl._time_returned > tstamp:
322+
self._open_count -= 1
323+
self._drop_conn_impl(conn_impl)
324+
is_open = False
325+
if is_open:
302326
for request in self._requests:
303327
if request.in_progress or request.wants_new \
304328
or request.conn_impl is not None \
@@ -349,7 +373,7 @@ cdef class BaseThinPoolImpl(BasePoolImpl):
349373
current_time = time.monotonic()
350374
while conn_impls_to_check and self._open_count > self.min:
351375
conn_impl = conn_impls_to_check[0]
352-
if current_time - conn_impl._time_in_pool < self._timeout:
376+
if current_time - conn_impl._time_returned < self._timeout:
353377
break
354378
conn_impls_to_check.pop(0)
355379
self._drop_conn_impl(conn_impl)
@@ -535,7 +559,6 @@ cdef class ThinPoolImpl(BaseThinPoolImpl):
535559
conn_impl = ThinConnImpl(self.dsn, self.connect_params)
536560
self._pre_connect(conn_impl, params)
537561
conn_impl.connect(self.connect_params)
538-
conn_impl._time_in_pool = time.monotonic()
539562
return conn_impl
540563

541564
def _notify_bg_task(self):
@@ -729,7 +752,6 @@ cdef class AsyncThinPoolImpl(BaseThinPoolImpl):
729752
conn_impl = AsyncThinConnImpl(self.dsn, self.connect_params)
730753
self._pre_connect(conn_impl, params)
731754
await conn_impl.connect(self.connect_params)
732-
conn_impl._time_in_pool = time.monotonic()
733755
return conn_impl
734756

735757
def _notify_bg_task(self):
@@ -856,7 +878,7 @@ cdef class PooledConnRequest:
856878
"""
857879
cdef:
858880
ReadBuffer buf = conn_impl._protocol._read_buf
859-
double elapsed_time
881+
double elapsed_time, min_create_time
860882
bint has_data_ready
861883
if not buf._transport._is_async:
862884
while buf._pending_error_num == 0:
@@ -865,20 +887,27 @@ cdef class PooledConnRequest:
865887
break
866888
buf.check_control_packet()
867889
if buf._pending_error_num != 0:
868-
self.pool_impl._drop_conn_impl(conn_impl)
869890
self.pool_impl._open_count -= 1
870-
else:
871-
self.conn_impl = conn_impl
872-
if self.pool_impl._ping_interval == 0:
891+
self.pool_impl._drop_conn_impl(conn_impl)
892+
return 0
893+
elif self.pool_impl._max_lifetime_session > 0:
894+
min_create_time = \
895+
time.monotonic() - self.pool_impl._max_lifetime_session
896+
if conn_impl._time_created < min_create_time:
897+
self.pool_impl._open_count -= 1
898+
self.pool_impl._drop_conn_impl(conn_impl)
899+
return 0
900+
self.conn_impl = conn_impl
901+
if self.pool_impl._ping_interval == 0:
902+
self.requires_ping = True
903+
elif self.pool_impl._ping_interval > 0:
904+
elapsed_time = time.monotonic() - conn_impl._time_returned
905+
if elapsed_time > self.pool_impl._ping_interval:
873906
self.requires_ping = True
874-
elif self.pool_impl._ping_interval > 0:
875-
elapsed_time = time.monotonic() - conn_impl._time_in_pool
876-
if elapsed_time > self.pool_impl._ping_interval:
877-
self.requires_ping = True
878-
if self.requires_ping:
879-
self.pool_impl._add_request(self)
880-
else:
881-
self.completed = True
907+
if self.requires_ping:
908+
self.pool_impl._add_request(self)
909+
else:
910+
self.completed = True
882911

883912
def fulfill(self):
884913
"""
@@ -890,8 +919,8 @@ cdef class PooledConnRequest:
890919
cdef:
891920
BaseThinPoolImpl pool = self.pool_impl
892921
BaseThinConnImpl conn_impl
922+
ssize_t ix
893923
object exc
894-
ssize_t i
895924

896925
# if an exception was raised in the background thread, raise it now
897926
if self.exception is not None:
@@ -910,13 +939,14 @@ cdef class PooledConnRequest:
910939
# connection is not required); in addition, ensure that the connection
911940
# class matches
912941
if not self.wants_new and pool._free_used_conn_impls:
913-
for i, conn_impl in enumerate(reversed(pool._free_used_conn_impls)):
942+
ix = len(pool._free_used_conn_impls) - 1
943+
for conn_impl in reversed(pool._free_used_conn_impls):
914944
if self.cclass is None or conn_impl._cclass == self.cclass:
915-
i = len(pool._free_used_conn_impls) - i - 1
916-
pool._free_used_conn_impls.pop(i)
945+
pool._free_used_conn_impls.pop(ix)
917946
self._check_connection(conn_impl)
918947
if self.completed or self.requires_ping:
919948
return self.completed
949+
ix -= 1
920950

921951
# check for an available new connection (only permitted if the
922952
# connection class matches)

tests/ext/test_ext_1000_pool_shrink.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -----------------------------------------------------------------------------
2-
# Copyright (c) 2024, Oracle and/or its affiliates.
2+
# Copyright (c) 2024, 2025, Oracle and/or its affiliates.
33
#
44
# This software is dual-licensed to you under the Universal Permissive License
55
# (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
@@ -62,9 +62,9 @@ def test_ext_1001(self):
6262
conn = pool.acquire()
6363
self.assertEqual(pool.opened, 3)
6464

65-
@unittest.skipIf(not test_env.get_is_thin(), "doesn't occur in thick mode")
65+
@unittest.skipUnless(test_env.get_is_thin(), "doesn't occur in thick mode")
6666
def test_ext_1002(self):
67-
"E1002 - test pool shrinks to min on pool inactivity"
67+
"E1002 - test pool timeout shrinks to min on pool inactivity"
6868
pool = test_env.get_pool(min=3, max=10, increment=2, timeout=4)
6969
conns = [pool.acquire() for i in range(6)]
7070
self.assertEqual(pool.opened, 6)
@@ -73,9 +73,9 @@ def test_ext_1002(self):
7373
time.sleep(6)
7474
self.assertEqual(pool.opened, 3)
7575

76-
@unittest.skipIf(not test_env.get_is_thin(), "doesn't occur in thick mode")
76+
@unittest.skipUnless(test_env.get_is_thin(), "doesn't occur in thick mode")
7777
def test_ext_1003(self):
78-
"E1003 - test pool eliminates extra connections on inactivity"
78+
"E1003 - test pool timeout eliminates extra connections on inactivity"
7979
pool = test_env.get_pool(min=4, max=10, increment=4, timeout=3)
8080
conns = [pool.acquire() for i in range(5)]
8181
self.assertEqual(pool.opened, 5)
@@ -85,6 +85,40 @@ def test_ext_1003(self):
8585
self.assertEqual(pool.opened, 5)
8686
del conns
8787

88+
@unittest.skipUnless(test_env.get_is_thin(), "doesn't occur in thick mode")
89+
def test_ext_1004(self):
90+
"E1004 - test pool max_lifetime_session on release"
91+
pool = test_env.get_pool(
92+
min=4, max=10, increment=4, max_lifetime_session=3
93+
)
94+
conns = [pool.acquire() for i in range(5)]
95+
self.assertEqual(pool.opened, 5)
96+
time.sleep(2)
97+
self.assertEqual(pool.opened, 8)
98+
time.sleep(2)
99+
for conn in conns:
100+
conn.close()
101+
time.sleep(2)
102+
self.assertEqual(pool.opened, 4)
103+
104+
@unittest.skipUnless(test_env.get_is_thin(), "doesn't occur in thick mode")
105+
def test_ext_1005(self):
106+
"E1005 - test pool max_lifetime_session on acquire"
107+
pool = test_env.get_pool(
108+
min=4, max=10, increment=4, max_lifetime_session=4
109+
)
110+
conns = [pool.acquire() for i in range(5)]
111+
self.assertEqual(pool.opened, 5)
112+
time.sleep(2)
113+
self.assertEqual(pool.opened, 8)
114+
for conn in conns:
115+
conn.close()
116+
time.sleep(4)
117+
with pool.acquire():
118+
pass
119+
time.sleep(2)
120+
self.assertEqual(pool.opened, 4)
121+
88122

89123
if __name__ == "__main__":
90124
test_env.run_test_cases()

tests/ext/test_ext_1900_pool_shrink_async.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -----------------------------------------------------------------------------
2-
# Copyright (c) 2024, Oracle and/or its affiliates.
2+
# Copyright (c) 2024, 2025, Oracle and/or its affiliates.
33
#
44
# This software is dual-licensed to you under the Universal Permissive License
55
# (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
@@ -69,7 +69,7 @@ async def test_ext_1901(self):
6969
self.assertEqual(pool.opened, 3)
7070

7171
async def test_ext_1902(self):
72-
"E1902 - test pool shrinks to min on pool inactivity"
72+
"E1902 - test pool timeout shrinks to min on pool inactivity"
7373
pool = test_env.get_pool_async(min=3, max=10, increment=2, timeout=4)
7474
conns = [await pool.acquire() for i in range(6)]
7575
self.assertEqual(pool.opened, 6)
@@ -79,7 +79,7 @@ async def test_ext_1902(self):
7979
self.assertEqual(pool.opened, 3)
8080

8181
async def test_ext_1903(self):
82-
"E1902 - test pool eliminates extra connections on inactivity"
82+
"E1902 - test pool timeout eliminates extra connections on inactivity"
8383
pool = test_env.get_pool_async(min=4, max=10, increment=4, timeout=3)
8484
conns = [await pool.acquire() for i in range(5)]
8585
self.assertEqual(pool.opened, 5)
@@ -89,6 +89,38 @@ async def test_ext_1903(self):
8989
self.assertEqual(pool.opened, 5)
9090
del conns
9191

92+
async def test_ext_1904(self):
93+
"E1904 - test pool max_lifetime_session on release"
94+
pool = test_env.get_pool_async(
95+
min=4, max=10, increment=4, max_lifetime_session=3
96+
)
97+
conns = [await pool.acquire() for i in range(5)]
98+
self.assertEqual(pool.opened, 5)
99+
await asyncio.sleep(2)
100+
self.assertEqual(pool.opened, 8)
101+
await asyncio.sleep(2)
102+
for conn in conns:
103+
await conn.close()
104+
await asyncio.sleep(2)
105+
self.assertEqual(pool.opened, 4)
106+
107+
async def test_ext_1905(self):
108+
"E1905 - test pool max_lifetime_session on acquire"
109+
pool = test_env.get_pool_async(
110+
min=4, max=10, increment=4, max_lifetime_session=4
111+
)
112+
conns = [await pool.acquire() for i in range(5)]
113+
self.assertEqual(pool.opened, 5)
114+
await asyncio.sleep(2)
115+
self.assertEqual(pool.opened, 8)
116+
for conn in conns:
117+
await conn.close()
118+
await asyncio.sleep(4)
119+
async with pool.acquire():
120+
pass
121+
await asyncio.sleep(2)
122+
self.assertEqual(pool.opened, 4)
123+
92124

93125
if __name__ == "__main__":
94126
test_env.run_test_cases()

0 commit comments

Comments
 (0)