Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/130 ignore socket errors #188

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ server supports.
information.


.. py:class:: StatsClient(host='localhost', port=8125, prefix=None, maxudpsize=512)
.. py:class:: StatsClient(host='localhost', port=8125, prefix=None, maxudpsize=512, ignore_socket_errors=False)

Create a new ``StatsClient`` instance with the appropriate connection and
prefix information.
Expand All @@ -27,11 +27,22 @@ server supports.
:param int maxudpsize: the largest safe UDP packet to send. 512 is
generally considered safe for the public internet, but private networks
may support larger packet sizes.
:param bool ignore_socket_errors: whether or not to ignore socket errors
when initializing the client. If set to ``True``, the client will ignore
the error and continue. If set to ``False``, the client will raise the error.
You can check the ``is_ready`` attribute to see if the client is ready to send.

.. py:method:: StatsClient.close()

Close the underlying UDP socket.

.. py:method:: StatsClient.reset()

Resets the underlying UDP socket. This is useful if the connection is
not ready due to network issues or if the connection was closed. This
method will honor the ``ignore_socket_errors`` parameter in the constructor.
You can check the ``is_ready`` attribute to see if the client is ready to send.

.. py:method:: StatsClient.incr(stat, count=1, rate=1)

Increment a :ref:`counter <counter-type>`.
Expand Down
9 changes: 9 additions & 0 deletions statsd/client/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@
class StatsClientBase:
"""A Base class for various statsd clients."""

@property
def is_ready(self):
"""Returns True if the client is ready to send data."""
raise NotImplementedError()

def reset(self):
"""Reset the client."""
raise NotImplementedError()

def close(self):
"""Used to close and clean up any underlying resources."""
raise NotImplementedError()
Expand Down
13 changes: 13 additions & 0 deletions statsd/client/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ def close(self):
self._sock.close()
self._sock = None

def reset(self):
self.reconnect()

def reconnect(self):
self.close()
self.connect()
Expand Down Expand Up @@ -48,6 +51,11 @@ def __init__(self, host='localhost', port=8125, prefix=None,
self._prefix = prefix
self._sock = None

@property
def is_ready(self):
"""Return True if the client is ready to send data."""
return self._sock is not None

def connect(self):
fam = socket.AF_INET6 if self._ipv6 else socket.AF_INET
family, _, _, _, addr = socket.getaddrinfo(
Expand All @@ -67,6 +75,11 @@ def __init__(self, socket_path, prefix=None, timeout=None):
self._prefix = prefix
self._sock = None

@property
def is_ready(self):
"""Return True if the client is ready to send data."""
return self._sock is not None

def connect(self):
self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self._sock.settimeout(self._timeout)
Expand Down
40 changes: 34 additions & 6 deletions statsd/client/udp.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,39 @@ class StatsClient(StatsClientBase):
"""A client for statsd."""

def __init__(self, host='localhost', port=8125, prefix=None,
maxudpsize=512, ipv6=False):
maxudpsize=512, ipv6=False, ignore_socket_errors=False):
"""Create a new client."""
fam = socket.AF_INET6 if ipv6 else socket.AF_INET
family, _, _, _, addr = socket.getaddrinfo(
host, port, fam, socket.SOCK_DGRAM)[0]
self._addr = addr
self._sock = socket.socket(family, socket.SOCK_DGRAM)
self._host = host
self._port = port
self._ipv6 = ipv6
self._prefix = prefix
self._maxudpsize = maxudpsize
self._addr = None
self._sock = None
self._ignore_socket_errors = ignore_socket_errors
self._ready = False
self._open()

@property
def is_ready(self):
"""Return True if the client is ready to send data."""
return self._ready

def _open(self):
fam = socket.AF_INET6 if self._ipv6 else socket.AF_INET

try:
family, _, _, _, addr = socket.getaddrinfo(
self._host, self._port, fam, socket.SOCK_DGRAM)[0]
self._addr = addr
self._sock = socket.socket(family, socket.SOCK_DGRAM)
self._prefix = self._prefix
self._maxudpsize = self._maxudpsize
self._ready = True
except (socket.gaierror, OSError):
if not self._ignore_socket_errors:
raise
self._ready = False

def _send(self, data):
"""Send data to statsd."""
Expand All @@ -44,6 +68,10 @@ def _send(self, data):
# No time for love, Dr. Jones!
pass

def reset(self):
self.close()
self._open()

def close(self):
if self._sock and hasattr(self._sock, 'close'):
self._sock.close()
Expand Down
46 changes: 40 additions & 6 deletions statsd/tests.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import functools
import os
import random
import re
import socket
Expand Down Expand Up @@ -35,12 +36,18 @@ def eq_(a, b, msg=None):
assert a == b, msg


def _udp_client(prefix=None, addr=None, port=None, ipv6=False):
def _is_travis():
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to ignore Travis :-)

return os.environ.get('TRAVIS') in ('true', '1')


def _udp_client(prefix=None, addr=None, port=None, ipv6=False,
ignore_socket_errors=False):
if not addr:
addr = ADDR[0]
if not port:
port = ADDR[1]
sc = StatsClient(host=addr, port=port, prefix=prefix, ipv6=ipv6)
sc = StatsClient(host=addr, port=port, prefix=prefix, ipv6=ipv6,
ignore_socket_errors=ignore_socket_errors)
sc._sock = mock.Mock()
return sc

Expand Down Expand Up @@ -268,20 +275,47 @@ def _test_resolution(cl, proto, addr):
_sock_check(cl._sock, 1, proto, 'foo:1|c', addr=addr)


def test_ipv6_resolution_tcp():
cl = _tcp_client(addr='localhost', ipv6=True)
_test_resolution(cl, 'tcp', ('::1', 8125, 0, 0))


def test_ipv6_resolution_udp():
raise SkipTest('IPv6 resolution is broken on Travis')
if _is_travis():
raise SkipTest('IPv6 resolution is broken on Travis')
cl = _udp_client(addr='localhost', ipv6=True)
_test_resolution(cl, 'udp', ('::1', 8125, 0, 0))
assert cl.is_ready is True


def test_ipv6_resolution_tcp():
cl = _tcp_client(addr='localhost', ipv6=True)
_test_resolution(cl, 'tcp', ('::1', 8125, 0, 0))
def test_ipv6_error_in_resolution_udp():
if _is_travis():
raise SkipTest('IPv6 resolution is broken on Travis')
with assert_raises(socket.gaierror):
_ = _udp_client(addr='fakehost', ipv6=True)


def test_ipv6_error_in_resolution_ignored_udp():
if _is_travis():
raise SkipTest('IPv6 resolution is broken on Travis')
cl = _udp_client(addr='fakehost', ipv6=True, ignore_socket_errors=True)
assert cl.is_ready is False


def test_ipv4_resolution_udp():
cl = _udp_client(addr='localhost')
_test_resolution(cl, 'udp', ('127.0.0.1', 8125))
assert cl.is_ready is True


def test_ipv4_error_in_resolution_udp():
with assert_raises(socket.gaierror):
_ = _udp_client(addr='fakehost')


def test_ipv4_error_in_resolution_ignored_udp():
cl = _udp_client(addr='fakehost', ignore_socket_errors=True)
assert cl.is_ready is False


def test_ipv4_resolution_tcp():
Expand Down