Skip to content

v1.2.0

Choose a tag to compare

@benoitc benoitc released this 21 Apr 09:12
· 128 commits to main since this release
0e239d1

Post-1.1.0 work split across three tracks: a client-side socket-backend
opt-in, a round of hot-path micro-optimisations on the send and
receive paths, and a migration fix for the default gen_udp client.

Added

  • Opt-in socket_backend => socket for client connections. Routes
    the client through quic_socket:open_for_send/2 so it picks up the
    OTP socket NIF on Linux with GSO available per-message via cmsg,
    instead of the gen_udp port driver. +18% download throughput on
    arm64 Linux docker (10 MB bench); upload is neutral. (#88, #91)
  • Client migration (quic:migrate/1) now works on the opt-in socket
    backend. Rebind closes the old OTP socket, stops its dedicated
    receiver process, opens a fresh one, and threads the new handle
    through the connection state. (#90)
  • quic_socket:start_client_receiver/2 / stop_client_receiver/1:
    dedicated receiver process for the socket-backend client path
    (the OTP socket NIF has no {active, N} mode). (#88)
  • quic_socket:set_socket/2 swaps the underlying socket handle
    inside a #socket_state{} while preserving batching configuration.
    Used by the migration rebind path. (#93)
  • Instrumentation counters ack_sent and retransmits on
    quic_connection:get_stats/1 and the throughput bench output
    (Phase 0a). (#77, #78)

Fixed

  • quic:migrate/1 on the default gen_udp client no longer drops
    post-migrate traffic. Rebinding previously left
    #state.socket_state pointing at the just-closed old socket; every
    send went through the dead handle and was silently dropped. Also
    flushes any pending batch to the old socket before rebind so
    pre-migrate packets reach the server under their original CID.
    (#93)
  • quic_dist: simultaneous-connect deadlock in the accept path.
    Two nodes dialling each other within a tight window wedged both
    net_kernel:connect_node/1 calls indefinitely. The old accept
    path ran the dist worker through a nine-hop handoff
    (register_pending / controller rendezvous in acceptor_loop) before
    reaching dist_util:mark_pending, so net_kernel's tie-breaker
    arbitration never ran in time. Collapsed to the TCP-dist shape:
    accept_connection/5 runs set_supervisor + start_timer +
    handshake_other_started inline. Docker 5-node regression now
    passes 5/5. (#106)
  • quic_dist: batch-yield path in input_handler_loop could lose
    or reorder buffered dist bytes when the mailbox had backlog.
    Yield now threads the buffer remnant through the normal return
    channel instead of piggybacking on the self-message. (#104)
  • quic_dist_user_stream_SUITE / accept_user_streams/2 doc:
    refreshed to match the auto-assign / direct
    {quic_dist_stream, _, {data, _, _}} delivery shape. (#105)
  • docker/dist: 3+ node cluster mesh formation. Each node now dials
    only higher-named peers and boots with -connect_all false, so
    global does not re-introduce cross-dials behind the explicit
    test topology. (#95, #106)
  • h3: preserve WebTransport and unknown SETTINGS identifiers in the
    peer settings map so extension-stream hooks can read them. (#96)
  • quic_socket: client migrate path opens the new socket before
    closing the old one, avoiding a window where the client has no
    valid send handle. (#97)
  • quic_socket: client_recv_loop exits cleanly on unexpected
    socket errors instead of spinning. (#98)
  • quic_socket: clear the pending batch buffer on flush error so
    stale frames do not get retried on the next flush. (#99)
  • quic:connect/4: reject the socket + {socket_backend, socket}
    option combination with a clear error instead of silently
    overriding one. (#100)
  • Client connection: treat receiver-process exit as a fatal error
    and close the connection, matching server behaviour. (#101)
  • Server: build a per-connection sender even when
    server_send_batching is false so the direct-send path uses the
    same quic_socket shape as the batched path. (#102, #103)

Performance

  • Fuse per-packet cwnd + pacing check into quic_cc:send_check/3
    (one BIF call and one record match instead of the previous four).
    (#79)
  • Hoist per-chunk lookups (stream_urgency, max_stream_data_per_packet,
    pre-computed stream-frame header prefix) out of the chunked send
    loop. (#80, #85)
  • ACK 1-RTT packets immediately on reorder (RFC 9002 §6.2) while
    keeping the decimation window for in-order traffic. (#81)
  • Fast-path single-stream-frame in contains_ack_eliciting_frames/1
    on the bulk-upload hot path. (#82)
  • Thread the updated socket_state back from do_socket_send via
    the return value, dropping the process-dictionary roundtrip. (#83)
  • Replace the crypto:exor/2 NIF call with inline Erlang XOR for
    the 1-4 byte header-protection mask. (#84)
  • Inline the ?QLOG_ENABLED check at packet/frame event call
    sites so the event-map is never built when qlog is off. (#86)
  • Coalesce the monotonic_time samples on the receive hot path
    (one BIF call per received datagram instead of three). (#87)
  • Flush the pending stream-data batch before emitting an ACK-only
    packet so it does not break GSO uniformity on the opt-in socket
    backend. +6.4% upload throughput on arm64 Linux docker. (#92)
  • Re-enable GSO on the opt-in socket-backend client: drop the
    socket-level UDP_SEGMENT setsockopt and rely on per-message cmsg
    via flush_gso/1. (#91)