Skip to content

Commit f455216

Browse files
authored
Merge pull request #182 from zhenlineo/1.5-max-pool-size
Max connection pool size
2 parents ffcc17c + acfac71 commit f455216

File tree

12 files changed

+162
-48
lines changed

12 files changed

+162
-48
lines changed

neo4j/bolt/connection.py

+57-21
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,27 @@
3232
from select import select
3333
from socket import socket, SOL_SOCKET, SO_KEEPALIVE, SHUT_RDWR, error as SocketError, timeout as SocketTimeout, AF_INET, AF_INET6
3434
from struct import pack as struct_pack, unpack as struct_unpack
35-
from threading import RLock
35+
from threading import RLock, Condition
3636

3737
from neo4j.addressing import SocketAddress, is_ip_address
3838
from neo4j.bolt.cert import KNOWN_HOSTS
3939
from neo4j.bolt.response import InitResponse, AckFailureResponse, ResetResponse
4040
from neo4j.compat.ssl import SSL_AVAILABLE, HAS_SNI, SSLError
41-
from neo4j.exceptions import ProtocolError, SecurityError, ServiceUnavailable
41+
from neo4j.exceptions import ClientError, ProtocolError, SecurityError, ServiceUnavailable
4242
from neo4j.meta import version
4343
from neo4j.packstream import Packer, Unpacker
4444
from neo4j.util import import_best as _import_best
45+
from time import clock
4546

4647
ChunkedInputBuffer = _import_best("neo4j.bolt._io", "neo4j.bolt.io").ChunkedInputBuffer
4748
ChunkedOutputBuffer = _import_best("neo4j.bolt._io", "neo4j.bolt.io").ChunkedOutputBuffer
4849

4950

51+
INFINITE = -1
52+
DEFAULT_MAX_CONNECTION_LIFETIME = INFINITE
53+
DEFAULT_MAX_CONNECTION_POOL_SIZE = INFINITE
5054
DEFAULT_CONNECTION_TIMEOUT = 5.0
55+
DEFAULT_CONNECTION_ACQUISITION_TIMEOUT = 60
5156
DEFAULT_PORT = 7687
5257
DEFAULT_USER_AGENT = "neo4j-python/%s" % version
5358

@@ -178,6 +183,8 @@ def __init__(self, address, sock, error_handler, **config):
178183
self.packer = Packer(self.output_buffer)
179184
self.unpacker = Unpacker()
180185
self.responses = deque()
186+
self._max_connection_lifetime = config.get("max_connection_lifetime", DEFAULT_MAX_CONNECTION_LIFETIME)
187+
self._creation_timestamp = clock()
181188

182189
# Determine the user agent and ensure it is a Unicode value
183190
user_agent = config.get("user_agent", DEFAULT_USER_AGENT)
@@ -201,6 +208,7 @@ def __init__(self, address, sock, error_handler, **config):
201208
# Pick up the server certificate, if any
202209
self.der_encoded_server_certificate = config.get("der_encoded_server_certificate")
203210

211+
def Init(self):
204212
response = InitResponse(self)
205213
self.append(INIT, (self.user_agent, self.auth_dict), response=response)
206214
self.sync()
@@ -360,6 +368,9 @@ def _unpack(self):
360368
more = False
361369
return details, summary_signature, summary_metadata
362370

371+
def timedout(self):
372+
return 0 <= self._max_connection_lifetime <= clock() - self._creation_timestamp
373+
363374
def sync(self):
364375
""" Send and fetch all outstanding messages.
365376
@@ -396,11 +407,14 @@ class ConnectionPool(object):
396407

397408
_closed = False
398409

399-
def __init__(self, connector, connection_error_handler):
410+
def __init__(self, connector, connection_error_handler, **config):
400411
self.connector = connector
401412
self.connection_error_handler = connection_error_handler
402413
self.connections = {}
403414
self.lock = RLock()
415+
self.cond = Condition(self.lock)
416+
self._max_connection_pool_size = config.get("max_connection_pool_size", DEFAULT_MAX_CONNECTION_POOL_SIZE)
417+
self._connection_acquisition_timeout = config.get("connection_acquisition_timeout", DEFAULT_CONNECTION_ACQUISITION_TIMEOUT)
404418

405419
def __enter__(self):
406420
return self
@@ -424,23 +438,42 @@ def acquire_direct(self, address):
424438
connections = self.connections[address]
425439
except KeyError:
426440
connections = self.connections[address] = deque()
427-
for connection in list(connections):
428-
if connection.closed() or connection.defunct():
429-
connections.remove(connection)
430-
continue
431-
if not connection.in_use:
432-
connection.in_use = True
433-
return connection
434-
try:
435-
connection = self.connector(address, self.connection_error_handler)
436-
except ServiceUnavailable:
437-
self.remove(address)
438-
raise
439-
else:
440-
connection.pool = self
441-
connection.in_use = True
442-
connections.append(connection)
443-
return connection
441+
442+
connection_acquisition_start_timestamp = clock()
443+
while True:
444+
# try to find a free connection in pool
445+
for connection in list(connections):
446+
if connection.closed() or connection.defunct() or connection.timedout():
447+
connections.remove(connection)
448+
continue
449+
if not connection.in_use:
450+
connection.in_use = True
451+
return connection
452+
# all connections in pool are in-use
453+
can_create_new_connection = self._max_connection_pool_size == INFINITE or len(connections) < self._max_connection_pool_size
454+
if can_create_new_connection:
455+
try:
456+
connection = self.connector(address, self.connection_error_handler)
457+
except ServiceUnavailable:
458+
self.remove(address)
459+
raise
460+
else:
461+
connection.pool = self
462+
connection.in_use = True
463+
connections.append(connection)
464+
return connection
465+
466+
# failed to obtain a connection from pool because the pool is full and no free connection in the pool
467+
span_timeout = self._connection_acquisition_timeout - (clock() - connection_acquisition_start_timestamp)
468+
if span_timeout > 0:
469+
self.cond.wait(span_timeout)
470+
# if timed out, then we throw error. This time computation is needed, as with python 2.7, we cannot
471+
# tell if the condition is notified or timed out when we come to this line
472+
if self._connection_acquisition_timeout <= (clock() - connection_acquisition_start_timestamp):
473+
raise ClientError("Failed to obtain a connection from pool within {!r}s".format(
474+
self._connection_acquisition_timeout))
475+
else:
476+
raise ClientError("Failed to obtain a connection from pool within {!r}s".format(self._connection_acquisition_timeout))
444477

445478
def acquire(self, access_mode=None):
446479
""" Acquire a connection to a server that can satisfy a set of parameters.
@@ -454,6 +487,7 @@ def release(self, connection):
454487
"""
455488
with self.lock:
456489
connection.in_use = False
490+
self.cond.notify_all()
457491

458492
def in_use_connection_count(self, address):
459493
""" Count the number of connections currently in use to a given
@@ -600,8 +634,10 @@ def connect(address, ssl_context=None, error_handler=None, **config):
600634
s.shutdown(SHUT_RDWR)
601635
s.close()
602636
elif agreed_version == 1:
603-
return Connection(address, s, der_encoded_server_certificate=der_encoded_server_certificate,
637+
connection = Connection(address, s, der_encoded_server_certificate=der_encoded_server_certificate,
604638
error_handler=error_handler, **config)
639+
connection.Init()
640+
return connection
605641
elif agreed_version == 0x48545450:
606642
log_error("S: [CLOSE]")
607643
s.close()

neo4j/v1/__init__.py

-3
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@
1818
# See the License for the specific language governing permissions and
1919
# limitations under the License.
2020

21-
22-
from neo4j.exceptions import *
23-
2421
from .api import *
2522
from .direct import *
2623
from .exceptions import *

neo4j/v1/api.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
from time import time, sleep
2626
from warnings import warn
2727

28-
from neo4j.bolt import ProtocolError, ServiceUnavailable
29-
from neo4j.compat import unicode, urlparse
28+
from neo4j.exceptions import ProtocolError, ServiceUnavailable
29+
from neo4j.compat import urlparse
3030
from neo4j.exceptions import CypherError, TransientError
3131

3232
from .exceptions import DriverError, SessionError, SessionExpired, TransactionError

neo4j/v1/direct.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121

2222
from neo4j.addressing import SocketAddress, resolve
23-
from neo4j.bolt import DEFAULT_PORT, ConnectionPool, connect, ConnectionErrorHandler
23+
from neo4j.bolt.connection import DEFAULT_PORT, ConnectionPool, connect, ConnectionErrorHandler
2424
from neo4j.exceptions import ServiceUnavailable
2525
from neo4j.v1.api import Driver
2626
from neo4j.v1.security import SecurityPlan
@@ -37,8 +37,8 @@ def __init__(self):
3737

3838
class DirectConnectionPool(ConnectionPool):
3939

40-
def __init__(self, connector, address):
41-
super(DirectConnectionPool, self).__init__(connector, DirectConnectionErrorHandler())
40+
def __init__(self, connector, address, **config):
41+
super(DirectConnectionPool, self).__init__(connector, DirectConnectionErrorHandler(), **config)
4242
self.address = address
4343

4444
def acquire(self, access_mode=None):
@@ -73,7 +73,7 @@ def __init__(self, uri, **config):
7373
def connector(address, error_handler):
7474
return connect(address, security_plan.ssl_context, error_handler, **config)
7575

76-
pool = DirectConnectionPool(connector, self.address)
76+
pool = DirectConnectionPool(connector, self.address, **config)
7777
pool.release(pool.acquire())
7878
Driver.__init__(self, pool, **config)
7979

neo4j/v1/routing.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535
LOAD_BALANCING_STRATEGY_LEAST_CONNECTED = 0
3636
LOAD_BALANCING_STRATEGY_ROUND_ROBIN = 1
37-
LOAD_BALANCING_STRATEGY_DEFAULT = LOAD_BALANCING_STRATEGY_LEAST_CONNECTED
37+
DEFAULT_LOAD_BALANCING_STRATEGY = LOAD_BALANCING_STRATEGY_LEAST_CONNECTED
3838

3939

4040
class OrderedSet(MutableSet):
@@ -166,7 +166,7 @@ class LoadBalancingStrategy(object):
166166

167167
@classmethod
168168
def build(cls, connection_pool, **config):
169-
load_balancing_strategy = config.get("load_balancing_strategy", LOAD_BALANCING_STRATEGY_DEFAULT)
169+
load_balancing_strategy = config.get("load_balancing_strategy", DEFAULT_LOAD_BALANCING_STRATEGY)
170170
if load_balancing_strategy == LOAD_BALANCING_STRATEGY_LEAST_CONNECTED:
171171
return LeastConnectedLoadBalancingStrategy(connection_pool)
172172
elif load_balancing_strategy == LOAD_BALANCING_STRATEGY_ROUND_ROBIN:
@@ -265,7 +265,7 @@ class RoutingConnectionPool(ConnectionPool):
265265
"""
266266

267267
def __init__(self, connector, initial_address, routing_context, *routers, **config):
268-
super(RoutingConnectionPool, self).__init__(connector, RoutingConnectionErrorHandler(self))
268+
super(RoutingConnectionPool, self).__init__(connector, RoutingConnectionErrorHandler(self), **config)
269269
self.initial_address = initial_address
270270
self.routing_context = routing_context
271271
self.routing_table = RoutingTable(routers)

test/integration/test_driver.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
# limitations under the License.
2020

2121

22-
from neo4j.v1 import GraphDatabase, ProtocolError, ServiceUnavailable
23-
22+
from neo4j.v1 import GraphDatabase, ServiceUnavailable
23+
from neo4j.exceptions import ProtocolError
2424
from test.integration.tools import IntegrationTestCase
2525

2626

test/integration/test_security.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
from ssl import SSLSocket
2424
from unittest import skipUnless
2525

26-
from neo4j.v1 import GraphDatabase, SSL_AVAILABLE, TRUST_ON_FIRST_USE, TRUST_CUSTOM_CA_SIGNED_CERTIFICATES, AuthError
26+
from neo4j.v1 import GraphDatabase, SSL_AVAILABLE, TRUST_ON_FIRST_USE, TRUST_CUSTOM_CA_SIGNED_CERTIFICATES
27+
from neo4j.exceptions import AuthError
2728

2829
from test.integration.tools import IntegrationTestCase
2930

test/integration/test_session.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
from neo4j.v1 import \
2525
READ_ACCESS, WRITE_ACCESS, \
2626
CypherError, SessionError, TransactionError, \
27-
Node, Relationship, Path, CypherSyntaxError
27+
Node, Relationship, Path
28+
from neo4j.exceptions import CypherSyntaxError
2829

2930
from test.integration.tools import DirectIntegrationTestCase
3031

test/integration/tools.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232

3333
from boltkit.controller import WindowsController, UnixController
3434

35-
from neo4j.v1 import GraphDatabase, AuthError
35+
from neo4j.v1 import GraphDatabase
36+
from neo4j.exceptions import AuthError
3637
from neo4j.util import ServerVersion
3738

3839
from test.env import NEO4J_SERVER_PACKAGE, NEO4J_USER, NEO4J_PASSWORD

test/performance/tools.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434

3535
from boltkit.controller import WindowsController, UnixController
3636

37-
from neo4j.v1 import GraphDatabase, AuthError
37+
from neo4j.v1 import GraphDatabase
38+
from neo4j.exceptions import AuthError
3839
from neo4j.util import ServerVersion
3940

4041
from test.env import NEO4J_SERVER_PACKAGE, NEO4J_USER, NEO4J_PASSWORD

test/stub/test_routingdriver.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121

2222
from neo4j.v1 import GraphDatabase, READ_ACCESS, WRITE_ACCESS, SessionExpired, \
2323
RoutingDriver, RoutingConnectionPool, LeastConnectedLoadBalancingStrategy, LOAD_BALANCING_STRATEGY_ROUND_ROBIN, \
24-
RoundRobinLoadBalancingStrategy, TransientError, ClientError
24+
RoundRobinLoadBalancingStrategy, TransientError
25+
from neo4j.exceptions import ClientError
2526
from neo4j.bolt import ProtocolError, ServiceUnavailable
2627

2728
from test.stub.tools import StubTestCase, StubCluster

0 commit comments

Comments
 (0)