fix: retry Block2 GETs on timeout, track server-negotiated block size

Ported from localthings fork: bounded per-block retransmission
(_BLOCK_MAX_ATTEMPTS/_BLOCK_ACK_TIMEOUT) instead of a single attempt
per block, SZX renegotiation tracking, and a bugfix moving
conn.send(datagram) inside the try that catches its own SSL errors.
This commit is contained in:
Marc Billow
2026-07-04 17:29:22 -05:00
parent 0fc65339b5
commit 58df1b242f
+39 -21
View File
@@ -48,6 +48,12 @@ _OCF_ROOT_CA = str(Path(__file__).parent / 'ocf_root_ca.pem')
# new resources and field semantics; otherwise quiet. # new resources and field semantics; otherwise quiet.
DEBUG_BRIDGE = os.environ.get('DEBUG_BRIDGE') == '1' DEBUG_BRIDGE = os.environ.get('DEBUG_BRIDGE') == '1'
# Per-block retransmission: send up to this many times before giving up.
# Each attempt waits at most _BLOCK_ACK_TIMEOUT seconds (capped by the
# overall deadline). Matches RFC 7252 CON retransmit behaviour.
_BLOCK_MAX_ATTEMPTS = 3
_BLOCK_ACK_TIMEOUT = 4.0
class DtlsCoapSession: class DtlsCoapSession:
"""Single sustained DTLS-CoAP session. """Single sustained DTLS-CoAP session.
@@ -252,8 +258,8 @@ class DtlsCoapSession:
with self._send_lock: with self._send_lock:
if self.conn is None: if self.conn is None:
raise ConnectionError("DTLS session closed") raise ConnectionError("DTLS session closed")
self.conn.send(datagram)
try: try:
self.conn.send(datagram)
while True: while True:
o = self.conn.bio_read(65535) o = self.conn.bio_read(65535)
if not o: if not o:
@@ -399,28 +405,37 @@ class DtlsCoapSession:
last_code = None last_code = None
last_opts = [] last_opts = []
deadline = time.time() + timeout deadline = time.time() + timeout
szx = BLOCK_SZX # server may negotiate down; track per-transfer
while True: while True:
ev = threading.Event() if num > 0:
self.pace()
container = {} container = {}
self._pending[tok] = (ev, container) for attempt in range(_BLOCK_MAX_ATTEMPTS):
try: ev = threading.Event()
mid = self._next_mid() container = {}
opts = [(URI_PATH, s.encode()) for s in path_segs] self._pending[tok] = (ev, container)
for q in query: try:
opts.append((URI_QUERY, q.encode())) mid = self._next_mid()
opts.append((ACCEPT, CF_CBOR)) opts = [(URI_PATH, s.encode()) for s in path_segs]
if num > 0: for q in query:
opts.append((BLOCK2, block_value(num, 0, BLOCK_SZX))) opts.append((URI_QUERY, q.encode()))
self._send_dgram( opts.append((ACCEPT, CF_CBOR))
build_coap(TYPE_CON, METHOD_GET, mid, tok, opts)) if num > 0:
wait = max(0.1, deadline - time.time()) opts.append((BLOCK2, block_value(num, 0, szx)))
if not ev.wait(wait): self._send_dgram(
raise TimeoutError( build_coap(TYPE_CON, METHOD_GET, mid, tok, opts))
f"GET /{'/'.join(path_segs)} block {num} timeout") per_wait = min(_BLOCK_ACK_TIMEOUT,
if 'err' in container: max(0.1, deadline - time.time()))
raise ConnectionError(container['err']) if ev.wait(per_wait):
finally: break # got a response
self._pending.pop(tok, None) remaining = deadline - time.time()
if remaining <= 0 or attempt == _BLOCK_MAX_ATTEMPTS - 1:
raise TimeoutError(
f"GET /{'/'.join(path_segs)} block {num} timeout")
finally:
self._pending.pop(tok, None)
if 'err' in container:
raise ConnectionError(container['err'])
code = container['code'] code = container['code']
payload = container['payload'] payload = container['payload']
@@ -437,6 +452,9 @@ class DtlsCoapSession:
if b2: if b2:
bv = int.from_bytes(b2[0], 'big') bv = int.from_bytes(b2[0], 'big')
more = (bv >> 3) & 1 more = (bv >> 3) & 1
server_szx = bv & 0x07
if server_szx != szx:
szx = server_szx
if not more: if not more:
break break
num += 1 num += 1