Expand exceptions

This commit is contained in:
zandercymatics 2023-10-13 11:24:58 -06:00
parent fa6ed6f318
commit a05d02bbfa
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
5 changed files with 133 additions and 24 deletions

View file

@ -45,9 +45,6 @@ except NameError:
# Attn: these imports should NOT be at the top of the file # Attn: these imports should NOT be at the top of the file
try: try:
from .client import CLIENT, commands from .client import CLIENT, commands
except ImportError:
pass
try:
from .errors import RegistryError, ErrorCode from .errors import RegistryError, ErrorCode
from epplib.models import common, info from epplib.models import common, info
from epplib.responses import extensions from epplib.responses import extensions

View file

@ -3,6 +3,8 @@
import logging import logging
from time import sleep from time import sleep
from epplibwrapper.utility.pool_status import PoolStatus
try: try:
from epplib.client import Client from epplib.client import Client
from epplib import commands from epplib import commands
@ -16,7 +18,7 @@ from django.conf import settings
from .cert import Cert, Key from .cert import Cert, Key
from .errors import LoginError, RegistryError from .errors import LoginError, RegistryError
from .socket import Socket from .socket import Socket
from .utility.pool import EppConnectionPool from .utility.pool import EPPConnectionPool
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -32,7 +34,6 @@ except Exception:
exc_info=True, exc_info=True,
) )
class EPPLibWrapper: class EPPLibWrapper:
""" """
A wrapper over epplib's client. A wrapper over epplib's client.
@ -61,35 +62,33 @@ class EPPLibWrapper:
password=settings.SECRET_REGISTRY_KEY_PASSPHRASE, password=settings.SECRET_REGISTRY_KEY_PASSPHRASE,
) )
) )
options = {
self.pool_options = {
# Pool size # Pool size
"size": settings.EPP_CONNECTION_POOL_SIZE, "size": settings.EPP_CONNECTION_POOL_SIZE,
# Which errors the pool should look out for # Which errors the pool should look out for
"exc_classes": ( "exc_classes": (
LoginError, TransportError,
RegistryError,
), ),
# Occasionally pings the registry to keep the connection alive # Occasionally pings the registry to keep the connection alive.
# Value in seconds => (keepalive / size)
"keepalive": settings.POOL_KEEP_ALIVE, "keepalive": settings.POOL_KEEP_ALIVE,
} }
self._pool = None self._pool = None
# Since we reuse the same creds for each pool, we can test on # Tracks the status of the pool
# one socket, and if successful, then we know we can connect. self.pool_status = PoolStatus()
if not settings.DEBUG or self._test_registry_connection_success():
self._pool = EppConnectionPool( self.start_connection_pool()
client=self._client, login=self._login, options=options
)
else:
logger.warning("Cannot contact the Registry")
# TODO - signal that the app may need to restart?
def _send(self, command): def _send(self, command):
"""Helper function used by `send`.""" """Helper function used by `send`."""
cmd_type = command.__class__.__name__ cmd_type = command.__class__.__name__
try: try:
if self._pool is None: if not self.pool_status.connection_success:
raise LoginError raise LoginError(
"Couldn't connect to the registry after three attempts"
)
# TODO - add a timeout # TODO - add a timeout
with self._pool.get() as connection: with self._pool.get() as connection:
response = connection.send(command) response = connection.send(command)
@ -122,6 +121,21 @@ class EPPLibWrapper:
# try to prevent use of this method without appropriate safeguards # try to prevent use of this method without appropriate safeguards
if not cleaned: if not cleaned:
raise ValueError("Please sanitize user input before sending it.") raise ValueError("Please sanitize user input before sending it.")
# Reopen the pool if its closed
if not self.pool_status.pool_running:
# We want to reopen the connection pool,
# but we don't want the end user to wait while it opens.
# Raise syntax doesn't allow this, so we use a try/catch
# block.
try:
raise RegistryError(
"Can't contact the Registry. Please try again later"
)
except RegistryError as err:
raise err
finally:
self.start_connection_pool()
counter = 0 # we'll try 3 times counter = 0 # we'll try 3 times
while True: while True:
@ -134,6 +148,53 @@ class EPPLibWrapper:
else: # don't try again else: # don't try again
raise err raise err
def start_connection_pool(self, restart_pool_if_exists = True):
"""Starts a connection pool for the registry.
restart_pool_if_exists -> bool:
If an instance of the pool already exists,
then then that instance will be killed first.
It is generally recommended to keep this enabled."""
# Since we reuse the same creds for each pool, we can test on
# one socket, and if successful, then we know we can connect.
if settings.DEBUG or self._test_registry_connection_success():
logger.warning("Cannot contact the Registry")
self.pool_status.connection_success = False
# Q: Should err be raised instead?
# Q2: Since we try to connect 3 times,
# this indicates that the Registry isn't responsive.
# What should we do in this case?
return
else:
self.pool_status.connection_success = True
# If this function is reinvoked, then ensure
# that we don't have duplicate data sitting around.
if self._pool is not None and restart_pool_if_exists:
logger.info("Connection pool restarting...")
self.kill_pool()
self._pool = EPPConnectionPool(
client=self._client, login=self._login, options=self.pool_options
)
self.pool_status.pool_running = True
logger.info("Connection pool started")
def kill_pool(self):
"""Kills the existing pool. Use this instead
of self._pool = None, as that doesn't clear
gevent instances."""
if self._pool is not None:
self._pool.kill_all_connections()
self._pool = None
self.pool_status.pool_running = False
return
logger.info(
"kill_pool() was invoked but there was no pool to delete"
)
def _test_registry_connection_success(self): def _test_registry_connection_success(self):
"""Check that determines if our login """Check that determines if our login
credentials are valid, and/or if the Registrar credentials are valid, and/or if the Registrar

View file

@ -79,6 +79,9 @@ class RegistryError(Exception):
def is_client_error(self): def is_client_error(self):
return self.code is not None and (self.code >= 2000 and self.code <= 2308) return self.code is not None and (self.code >= 2000 and self.code <= 2308)
def is_not_retryable(self):
pass
class LoginError(RegistryError): class LoginError(RegistryError):

View file

@ -1,4 +1,6 @@
from collections import deque
import logging import logging
import gevent
from geventconnpool import ConnectionPool from geventconnpool import ConnectionPool
from epplibwrapper.errors import RegistryError, LoginError from epplibwrapper.errors import RegistryError, LoginError
from epplibwrapper.socket import Socket from epplibwrapper.socket import Socket
@ -11,15 +13,23 @@ except ImportError:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class EppConnectionPool(ConnectionPool): class EPPConnectionPool(ConnectionPool):
def __init__(self, client, login, options): """A connection pool for EPPLib.
Args:
client (Client): The client
login (commands.Login): Login creds
options (dict): Options for the ConnectionPool
base class
"""
def __init__(self, client, login, options: dict):
# For storing shared credentials # For storing shared credentials
self._client = client self._client = client
self._login = login self._login = login
super().__init__(**options) super().__init__(**options)
def _new_connection(self): def _new_connection(self):
socket = self.create_socket(self._client, self._login) socket = self._create_socket(self._client, self._login)
try: try:
connection = socket.connect() connection = socket.connect()
return connection return connection
@ -37,7 +47,39 @@ class EppConnectionPool(ConnectionPool):
logger.error("Failed to keep the connection alive.", exc_info=True) logger.error("Failed to keep the connection alive.", exc_info=True)
raise RegistryError("Failed to keep the connection alive.") from err raise RegistryError("Failed to keep the connection alive.") from err
def create_socket(self, client, login) -> Socket: def _create_socket(self, client, login) -> Socket:
"""Creates and returns a socket instance""" """Creates and returns a socket instance"""
socket = Socket(client, login) socket = Socket(client, login)
return socket return socket
def get_connections(self):
"""Returns the connection queue"""
return self.conn
def kill_all_connections(self):
"""Kills all active connections in the pool."""
try:
gevent.killall(self.conn)
self.conn.clear()
# Clear the semaphore
for i in range(self.lock.counter):
self.lock.release()
# TODO - connection pool err
except Exception as err:
logger.error(
"Could not kill all connections."
)
raise err
def repopulate_all_connections(self):
"""Regenerates the connection pool.
If any connections exist, kill them first.
"""
if len(self.conn) > 0:
self.kill_all_connections()
for i in range(self.size):
self.lock.acquire()
for i in range(self.size):
gevent.spawn_later(self.SPAWN_FREQUENCY*i, self._addOne)

View file

@ -0,0 +1,6 @@
class PoolStatus:
"""A list of Booleans to keep track of Pool Status"""
def __init__(self):
self.pool_running = False
self.connection_success = False
self.pool_hanging = False