Skip to content

Commit 46c5e9e

Browse files
committed
Refactor udp.sendto() and pyaddr->sockaddr conversion
* sendto() now tries to use uv_udp_try_send() first * __convert_pyaddr_to_sockaddr() was optimized to maintain an internal LRU cache of resolved addresses (quite an expensive operation)
1 parent c2b65bc commit 46c5e9e

File tree

8 files changed

+181
-58
lines changed

8 files changed

+181
-58
lines changed

uvloop/dns.pyx

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ cdef __convert_sockaddr_to_pyaddr(const system.sockaddr* addr):
4242
raise convert_error(err)
4343

4444
return (
45-
(<bytes>buf).decode(),
45+
PyUnicode_FromString(buf),
4646
system.ntohs(addr4.sin_port)
4747
)
4848

@@ -54,7 +54,7 @@ cdef __convert_sockaddr_to_pyaddr(const system.sockaddr* addr):
5454
raise convert_error(err)
5555

5656
return (
57-
(<bytes>buf).decode(),
57+
PyUnicode_FromString(buf),
5858
system.ntohs(addr6.sin6_port),
5959
system.ntohl(addr6.sin6_flowinfo),
6060
addr6.sin6_scope_id
@@ -63,6 +63,17 @@ cdef __convert_sockaddr_to_pyaddr(const system.sockaddr* addr):
6363
raise RuntimeError("cannot convert sockaddr into Python object")
6464

6565

66+
@cython.freelist(DEFAULT_FREELIST_SIZE)
67+
cdef class SockAddrHolder:
68+
cdef:
69+
int family
70+
system.sockaddr_storage addr
71+
Py_ssize_t addr_size
72+
73+
74+
cdef LruCache sockaddrs = LruCache(maxsize=DNS_PYADDR_TO_SOCKADDR_CACHE_SIZE)
75+
76+
6677
cdef __convert_pyaddr_to_sockaddr(int family, object addr,
6778
system.sockaddr* res):
6879
cdef:
@@ -72,7 +83,14 @@ cdef __convert_pyaddr_to_sockaddr(int family, object addr,
7283
int flowinfo = 0
7384
char *buf
7485
Py_ssize_t buflen
86+
SockAddrHolder ret
87+
88+
ret = sockaddrs.get(addr, None)
89+
if ret is not None and ret.family == family:
90+
memcpy(res, &ret.addr, ret.addr_size)
91+
return
7592

93+
ret = SockAddrHolder.__new__(SockAddrHolder)
7694
if family == uv.AF_INET:
7795
if not isinstance(addr, tuple):
7896
raise TypeError('AF_INET address must be tuple')
@@ -90,7 +108,8 @@ cdef __convert_pyaddr_to_sockaddr(int family, object addr,
90108

91109
port = __port_to_int(port, None)
92110

93-
err = uv.uv_ip4_addr(host, <int>port, <system.sockaddr_in*>res)
111+
ret.addr_size = sizeof(system.sockaddr_in)
112+
err = uv.uv_ip4_addr(host, <int>port, <system.sockaddr_in*>&ret.addr)
94113
if err < 0:
95114
raise convert_error(err)
96115

@@ -121,12 +140,14 @@ cdef __convert_pyaddr_to_sockaddr(int family, object addr,
121140
if addr_len > 3:
122141
scope_id = addr[3]
123142

124-
err = uv.uv_ip6_addr(host, port, <system.sockaddr_in6*>res)
143+
ret.addr_size = sizeof(system.sockaddr_in6)
144+
145+
err = uv.uv_ip6_addr(host, port, <system.sockaddr_in6*>&ret.addr)
125146
if err < 0:
126147
raise convert_error(err)
127148

128-
(<system.sockaddr_in6*>res).sin6_flowinfo = flowinfo
129-
(<system.sockaddr_in6*>res).sin6_scope_id = scope_id
149+
(<system.sockaddr_in6*>&ret.addr).sin6_flowinfo = flowinfo
150+
(<system.sockaddr_in6*>&ret.addr).sin6_scope_id = scope_id
130151

131152
elif family == uv.AF_UNIX:
132153
if isinstance(addr, str):
@@ -139,13 +160,18 @@ cdef __convert_pyaddr_to_sockaddr(int family, object addr,
139160
raise ValueError(
140161
f'unix socket path {addr!r} is longer than 107 characters')
141162

142-
memset(res, 0, sizeof(system.sockaddr_un))
143-
(<system.sockaddr_un*>res).sun_family = uv.AF_UNIX
144-
memcpy((<system.sockaddr_un*>res).sun_path, buf, buflen)
163+
ret.addr_size = sizeof(system.sockaddr_un)
164+
memset(&ret.addr, 0, sizeof(system.sockaddr_un))
165+
(<system.sockaddr_un*>&ret.addr).sun_family = uv.AF_UNIX
166+
memcpy((<system.sockaddr_un*>&ret.addr).sun_path, buf, buflen)
145167

146168
else:
147169
raise ValueError(
148-
f'epected AF_INET, AF_INET6, or AF_UNIX family, got {family}')
170+
f'expected AF_INET, AF_INET6, or AF_UNIX family, got {family}')
171+
172+
ret.family = family
173+
sockaddrs[addr] = ret
174+
memcpy(res, &ret.addr, ret.addr_size)
149175

150176

151177
cdef __static_getaddrinfo(object host, object port,

uvloop/handles/udp.pyx

Lines changed: 57 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ cdef class _UDPSendContext:
2020
self.closed = 1
2121
PyBuffer_Release(&self.py_buf) # void
2222
self.req.data = NULL
23+
self.uv_buf.base = NULL
2324
Py_DECREF(self)
2425
self.udp = None
2526

@@ -34,7 +35,8 @@ cdef class _UDPSendContext:
3435
Py_INCREF(ctx)
3536

3637
PyObject_GetBuffer(data, &ctx.py_buf, PyBUF_SIMPLE)
37-
ctx.uv_buf = uv.uv_buf_init(<char*>ctx.py_buf.buf, ctx.py_buf.len)
38+
ctx.uv_buf.base = <char*>ctx.py_buf.buf
39+
ctx.uv_buf.len = ctx.py_buf.len
3840
ctx.udp = udp
3941

4042
ctx.closed = 0
@@ -193,37 +195,63 @@ cdef class UDPTransport(UVBaseTransport):
193195
_UDPSendContext ctx
194196
system.sockaddr_storage saddr_st
195197
system.sockaddr *saddr
198+
Py_buffer try_pybuf
199+
uv.uv_buf_t try_uvbuf
200+
201+
self._ensure_alive()
196202

197203
if self._family not in (uv.AF_INET, uv.AF_INET6, uv.AF_UNIX):
198204
raise RuntimeError('UDPTransport.family is undefined; cannot send')
199205

200-
if addr is not None and self._family != uv.AF_UNIX:
201-
validate_address(addr, self._family, uv.SOCK_DGRAM, 0)
202-
203-
ctx = _UDPSendContext.new(self, data)
204-
try:
205-
if addr is None:
206-
saddr = NULL
207-
else:
206+
if addr is None:
207+
saddr = NULL
208+
else:
209+
try:
208210
__convert_pyaddr_to_sockaddr(self._family, addr,
209211
<system.sockaddr*>&saddr_st)
210-
saddr = <system.sockaddr*>(&saddr_st)
211-
except Exception:
212-
ctx.close()
213-
raise
214-
215-
err = uv.uv_udp_send(&ctx.req,
216-
<uv.uv_udp_t*>self._handle,
217-
&ctx.uv_buf,
218-
1,
219-
saddr,
220-
__uv_udp_on_send)
221-
222-
if err < 0:
223-
ctx.close()
212+
except (ValueError, TypeError):
213+
raise
214+
except Exception:
215+
raise ValueError(
216+
f'{addr!r}: socket family mismatch or '
217+
f'a DNS lookup is required')
218+
saddr = <system.sockaddr*>(&saddr_st)
219+
220+
if self._get_write_buffer_size() == 0:
221+
PyObject_GetBuffer(data, &try_pybuf, PyBUF_SIMPLE)
222+
try_uvbuf.base = <char*>try_pybuf.buf
223+
try_uvbuf.len = try_pybuf.len
224+
err = uv.uv_udp_try_send(<uv.uv_udp_t*>self._handle,
225+
&try_uvbuf,
226+
1,
227+
saddr)
228+
PyBuffer_Release(&try_pybuf)
229+
else:
230+
err = uv.UV_EAGAIN
231+
232+
if err == uv.UV_EAGAIN:
233+
ctx = _UDPSendContext.new(self, data)
234+
err = uv.uv_udp_send(&ctx.req,
235+
<uv.uv_udp_t*>self._handle,
236+
&ctx.uv_buf,
237+
1,
238+
saddr,
239+
__uv_udp_on_send)
240+
241+
if err < 0:
242+
ctx.close()
243+
244+
exc = convert_error(err)
245+
self._fatal_error(exc, True)
246+
else:
247+
self._maybe_pause_protocol()
224248

225-
exc = convert_error(err)
226-
self._fatal_error(exc, True)
249+
else:
250+
if err < 0:
251+
exc = convert_error(err)
252+
self._fatal_error(exc, True)
253+
else:
254+
self._on_sent(None)
227255

228256
cdef _on_receive(self, bytes data, object exc, object addr):
229257
if exc is None:
@@ -248,15 +276,17 @@ cdef class UDPTransport(UVBaseTransport):
248276

249277
def sendto(self, data, addr=None):
250278
if not data:
279+
# Replicating asyncio logic here.
251280
return
252281

253282
if self._conn_lost:
254-
# TODO add warning
283+
# Replicating asyncio logic here.
284+
if self._conn_lost >= LOG_THRESHOLD_FOR_CONNLOST_WRITES:
285+
aio_logger.warning('socket.send() raised exception.')
255286
self._conn_lost += 1
256287
return
257288

258289
self._send(data, addr)
259-
self._maybe_pause_protocol()
260290

261291

262292
cdef void __uv_udp_on_receive(uv.uv_udp_t* handle,
@@ -357,17 +387,3 @@ cdef void __uv_udp_on_send(uv.uv_udp_send_t* req, int status) with gil:
357387
udp._on_sent(exc)
358388
except BaseException as exc:
359389
udp._error(exc, False)
360-
361-
362-
@ft_lru_cache()
363-
def validate_address(object addr, int sock_family, int sock_type,
364-
int sock_proto):
365-
addrinfo = __static_getaddrinfo_pyaddr(
366-
addr[0], addr[1],
367-
uv.AF_UNSPEC, sock_type, sock_proto, 0)
368-
if addrinfo is None:
369-
raise ValueError(
370-
'UDP.sendto(): address {!r} requires a DNS lookup'.format(addr))
371-
if addrinfo[0] != sock_family:
372-
raise ValueError(
373-
'UDP.sendto(): {!r} socket family mismatch'.format(addr))

uvloop/includes/consts.pxi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ DEF FLOW_CONTROL_HIGH_WATER_SSL_READ = 256 # KiB
55
DEF FLOW_CONTROL_HIGH_WATER_SSL_WRITE = 512 # KiB
66

77
DEF DEFAULT_FREELIST_SIZE = 250
8+
DEF DNS_PYADDR_TO_SOCKADDR_CACHE_SIZE = 2048
89

910
DEF DEBUG_STACK_DEPTH = 10
1011

uvloop/includes/python.pxd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
cdef extern from "Python.h":
22
int PY_VERSION_HEX
33

4+
unicode PyUnicode_FromString(const char *)
5+
46
void* PyMem_RawMalloc(size_t n)
57
void* PyMem_RawRealloc(void *p, size_t n)
68
void* PyMem_RawCalloc(size_t nelem, size_t elsize)

uvloop/includes/stdlib.pxi

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ cdef errno_EBADF = errno.EBADF
6060
cdef errno_EINVAL = errno.EINVAL
6161

6262
cdef ft_partial = functools.partial
63-
cdef ft_lru_cache = functools.lru_cache
6463

6564
cdef gc_disable = gc.disable
6665

uvloop/includes/uv.pxd

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -245,10 +245,6 @@ cdef extern from "uv.h" nogil:
245245
const system.sockaddr* addr,
246246
unsigned flags) with gil
247247

248-
# Buffers
249-
250-
uv_buf_t uv_buf_init(char* base, unsigned int len)
251-
252248
# Generic request functions
253249
int uv_cancel(uv_req_t* req)
254250

@@ -376,6 +372,9 @@ cdef extern from "uv.h" nogil:
376372
int uv_udp_send(uv_udp_send_t* req, uv_udp_t* handle,
377373
const uv_buf_t bufs[], unsigned int nbufs,
378374
const system.sockaddr* addr, uv_udp_send_cb send_cb)
375+
int uv_udp_try_send(uv_udp_t* handle,
376+
const uv_buf_t bufs[], unsigned int nbufs,
377+
const system.sockaddr* addr)
379378
int uv_udp_recv_start(uv_udp_t* handle, uv_alloc_cb alloc_cb,
380379
uv_udp_recv_cb recv_cb)
381380
int uv_udp_recv_stop(uv_udp_t* handle)

uvloop/loop.pyx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ from .includes.python cimport (
1919
PyMemoryView_FromMemory, PyBUF_WRITE,
2020
PyMemoryView_FromObject, PyMemoryView_Check,
2121
PyOS_AfterFork_Parent, PyOS_AfterFork_Child,
22-
PyOS_BeforeFork
22+
PyOS_BeforeFork,
23+
PyUnicode_FromString
2324
)
2425
from .includes.flowcontrol cimport add_flowcontrol_defaults
2526

@@ -2962,7 +2963,6 @@ cdef class Loop:
29622963

29632964
if rads is not None:
29642965
rai = (<AddrInfo>rads).data
2965-
sock = udp._get_socket()
29662966
while rai is not NULL:
29672967
if rai.ai_family != lai.ai_family:
29682968
rai = rai.ai_next
@@ -3088,6 +3088,7 @@ class _SyncSocketWriterFuture(aio_Future):
30883088

30893089
include "cbhandles.pyx"
30903090
include "pseudosock.pyx"
3091+
include "lru.pyx"
30913092

30923093
include "handles/handle.pyx"
30933094
include "handles/async_.pyx"

uvloop/lru.pyx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
cdef object _LRU_MARKER = object()
2+
3+
4+
@cython.final
5+
cdef class LruCache:
6+
7+
cdef:
8+
object _dict
9+
int _maxsize
10+
object _dict_move_to_end
11+
object _dict_get
12+
13+
# We use an OrderedDict for LRU implementation. Operations:
14+
#
15+
# * We use a simple `__setitem__` to push a new entry:
16+
# `entries[key] = new_entry`
17+
# That will push `new_entry` to the *end* of the entries dict.
18+
#
19+
# * When we have a cache hit, we call
20+
# `entries.move_to_end(key, last=True)`
21+
# to move the entry to the *end* of the entries dict.
22+
#
23+
# * When we need to remove entries to maintain `max_size`, we call
24+
# `entries.popitem(last=False)`
25+
# to remove an entry from the *beginning* of the entries dict.
26+
#
27+
# So new entries and hits are always promoted to the end of the
28+
# entries dict, whereas the unused one will group in the
29+
# beginning of it.
30+
31+
def __init__(self, *, maxsize):
32+
if maxsize <= 0:
33+
raise ValueError(
34+
f'maxsize is expected to be greater than 0, got {maxsize}')
35+
36+
self._dict = col_OrderedDict()
37+
self._dict_move_to_end = self._dict.move_to_end
38+
self._dict_get = self._dict.get
39+
self._maxsize = maxsize
40+
41+
cdef get(self, key, default):
42+
o = self._dict_get(key, _LRU_MARKER)
43+
if o is _LRU_MARKER:
44+
return default
45+
self._dict_move_to_end(key) # last=True
46+
return o
47+
48+
cdef inline needs_cleanup(self):
49+
return len(self._dict) > self._maxsize
50+
51+
cdef inline cleanup_one(self):
52+
k, _ = self._dict.popitem(last=False)
53+
return k
54+
55+
def __getitem__(self, key):
56+
o = self._dict[key]
57+
self._dict_move_to_end(key) # last=True
58+
return o
59+
60+
def __setitem__(self, key, o):
61+
if key in self._dict:
62+
self._dict[key] = o
63+
self._dict_move_to_end(key) # last=True
64+
else:
65+
self._dict[key] = o
66+
while self.needs_cleanup():
67+
self.cleanup_one()
68+
69+
def __delitem__(self, key):
70+
del self._dict[key]
71+
72+
def __contains__(self, key):
73+
return key in self._dict
74+
75+
def __len__(self):
76+
return len(self._dict)
77+
78+
def __iter__(self):
79+
return iter(self._dict)

0 commit comments

Comments
 (0)