|
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.
|