Project

General

Profile

proposal-issue-major-functional-2.txt

Sergey Ivanovskiy, 08/12/2026 02:56 AM

Download (10.7 KB)

 
1
================================================================================================
2
PROPOSAL - [MAJOR] functional - WebClientProtocol.startKeepAlivePing
3
Dead-peer deadline derived from pingPongInterval, so the configured idle tolerance is ignored
4
================================================================================================
5

    
6
REVIEW FINDING (abridged)
7
   "the dead-peer deadline is hard-coded at DATA_QUIET_MULTIPLIER * interval where interval =
8
   min(pingPongInterval, idleMs / PING_INTERVAL_DIVISOR), so whenever pingPongInterval < idleMs/3 the
9
   configured idle tolerance is ignored entirely and teardown is governed solely by
10
   client/web/pingPongInterval - silently converting that key from 'how often to ping' into 'how long
11
   may the peer be silent'. There is no independent directory key for the deadline and no way to
12
   disable the check. [...] hotel_gui (maxIdleTime=600000, pingPongInterval=10000) yields a 30 s
13
   server deadline against a configured 10-minute tolerance [...] counteract (maxIdleTime=3600000,
14
   pingPongInterval=30000) yields 90 s against a one-hour tolerance."
15

    
16
FILES
17
   src/com/goldencode/p2j/ui/client/driver/web/WebClientProtocol.java
18
   (optional part 2.3: ConfigItem.java, WebConfigurationConstants.java, GuiWebDriver.java,
19
    ChuiWebSimulator.java, GuiWebSocket.java, StorageMessagingWebSocket.java,
20
    WebClientBuilderOptions.java, WebClientSpawner.java)
21

    
22
------------------------------------------------------------------------------------------------
23
1. ROOT CAUSE
24
------------------------------------------------------------------------------------------------
25

    
26
WebClientProtocol.java:1664:
27

    
28
   final long dataIdleMs = interval * DATA_QUIET_MULTIPLIER;
29

    
30
The deadline is a multiple of the ping *cadence*. Cadence and tolerance are independent settings: how
31
often the server asks has nothing to do with how long silence should be forgiven. Because interval is
32
already clamped down to idleMs / 3, dataIdleMs can never exceed idleMs - and whenever pingPongInterval
33
is the smaller of the two (the common case) the configured tolerance vanishes from the calculation
34
entirely.
35

    
36
The result is a silent redefinition of an existing directory key. Administrators who set
37
pingPongInterval to get a livelier heartbeat now get a *shorter* death sentence, and the browser does
38
not agree with them either: p2j.socket.js still derives its own tolerance from maxIdleTime
39
(maxLostPings = max(trunc(maxIdleTime / (2 * pingPongInterval)), 2)), so the two ends of the same
40
heartbeat believe in different deadlines.
41

    
42
Shipped configurations in this workspace:
43

    
44
   hotel_gui/deploy/server/directory.xml   maxIdleTime=600000  pingPongInterval=10000  no socketTimeout
45
      server deadline 30 s   vs. configured 10 min   (browser believes ~5 min)
46
   counteract/deploy/server/directory.xml  maxIdleTime=3600000 pingPongInterval=30000
47
      server deadline 90 s   vs. configured 1 h
48

    
49
Neither has any way to opt out: startKeepAlivePing is called unconditionally from onConnect, and both
50
drivers coerce a configured 0 or negative pingPongInterval back to 30000.
51

    
52
------------------------------------------------------------------------------------------------
53
2. PROPOSED FIX
54
------------------------------------------------------------------------------------------------
55

    
56
2.1 Derive the deadline from the idle tolerance (recommended, minimal)
57
-----------------------------------------------------------------------
58

    
59
Replace WebClientProtocol.java:1663-1664 with:
60

    
61
   -  // the deadline must outlast the ping interval, which is what feeds it, not the idle timeout
62
   -  final long dataIdleMs = interval * DATA_QUIET_MULTIPLIER;
63
   +  // The deadline is the configured idle tolerance, not a multiple of the ping cadence. The
64
   +  // cadence says how often the peer is asked; the idle timeout says how long its silence is
65
   +  // acceptable, and before this heartbeat existed the idle timeout was exactly what bounded peer
66
   +  // silence - so honouring it here keeps client/web/pingPongInterval meaning "how often to ping"
67
   +  // and nothing more. The DATA_QUIET_MULTIPLIER floor keeps a couple of lost or slowly answered
68
   +  // pings from costing a healthy session where the idle tolerance is short or absent.
69
   +  final long dataIdleMs = Math.max(idleMs, interval * DATA_QUIET_MULTIPLIER);
70

    
71
and retarget the DATA_QUIET_MULTIPLIER javadoc from "the deadline" to "the floor of the deadline":
72

    
73
   /**
74
    * The multiplier applied to the ping interval to obtain the *floor* of the deadline by which the
75
    * peer must have answered. It is deliberately larger than 1, so that a couple of lost or slowly
76
    * answered pings do not cost a healthy session where no idle timeout is configured, or where the
77
    * configured one is shorter than a few heartbeats.
78
    */
79

    
80
Resulting behaviour:
81

    
82
   idleMs   pingPongInterval   interval   deadline (was -> now)
83
   -------  -----------------  ---------  ---------------------
84
   600000   10000              10000      30000  -> 600000     (hotel_gui: matches configuration)
85
   3600000  30000              30000      90000  -> 3600000    (counteract: matches configuration)
86
   90000    30000              30000      90000  -> 90000      (default: unchanged)
87
   10000    30000              3333       9999   -> 10000      (socketTimeout=10000: honoured)
88
   0        30000              30000      90000  -> 90000      (no idle timeout: floor applies)
89

    
90
This restores pre-r16675 semantics exactly (peer silence bounded by the configured idle tolerance)
91
while keeping the property the heartbeat was introduced for: the check still runs when there is no
92
idle timeout at all, which is where Jetty would never reap a half-open socket.
93

    
94
It also re-aligns the two ends of the heartbeat: the browser's maxLostPings is derived from
95
maxIdleTime, so both sides now scale with the same key.
96

    
97
2.2 Fix the javadoc claims that go with it
98
-------------------------------------------
99

    
100
The method javadoc currently asserts the interval "tracks client/web/pingPongInterval, so that one
101
setting governs the heartbeat cadence instead of two drifting apart", and that the idle timeout "is
102
only ever used to clamp the interval". After 2.1 the idle timeout also sets the deadline; say so, and
103
state the resulting worst-case teardown time (see proposal-issue-minor-security-1, which owns the
104
worst-case wording). Also fix the wrong key name in the same block
105
(proposal-issue-minor-functional-5).
106

    
107
2.3 Optional: an explicit key, including an off switch
108
------------------------------------------------------
109

    
110
2.1 removes the surprise but still offers no way to disable the check, which the finding flags. If an
111
off switch is wanted, add a dedicated key rather than overloading pingPongInterval further:
112

    
113
ConfigItem.java, next to PING_PONG_INTERVAL:
114

    
115
   /**
116
    * The maximum time in milliseconds the web client may stay silent before the server closes its
117
    * web socket. Unset (or non-positive) means "derive it from the idle tolerance"; see
118
    * WebClientProtocol.startKeepAlivePing.
119
    */
120
   public static final ConfigItem<Integer> HEARTBEAT_DEADLINE =
121
      new ConfigItem<>(Integer.class, "heartbeatDeadline", "client", "web", "heartbeatDeadline",
122
                       Type.WEB_CLIENT);
123

    
124
WebConfigurationConstants:
125

    
126
   /** The default heartbeat deadline; -1 means derive it from the web socket idle tolerance */
127
   int HEARTBEAT_DEADLINE = -1;
128

    
129
Plumbing mirrors pingPongInterval exactly, which r16675 already established:
130
GuiWebDriver.init / ChuiWebSimulator read it (keeping -1 rather than coercing, since -1 is the
131
documented "unset" convention of the neighbouring items) -> GuiWebSocket / StorageMessagingWebSocket
132
constructors -> WebClientProtocol constructor field; WebClientBuilderOptions.java:307-309 and
133
WebClientSpawner.java:638-644 must pass it through for spawned clients, or a spawned client will
134
silently fall back to the derived value.
135

    
136
In startKeepAlivePing:
137

    
138
   final long dataIdleMs = (heartbeatDeadline > 0)
139
      ? Math.max(heartbeatDeadline, interval * DATA_QUIET_MULTIPLIER)
140
      : Math.max(idleMs, interval * DATA_QUIET_MULTIPLIER);
141

    
142
For a hard off switch, treat an explicit 0 as "no dead-peer check" and skip the deadline branch
143
entirely (still sending the pings, which are what defeat Jetty's idle timeout). Document loudly that
144
this reinstates the half-open-socket linger the heartbeat exists to prevent.
145

    
146
Recommendation: land 2.1 + 2.2 now; treat 2.3 as a follow-up only if a site actually needs it. 2.1
147
alone removes every mismatch found in the shipped configurations, and it does so without adding a
148
directory key that has no schema validation behind it (dir_schema.xml has no client/web object
149
class - see proposal-issue-minor-functional-1).
150

    
151
------------------------------------------------------------------------------------------------
152
3. RELATIONSHIP TO OTHER PROPOSALS
153
------------------------------------------------------------------------------------------------
154

    
155
   proposal-issue-major-functional-1   the outbound grace; complementary - this fixes the deadline's
156
                                       magnitude, that one fixes what evidence can postpone it
157
   proposal-issue-minor-functional-1   clamp order for interval, which feeds the floor computed here
158
   proposal-issue-minor-functional-5   the wrong key name in the same javadoc block
159
   proposal-issue-minor-functional-6   ConfigItem.MAX_IDLE_TIME javadoc, which must be corrected in
160
                                       the same breath: after 2.1 the key does bound teardown again,
161
                                       for WebClientProtocol websockets
162

    
163
------------------------------------------------------------------------------------------------
164
4. RISK AND VERIFICATION
165
------------------------------------------------------------------------------------------------
166

    
167
Risk: a long configured maxIdleTime now means a long dead-peer detection window again - e.g. one hour
168
on counteract. That is the administrator's stated intent and the pre-r16675 behaviour, and the
169
watchdog (client-side, 120 s default) still covers the abandoned-client case. Sites wanting fast
170
reclamation should shorten maxIdleTime / socketTimeout, which is what those keys have always meant.
171

    
172
Verification
173
   1. Unit-level: extract the interval/deadline computation into a package-private static helper
174
      (long computeDeadline(long idleMs, long pingPongInterval)) and table-test the matrix in 2.1.
175
      There is currently no test coverage of any of this arithmetic.
176
   2. Run with hotel_gui/deploy/server/directory.xml: startup log must read
177
      "interval=10000, idle timeout=600000, heartbeat deadline=600000".
178
   3. Suspend the client host for 5 minutes with the counteract configuration: session survives
179
      (previously lost at ~90 s + watchdog).
180
   4. Kill the browser process outright with the default configuration: teardown still at 90 s.