Skip to content

Commit 052bb47

Browse files
ccie18643claude
andcommitted
feat(tcp): source TCP buffer defaults from tcp.rmem/wmem.default (autotune step 5)
Fifth phase of the Tier-3 buffer auto-tuning plan (docs/refactor/tcp_buffer_autotuning.md §2/§7/§10 step 5). Wire a TCP socket's unset receive-window seed and send-buffer floor to the tcp.rmem.default / tcp.wmem.default sysctls (Linux tcp_rmem[1] / tcp_wmem[1]), giving the step-1 .default knobs live consumers and making the TCP starting buffers operator-tunable. Deliberately conservative: rather than adopting Linux's small initial values (which would make auto-tuning immediately visible but change every unset-buffer connection's start point), this keeps PyTCP's conservative 65535 / 212992 defaults and only re-sources them. The shipped knob values equal the old constants, so there is ZERO behaviour change and no test churn; an operator who wants Linux-style small-start-plus-autotune sets 'sysctl tcp.wmem.default=16384' with no code change. Mechanism — a template-method seam on the base socket: - socket._default_sndbuf() / _default_rcvbuf() return the generic SOCKET__SO_{SND,RCV}BUF__DEFAULT (Linux net.core.{w,r}mem_default), the datagram default; _effective_sndbuf() / _effective_rcvbuf() now call through them. - TcpSocket overrides both to return tcp__constants.TCP__WMEM__DEFAULT / TCP__RMEM__DEFAULT via qualified module access (live sysctl value). The TcpSession rcv_wnd_max seed already reads _effective_rcvbuf(), so it picks up tcp.rmem.default automatically; datagram sockets keep the net.core stand-ins (matching Linux, where datagram and TCP defaults are distinct sysctls). Tests-first: test__tcp__session__buffer_defaults.py (5 cases — rcv/snd defaults keep 65535/212992, operator override of each raises the seed/floor, datagram sockets unaffected by the TCP knobs). RED pre-impl: the two operator-override cases fail (seed/floor hardcoded to the SOCKET__ constants); the three regression guards pass pre- and post-impl. Plan §2/§7/§10 step 5 marked done (revised to the conservative decision). Reference: RFC 9293 §3.8.6 (receive window management). Reference: RFC 9293 §3.9 (SEND — send-buffer flow control). Reference: Linux net.ipv4.tcp_rmem[1] / tcp_wmem[1]. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 098e02d commit 052bb47

4 files changed

Lines changed: 272 additions & 26 deletions

File tree

docs/refactor/tcp_buffer_autotuning.md

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -350,13 +350,17 @@ Original plan text:
350350
by the same ACK's `_wake_sndbuf_waiters()`, then re-reads the widened
351351
`_effective_sndbuf()`.
352352

353-
### S3 — pairing with the small initial default (see §2 / §7)
354-
355-
Only meaningful once the Tier-2 `tcp.wmem` triple lands and the unset
356-
send default drops to `tcp.wmem.default` (small). Until then S2 grows
357-
only above 208 KiB (near-inert). Land S1/S2 behind the Tier-2 default
358-
switch, or ship S with the default-switch in the same series so the
359-
behaviour is observable.
353+
### S3 — pairing with the initial default (see §2 / §7)
354+
355+
**Resolved conservatively (step 5).** The unset send floor is now sourced
356+
from `tcp.wmem.default` (via the `TcpSocket._default_sndbuf()` override),
357+
but the shipped value stays PyTCP's conservative 208 KiB rather than
358+
Linux's small 16 KiB. So S2 remains near-inert on the common path (it
359+
grows only once `2*cwnd*mss` exceeds 208 KiB) — a deliberate trade: no
360+
per-connection behaviour change, and an operator who wants Linux-style
361+
small-start-plus-autotune sets `tcp.wmem.default=16384` with no code
362+
change. Send auto-tuning is fully *functional*; whether it is *visible*
363+
on a given connection is now an operator knob.
360364

361365
## 6. Sysctl knobs (registered as part of this work)
362366

@@ -381,10 +385,16 @@ means **Track R/S subsumes that slice of Tier-2**. Keep them flat (not
381385

382386
## 7. Decisions to lock
383387

384-
- **Pair Track S with the small-default switch (§2).** Send auto-tuning
385-
against the 208 KiB static default is cosmetic. Ship S2 together with
386-
adopting `tcp.wmem.default` as the unset send bound, or the track
387-
delivers no observable parity.
388+
- **Default sourcing over default *value* (§2, revised at step 5).** The
389+
original decision was to pair Track S with adopting Linux's *small*
390+
send default so auto-tuning is immediately visible. The project instead
391+
chose to **keep PyTCP's conservative 208 KiB / 65535 defaults** and
392+
only re-source them from `tcp.wmem.default` / `tcp.rmem.default`
393+
(operator-tunable). Rationale: zero per-connection behaviour change and
394+
no test churn, while still giving the `.default` knobs live consumers —
395+
the Linux small-start behaviour is one `sysctl` away with no code.
396+
Send auto-tuning is therefore near-inert on the common path by design,
397+
not by omission.
388398
- **DRS is timestamps-gated for the first pass (R1).** The receiver RTT
389399
estimator uses the RFC 7323 TSecr echo. The no-timestamps
390400
`tcp_rcv_rtt_measure` fallback is a documented follow-on, not first-pass
@@ -537,10 +547,23 @@ CLAUDE.md "Linux as tiebreaker" precedence.
537547
`test__tcp__session__drs.py`. **DRS is functionally complete** (still
538548
inert on the common path until step 5 raises throughput enough to
539549
grow, but it fires whenever per-RTT throughput exceeds the window).
540-
5. **The small-default switch** (§2 / §7) — flip the unset `SO_SNDBUF`
541-
(and optionally `rcv_wnd_max`) default to the Tier-2 `.default`,
542-
making auto-tuning observable. Behaviour-changing → its own deliberate
543-
commit with the test-churn it implies.
550+
5. **The default-sourcing switch** (§2 / §7) — **DONE, but conservative
551+
by decision.** Rather than dropping to Linux's *small* initial values
552+
(which would have made auto-tuning immediately observable at the cost
553+
of changing every unset-buffer connection's start point), the project
554+
chose to **keep PyTCP's conservative 65535 / 212992 defaults** and
555+
only re-source them from the `tcp.rmem.default` / `tcp.wmem.default`
556+
sysctls. A `_default_rcvbuf()` / `_default_sndbuf()` template method
557+
on the base socket (generic `net.core.*_default`) is overridden by
558+
`TcpSocket` (`tcp_rmem[1]` / `tcp_wmem[1]`), so `_effective_rcvbuf()`
559+
/ `_effective_sndbuf()` — and thus the `rcv_wnd_max` seed and the TCP
560+
send floor — become operator-tunable with **zero behaviour change**
561+
(the shipped knob values equal the old constants). This gives the
562+
`.default` knobs live consumers and completes the Linux default
563+
sourcing; adopting the small values remains available as a pure
564+
sysctl override (`sysctl tcp.wmem.default=16384`) with no further
565+
code. Tests: `test__tcp__session__buffer_defaults.py`. **No test
566+
churn** — values unchanged.
544567
6. **R0 / no-TS fallback** — optional follow-ons, only if a consumer needs
545568
>8 MiB windows or DRS on non-timestamped connections.
546569

packages/pytcp/pytcp/runtime/socket/__init__.py

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,20 +1447,42 @@ def _multicast_send_suppressed(self, remote_ip_address: Ip4Address | Ip6Address,
14471447

14481448
return remote_ip_address.is_multicast and self._effective_ip_ttl(remote_ip_address) == 0
14491449

1450+
def _default_sndbuf(self) -> int:
1451+
"""
1452+
Get the unset-SO_SNDBUF send-buffer default. The base value is
1453+
the 'SOCKET__SO_SNDBUF__DEFAULT' stand-in for Linux
1454+
'net.core.wmem_default' — the datagram default. 'TcpSocket'
1455+
overrides this to the operator-tunable 'tcp.wmem.default'
1456+
(Linux 'tcp_wmem[1]').
1457+
"""
1458+
1459+
return SOCKET__SO_SNDBUF__DEFAULT
1460+
1461+
def _default_rcvbuf(self) -> int:
1462+
"""
1463+
Get the unset-SO_RCVBUF receive-buffer default. The base value
1464+
is the 'SOCKET__SO_RCVBUF__DEFAULT' stand-in for Linux
1465+
'net.core.rmem_default' — the datagram default. 'TcpSocket'
1466+
overrides this to the operator-tunable 'tcp.rmem.default'
1467+
(Linux 'tcp_rmem[1]').
1468+
"""
1469+
1470+
return SOCKET__SO_RCVBUF__DEFAULT
1471+
14501472
def _effective_sndbuf(self) -> int:
14511473
"""
14521474
Get the SO_SNDBUF send-buffer bound. An explicit application
14531475
value (SOCK_SNDBUF_LOCK) always wins and pins the bound. When
1454-
unset, the bound is the larger of the 'SOCKET__SO_SNDBUF__DEFAULT'
1455-
stand-in (Linux 'net.core.wmem_default') and the TCP
1456-
auto-tuning bound '_sndbuf_auto' (Tier-3 Track S; 0 for
1457-
datagram sockets, so their bound is the static default). PyTCP
1458-
does not apply Linux's 2x doubling of the requested value.
1476+
unset, the bound is the larger of '_default_sndbuf()' (the
1477+
per-flavour default — 'net.core.wmem_default' for datagram
1478+
sockets, 'tcp.wmem.default' for TCP) and the TCP auto-tuning
1479+
bound '_sndbuf_auto' (Tier-3 Track S; 0 for datagram sockets).
1480+
PyTCP does not apply Linux's 2x doubling of the requested value.
14591481
"""
14601482

14611483
if self._so_sndbuf is not None:
14621484
return self._so_sndbuf
1463-
return max(SOCKET__SO_SNDBUF__DEFAULT, self._sndbuf_auto)
1485+
return max(self._default_sndbuf(), self._sndbuf_auto)
14641486

14651487
def _grow_sndbuf_auto(self, target: int, /) -> None:
14661488
"""
@@ -1476,13 +1498,14 @@ def _grow_sndbuf_auto(self, target: int, /) -> None:
14761498
def _effective_rcvbuf(self) -> int:
14771499
"""
14781500
Get the SO_RCVBUF receive-buffer size: the value the
1479-
application set, else the 'SOCKET__SO_RCVBUF__DEFAULT'
1480-
stand-in for Linux 'net.core.rmem_default'. The TCP advertised
1481-
receive window and the datagram RX-drop cap derive from this;
1482-
PyTCP does not apply Linux's 2x doubling of the requested value.
1501+
application set, else '_default_rcvbuf()' (the per-flavour
1502+
default — 'net.core.rmem_default' for datagram sockets,
1503+
'tcp.rmem.default' for TCP). The TCP advertised receive window
1504+
and the datagram RX-drop cap derive from this; PyTCP does not
1505+
apply Linux's 2x doubling of the requested value.
14831506
"""
14841507

1485-
return self._so_rcvbuf if self._so_rcvbuf is not None else SOCKET__SO_RCVBUF__DEFAULT
1508+
return self._so_rcvbuf if self._so_rcvbuf is not None else self._default_rcvbuf()
14861509

14871510
def _charge_sndbuf(self, nbytes: int, /) -> None:
14881511
"""

packages/pytcp/pytcp/runtime/socket/tcp__socket.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from net_proto.lib.proto_enum import ProtoEnum
5151
from pytcp import stack
5252
from pytcp.lib.logger import log
53+
from pytcp.protocols.tcp import tcp__constants
5354
from pytcp.protocols.tcp.session import TcpSession
5455
from pytcp.protocols.tcp.tcp__enums import CcMode, FsmState
5556
from pytcp.protocols.tcp.tcp__errors import TcpSessionError
@@ -286,6 +287,29 @@ def parent_socket(self) -> TcpSocket | None:
286287

287288
return self._parent_socket
288289

290+
@override
291+
def _default_sndbuf(self) -> int:
292+
"""
293+
Get the unset-SO_SNDBUF send-buffer default for TCP: the
294+
operator-tunable 'tcp.wmem.default' (Linux 'tcp_wmem[1]'),
295+
distinct from the datagram 'net.core.wmem_default'. Read via
296+
qualified module access so a runtime sysctl override is picked
297+
up live.
298+
"""
299+
300+
return tcp__constants.TCP__WMEM__DEFAULT
301+
302+
@override
303+
def _default_rcvbuf(self) -> int:
304+
"""
305+
Get the unset-SO_RCVBUF receive-buffer default for TCP: the
306+
operator-tunable 'tcp.rmem.default' (Linux 'tcp_rmem[1]'),
307+
distinct from the datagram 'net.core.rmem_default'. Seeds the
308+
advertised-window cap ('rcv_wnd_max') at session construction.
309+
"""
310+
311+
return tcp__constants.TCP__RMEM__DEFAULT
312+
289313
def status(self) -> TcpStatus:
290314
"""
291315
Return a read-only snapshot of the connection's
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
################################################################################
2+
## ##
3+
## PyTCP - Python TCP/IP stack ##
4+
## Copyright (C) 2020-present Sebastian Majewski ##
5+
## ##
6+
## This program is free software: you can redistribute it and/or modify ##
7+
## it under the terms of the GNU General Public License as published by ##
8+
## the Free Software Foundation, either version 3 of the License, or ##
9+
## (at your option) any later version. ##
10+
## ##
11+
## This program is distributed in the hope that it will be useful, ##
12+
## but WITHOUT ANY WARRANTY; without even the implied warranty of ##
13+
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ##
14+
## GNU General Public License for more details. ##
15+
## ##
16+
## You should have received a copy of the GNU General Public License ##
17+
## along with this program. If not, see <https://www.gnu.org/licenses/>. ##
18+
## ##
19+
## Author's email: ccie18643@gmail.com ##
20+
## Github repository: https://github.com/ccie18643/PyTCP ##
21+
## ##
22+
################################################################################
23+
24+
25+
# pylint: disable=protected-access
26+
# pyright: reportPrivateUsage=false
27+
28+
29+
"""
30+
Integration tests for the TCP buffer-default sourcing (Tier-3 Track R/S,
31+
step 5). A TCP socket's unset receive-window seed and send-buffer floor
32+
come from the 'tcp.rmem.default' / 'tcp.wmem.default' sysctls (Linux
33+
'tcp_rmem[1]' / 'tcp_wmem[1]'), so they are operator-tunable — while a
34+
datagram socket keeps the generic 'net.core.*_default' stand-ins. The
35+
shipped defaults keep PyTCP's conservative 65535 / 212992 values, so this
36+
changes no behaviour; it only makes the starting points configurable and
37+
gives the registered '.default' knobs live consumers.
38+
39+
pytcp/tests/integration/protocols/tcp/test__tcp__session__buffer_defaults.py
40+
41+
ver 3.0.8
42+
"""
43+
44+
from typing import override
45+
46+
from pytcp.runtime.socket import (
47+
SOCKET__SO_RCVBUF__DEFAULT,
48+
SOCKET__SO_SNDBUF__DEFAULT,
49+
AddressFamily,
50+
)
51+
from pytcp.runtime.socket.udp__socket import UdpSocket
52+
from pytcp.stack import sysctl as sysctl_module
53+
from pytcp.tests.lib.tcp_testcase import TcpTestCase
54+
55+
56+
class TestTcpSessionBufferDefaults(TcpTestCase):
57+
"""
58+
The TCP buffer-default sourcing tests.
59+
"""
60+
61+
@override
62+
def tearDown(self) -> None:
63+
"""
64+
Restore any sysctl overridden by an operator-override test so
65+
the mutation cannot leak into a sibling test.
66+
"""
67+
68+
sysctl_module.reset_to_defaults()
69+
super().tearDown()
70+
71+
def test__rcv_default__seeds_rcv_wnd_max_from_tcp_rmem_default(self) -> None:
72+
"""
73+
Ensure a fresh session with no SO_RCVBUF seeds its advertised
74+
receive-window cap from 'tcp.rmem.default' — the shipped value
75+
keeps the conservative 65535.
76+
77+
Reference: RFC 9293 §3.8.6 (receive window management).
78+
Reference: Linux net.ipv4.tcp_rmem[1] (default receive window).
79+
"""
80+
81+
session = self._make_active_session(iss=1000)
82+
self.assertEqual(
83+
session.rcv_wnd_max,
84+
sysctl_module.get("tcp.rmem.default"),
85+
msg="rcv_wnd_max must seed from tcp.rmem.default.",
86+
)
87+
self.assertEqual(
88+
session.rcv_wnd_max,
89+
65535,
90+
msg="The shipped tcp.rmem.default must keep the conservative 65535.",
91+
)
92+
93+
def test__rcv_default__operator_override_raises_seed(self) -> None:
94+
"""
95+
Ensure raising 'tcp.rmem.default' raises the advertised-window
96+
seed of a subsequently-opened session, so the starting window
97+
is operator-tunable.
98+
99+
Reference: RFC 9293 §3.8.6 (receive window management).
100+
Reference: Linux net.ipv4.tcp_rmem[1] (operator-tunable default).
101+
"""
102+
103+
sysctl_module.set("tcp.rmem.default", 131072)
104+
session = self._make_active_session(iss=1000)
105+
self.assertEqual(
106+
session.rcv_wnd_max,
107+
131072,
108+
msg="A raised tcp.rmem.default must seed a larger rcv_wnd_max.",
109+
)
110+
111+
def test__snd_default__effective_sndbuf_from_tcp_wmem_default(self) -> None:
112+
"""
113+
Ensure a fresh TCP socket with no SO_SNDBUF reports a
114+
send-buffer floor from 'tcp.wmem.default' — the shipped value
115+
keeps the conservative 212992.
116+
117+
Reference: RFC 9293 §3.9 (SEND — send-buffer flow control).
118+
Reference: Linux net.ipv4.tcp_wmem[1] (default send buffer).
119+
"""
120+
121+
session = self._make_active_session(iss=1000)
122+
self.assertEqual(
123+
session._socket._effective_sndbuf(),
124+
sysctl_module.get("tcp.wmem.default"),
125+
msg="The TCP send-buffer floor must come from tcp.wmem.default.",
126+
)
127+
self.assertEqual(
128+
session._socket._effective_sndbuf(),
129+
212992,
130+
msg="The shipped tcp.wmem.default must keep the conservative 212992.",
131+
)
132+
133+
def test__snd_default__operator_override_raises_floor(self) -> None:
134+
"""
135+
Ensure raising 'tcp.wmem.default' raises the TCP send-buffer
136+
floor, so the starting send bound is operator-tunable.
137+
138+
Reference: RFC 9293 §3.9 (SEND — send-buffer flow control).
139+
Reference: Linux net.ipv4.tcp_wmem[1] (operator-tunable default).
140+
"""
141+
142+
session = self._make_active_session(iss=1000)
143+
sysctl_module.set("tcp.wmem.default", 300000)
144+
self.assertEqual(
145+
session._socket._effective_sndbuf(),
146+
300000,
147+
msg="A raised tcp.wmem.default must raise the TCP send-buffer floor.",
148+
)
149+
150+
def test__datagram_default__unaffected_by_tcp_knob(self) -> None:
151+
"""
152+
Ensure a datagram socket sources its buffer defaults from the
153+
generic 'net.core.*_default' stand-ins, not the TCP-specific
154+
knobs — overriding 'tcp.rmem.default' / 'tcp.wmem.default' must
155+
not move a UDP socket's effective buffers.
156+
157+
Reference: PyTCP test infrastructure (no RFC clause).
158+
Reference: Linux net.core.rmem_default / wmem_default (datagram
159+
default, distinct from tcp_rmem / tcp_wmem).
160+
"""
161+
162+
sysctl_module.set("tcp.rmem.default", 131072)
163+
sysctl_module.set("tcp.wmem.default", 300000)
164+
sock = UdpSocket(family=AddressFamily.INET4)
165+
self.addCleanup(sock.close)
166+
167+
self.assertEqual(
168+
sock._effective_rcvbuf(),
169+
SOCKET__SO_RCVBUF__DEFAULT,
170+
msg="A datagram socket's rcvbuf default must be the generic net.core stand-in.",
171+
)
172+
self.assertEqual(
173+
sock._effective_sndbuf(),
174+
SOCKET__SO_SNDBUF__DEFAULT,
175+
msg="A datagram socket's sndbuf default must be the generic net.core stand-in.",
176+
)

0 commit comments

Comments
 (0)