Files
opensearch-pyd/opensearch/connection_pool.py
T

328 lines
12 KiB
Python
Raw Normal View History

# SPDX-License-Identifier: Apache-2.0
#
# The OpenSearch Contributors require contributions made to
# this file be licensed under the Apache-2.0 license or a
# compatible open source license.
#
# Modifications Copyright OpenSearch Contributors. See
# GitHub history for details.
#
# Licensed to Elasticsearch B.V. under one or more contributor
# license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright
# ownership. Elasticsearch B.V. licenses this file to you under
# the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
2020-04-23 11:22:08 -05:00
2013-11-03 22:39:03 +01:00
import logging
import random
import threading
import time
2013-05-02 17:54:26 +02:00
try:
from Queue import Empty, PriorityQueue
except ImportError:
from queue import PriorityQueue, Empty
from .exceptions import ImproperlyConfigured
2021-08-13 15:51:50 +05:30
logger = logging.getLogger("opensearch")
2019-05-10 09:16:33 -06:00
2013-11-03 22:39:03 +01:00
2013-05-02 17:54:26 +02:00
class ConnectionSelector(object):
2013-05-22 20:30:44 +02:00
"""
Simple class used to select a connection from a list of currently live
connection instances. In init time it is passed a dictionary containing all
the connections' options which it can then use during the selection
process. When the `select` method is called it is given a list of
*currently* live connections to choose from.
The options dictionary is the one that has been passed to
2021-08-13 15:51:50 +05:30
:class:`~opensearch.Transport` as `hosts` param and the same that is
2013-05-22 20:30:44 +02:00
used to construct the Connection object itself. When the Connection was
created from information retrieved from the cluster via the sniffing
process it will be the dictionary returned by the `host_info_callback`.
Example of where this would be useful is a zone-aware selector that would
only select connections from it's own zones and only fall back to other
connections where there would be none in it's zones.
"""
2019-05-10 09:16:33 -06:00
2013-05-02 17:54:26 +02:00
def __init__(self, opts):
2013-05-22 20:30:44 +02:00
"""
2013-09-24 15:49:38 +02:00
:arg opts: dictionary of connection instances and their options
2013-05-22 20:30:44 +02:00
"""
2013-05-02 17:54:26 +02:00
self.connection_opts = opts
2013-05-22 20:30:44 +02:00
def select(self, connections):
"""
Select a connection from the given list.
:arg connections: list of live connections to choose from
"""
pass
2013-05-02 17:54:26 +02:00
2013-05-22 20:30:38 +02:00
class RandomSelector(ConnectionSelector):
"""
Select a connection at random
"""
2019-05-10 09:16:33 -06:00
2013-05-22 20:30:38 +02:00
def select(self, connections):
return random.choice(connections)
2013-05-02 17:54:26 +02:00
class RoundRobinSelector(ConnectionSelector):
2013-05-22 20:30:44 +02:00
"""
Selector using round-robin.
"""
2019-05-10 09:16:33 -06:00
2013-05-02 17:54:26 +02:00
def __init__(self, opts):
super(RoundRobinSelector, self).__init__(opts)
self.data = threading.local()
2013-05-02 17:54:26 +02:00
def select(self, connections):
2019-05-10 09:16:33 -06:00
self.data.rr = getattr(self.data, "rr", -1) + 1
self.data.rr %= len(connections)
return connections[self.data.rr]
2013-05-02 17:54:26 +02:00
2019-05-10 09:16:33 -06:00
2013-05-02 17:54:26 +02:00
class ConnectionPool(object):
2013-05-22 20:30:44 +02:00
"""
2021-08-13 15:51:50 +05:30
Container holding the :class:`~opensearch.Connection` instances,
2013-05-22 20:30:44 +02:00
managing the selection process (via a
2021-08-13 15:51:50 +05:30
:class:`~opensearch.ConnectionSelector`) and dead connections.
2013-05-22 20:30:44 +02:00
2021-08-13 15:51:50 +05:30
It's only interactions are with the :class:`~opensearch.Transport` class
2013-05-22 20:30:44 +02:00
that drives all the actions within `ConnectionPool`.
Initially connections are stored on the class as a list and, along with the
connection options, get passed to the `ConnectionSelector` instance for
future reference.
Upon each request the `Transport` will ask for a `Connection` via the
`get_connection` method. If the connection fails (it's `perform_request`
raises a `ConnectionError`) it will be marked as dead (via `mark_dead`) and
put on a timeout (if it fails N times in a row the timeout is exponentially
longer - the formula is `default_timeout * 2 ** (fail_count - 1)`). When
the timeout is over the connection will be resurrected and returned to the
live pool. A connection that has been previously marked as dead and
succeeds will be marked as live (its fail count will be deleted).
2013-05-22 20:30:44 +02:00
"""
2019-05-10 09:16:33 -06:00
def __init__(
self,
connections,
dead_timeout=60,
timeout_cutoff=5,
selector_class=RoundRobinSelector,
randomize_hosts=True,
**kwargs
):
2013-05-22 20:30:44 +02:00
"""
:arg connections: list of tuples containing the
2021-08-13 15:51:50 +05:30
:class:`~opensearch.Connection` instance and it's options
2013-05-22 20:30:44 +02:00
:arg dead_timeout: number of seconds a connection should be retired for
after a failure, increases on consecutive failures
:arg timeout_cutoff: number of consecutive failures after which the
timeout doesn't increase
2021-08-13 15:51:50 +05:30
:arg selector_class: :class:`~opensearch.ConnectionSelector`
2014-12-20 00:34:42 +01:00
subclass to use if more than one connection is live
2013-05-22 20:30:44 +02:00
:arg randomize_hosts: shuffle the list of connections upon arrival to
avoid dog piling effect across processes
"""
if not connections:
2019-05-10 09:16:33 -06:00
raise ImproperlyConfigured(
"No defined connections, you need to " "specify at least one host."
)
self.connection_opts = connections
2013-05-02 17:54:26 +02:00
self.connections = [c for (c, opts) in connections]
2014-12-20 00:34:42 +01:00
# remember original connection list for resurrect(force=True)
self.orig_connections = tuple(self.connections)
2013-05-22 20:30:44 +02:00
# PriorityQueue for thread safety and ease of timeout management
self.dead = PriorityQueue(len(self.connections))
self.dead_count = {}
2013-05-02 17:54:26 +02:00
if randomize_hosts:
# randomize the connection list to avoid all clients hitting same node
# after startup/restart
random.shuffle(self.connections)
# default timeout after which to try resurrecting a connection
self.dead_timeout = dead_timeout
self.timeout_cutoff = timeout_cutoff
2013-05-02 17:54:26 +02:00
self.selector = selector_class(dict(connections))
def mark_dead(self, connection, now=None):
2013-05-22 20:30:44 +02:00
"""
Mark the connection as dead (failed). Remove it from the live pool and
put it on a timeout.
:arg connection: the failed instance
"""
2013-05-02 17:54:26 +02:00
# allow inject for testing purposes
now = now if now else time.time()
try:
self.connections.remove(connection)
except ValueError:
2020-03-09 11:51:35 -05:00
logger.info(
"Attempted to remove %r, but it does not exist in the connection pool.",
connection,
)
# connection not alive or another thread marked it already, ignore
2013-05-02 17:54:26 +02:00
return
else:
dead_count = self.dead_count.get(connection, 0) + 1
self.dead_count[connection] = dead_count
timeout = self.dead_timeout * 2 ** min(dead_count - 1, self.timeout_cutoff)
self.dead.put((now + timeout, connection))
2013-11-03 22:39:03 +01:00
logger.warning(
2019-05-10 09:16:33 -06:00
"Connection %r has failed for %i times in a row, putting on %i second timeout.",
connection,
dead_count,
timeout,
2013-11-03 22:39:03 +01:00
)
def mark_live(self, connection):
2013-05-22 20:30:44 +02:00
"""
Mark connection as healthy after a resurrection. Resets the fail
counter for the connection.
:arg connection: the connection to redeem
"""
try:
del self.dead_count[connection]
except KeyError:
# race condition, safe to ignore
pass
2013-05-02 17:54:26 +02:00
def resurrect(self, force=False):
2013-05-22 20:30:44 +02:00
"""
Attempt to resurrect a connection from the dead pool. It will try to
locate one (not all) eligible (it's timeout is over) connection to
2014-12-20 00:34:42 +01:00
return to the live pool. Any resurrected connection is also returned.
2013-05-22 20:30:44 +02:00
:arg force: resurrect a connection even if there is none eligible (used
2014-12-20 00:34:42 +01:00
when we have no live connections). If force is specified resurrect
always returns a connection.
2013-05-22 20:30:44 +02:00
"""
2013-05-02 17:54:26 +02:00
# no dead connections
if self.dead.empty():
2014-12-20 00:34:42 +01:00
# we are forced to return a connection, take one from the original
# list. This is to avoid a race condition where get_connection can
# see no live connections but when it calls resurrect self.dead is
# also empty. We assume that other threat has resurrected all
# available connections so we can safely return one at random.
if force:
return random.choice(self.orig_connections)
2013-05-02 17:54:26 +02:00
return
try:
# retrieve a connection to check
timeout, connection = self.dead.get(block=False)
except Empty:
2014-12-20 00:34:42 +01:00
# other thread has been faster and the queue is now empty. If we
# are forced, return a connection at random again.
if force:
return random.choice(self.orig_connections)
return
if not force and timeout > time.time():
# return it back if not eligible and not forced
self.dead.put((timeout, connection))
2013-05-02 17:54:26 +02:00
return
# either we were forced or the connection is elligible to be retried
2013-05-02 17:54:26 +02:00
self.connections.append(connection)
2019-05-10 09:16:33 -06:00
logger.info("Resurrecting connection %r (force=%s).", connection, force)
2014-12-20 00:34:42 +01:00
return connection
2013-05-02 17:54:26 +02:00
def get_connection(self):
2013-05-22 20:30:44 +02:00
"""
Return a connection from the pool using the `ConnectionSelector`
instance.
It tries to resurrect eligible connections, forces a resurrection when
2021-05-25 14:44:41 +02:00
no connections are available and passes the list of live connections to
2013-05-22 20:30:44 +02:00
the selector instance to choose from.
Returns a connection instance and it's current fail count.
"""
self.resurrect()
2014-12-20 00:34:42 +01:00
connections = self.connections[:]
# no live nodes, resurrect one by force and return it
if not connections:
return self.resurrect(True)
2013-05-02 17:54:26 +02:00
2014-12-20 00:34:42 +01:00
# only call selector if we have a selection
if len(connections) > 1:
return self.selector.select(connections)
2013-05-02 17:54:26 +02:00
2014-12-20 00:34:42 +01:00
# only one connection, no need for a selector
return connections[0]
def close(self):
"""
Explicitly closes connections
"""
2020-05-13 13:21:42 -05:00
for conn in self.connections:
conn.close()
2020-05-13 13:21:42 -05:00
def __repr__(self):
return "<%s: %r>" % (type(self).__name__, self.connections)
2019-05-10 09:16:33 -06:00
class DummyConnectionPool(ConnectionPool):
def __init__(self, connections, **kwargs):
if len(connections) != 1:
2019-05-10 09:16:33 -06:00
raise ImproperlyConfigured(
"DummyConnectionPool needs exactly one " "connection defined."
)
# we need connection opts for sniffing logic
self.connection_opts = connections
self.connection = connections[0][0]
2019-05-10 09:16:33 -06:00
self.connections = (self.connection,)
def get_connection(self):
return self.connection
def close(self):
"""
Explicitly closes connections
"""
2016-03-03 19:50:17 -05:00
self.connection.close()
def _noop(self, *args, **kwargs):
pass
2013-05-02 17:54:26 +02:00
2019-05-10 09:16:33 -06:00
mark_dead = mark_live = resurrect = _noop
2020-05-13 13:21:42 -05:00
class EmptyConnectionPool(ConnectionPool):
"""A connection pool that is empty. Errors out if used."""
def __init__(self, *_, **__):
self.connections = []
self.connection_opts = []
def get_connection(self):
raise ImproperlyConfigured("No connections were configured")
def _noop(self, *args, **kwargs):
pass
close = mark_dead = mark_live = resurrect = _noop