|
1
|
================================================================================================
|
|
2
|
PROPOSAL - [MAJOR] functional - WebClientProtocol.startKeepAlivePing
|
|
3
|
Inbound-only deadline: outbound activity no longer buys any tolerance
|
|
4
|
================================================================================================
|
|
5
|
|
|
6
|
REVIEW FINDING (abridged)
|
|
7
|
"because the echo is emitted from the browser's onmessage handler, the deadline measures the
|
|
8
|
client's inbound message-processing lag rather than its liveness - a page whose event loop is
|
|
9
|
blocked [...] is declared 'gone or its page is no longer running' and closed. Outbound activity
|
|
10
|
no longer buys any tolerance [...] Concrete blocker reachable from converted 4GL today:
|
|
11
|
HTML-BROWSER:PRINT() [...] blocks the page task queue [...] a dialog left open past deadline +
|
|
12
|
watchdog (~3.5 min at defaults) makes WatchdogTimer System.exit(-1) the client JVM and lose the
|
|
13
|
FWD session and its record locks."
|
|
14
|
|
|
15
|
FILES
|
|
16
|
src/com/goldencode/p2j/ui/client/driver/web/WebClientProtocol.java
|
|
17
|
src/com/goldencode/p2j/ui/client/driver/web/PushMessagesWorker.java
|
|
18
|
|
|
19
|
------------------------------------------------------------------------------------------------
|
|
20
|
1. ROOT CAUSE
|
|
21
|
------------------------------------------------------------------------------------------------
|
|
22
|
|
|
23
|
Before r16675 a session survived on *either* direction of traffic. Jetty's EndPoint idle timeout is
|
|
24
|
reset by every successful write (SocketChannelEndPoint.flush() -> notIdle()), so a client the server
|
|
25
|
was still pushing output to was never reaped, no matter how long it had been silent.
|
|
26
|
|
|
27
|
r16675 replaces that with a strictly inbound deadline: lastDataNanos is stamped only from onMessage
|
|
28
|
(WebClientProtocol.java:463 and :478) and the tick closes the session when
|
|
29
|
(now - lastDataNanos) > dataIdleMs. The only thing that can refresh it is the page's own echo of
|
|
30
|
MSG_SERVER_PING, which is dispatched from the JS onmessage handler - i.e. from the page task queue.
|
|
31
|
Any page whose task queue is blocked therefore looks dead, even though its host, socket and JVM are
|
|
32
|
all healthy and the server's own writes are still completing normally.
|
|
33
|
|
|
34
|
The queue-depth grace does not cover it: PushMessagesWorker.sendMessage() polls the message off the
|
|
35
|
deque *before* writing (PushMessagesWorker.java:304), so getQueueSize() reads 0 while the write is
|
|
36
|
in flight, stuckTicks never engages, and teardown happens at the bare deadline.
|
|
37
|
|
|
38
|
------------------------------------------------------------------------------------------------
|
|
39
|
2. PROPOSED FIX
|
|
40
|
------------------------------------------------------------------------------------------------
|
|
41
|
|
|
42
|
Two changes, neither of which reintroduces the half-open-socket hole the heartbeat exists to close:
|
|
43
|
|
|
44
|
(a) [this proposal] Let *outbound progress* postpone the deadline again, but as bounded evidence
|
|
45
|
rather than as a full reset. Measure progress as "writes are still completing", which is
|
|
46
|
exactly the signal Jetty's notIdle() used to give us, and which stops dead on a half-open
|
|
47
|
socket (the write wedges and never completes).
|
|
48
|
|
|
49
|
(b) [proposal-issue-major-functional-2] Derive the deadline from the configured idle tolerance
|
|
50
|
instead of from the ping cadence. On the shipped configurations in this workspace that alone
|
|
51
|
moves the print-dialog deadline from 90 s to 10 min (hotel_gui) or 1 h (counteract).
|
|
52
|
|
|
53
|
Both are needed. (b) makes the deadline honest about what the administrator configured; (a) makes a
|
|
54
|
busy-but-live peer survive it the way it did before this revision.
|
|
55
|
|
|
56
|
2.1 PushMessagesWorker: expose completed-write progress
|
|
57
|
--------------------------------------------------------
|
|
58
|
|
|
59
|
Add a monotonic counter of successfully transmitted messages. It is the outbound counterpart of
|
|
60
|
lastDataNanos and, unlike getQueueSize(), it is not blind to an in-flight write.
|
|
61
|
|
|
62
|
/** Number of messages successfully handed to the socket, used as outbound liveness evidence. */
|
|
63
|
private volatile long transmitted;
|
|
64
|
|
|
65
|
/**
|
|
66
|
* The number of messages successfully written to the socket since this worker started.
|
|
67
|
* <p>
|
|
68
|
* Used by the keep-alive timer as outbound evidence: a write completes when the frame has been
|
|
69
|
* flushed to the peer's socket, which is the same signal that used to reset Jetty's idle
|
|
70
|
* timeout, and it stops advancing on a half-open socket. Unlike {@link #getQueueSize} it is not
|
|
71
|
* blind to a message which has already been polled off the queue but is still in flight.
|
|
72
|
*
|
|
73
|
* @return The monotonically increasing count of transmitted messages.
|
|
74
|
*/
|
|
75
|
public long getTransmittedCount()
|
|
76
|
{
|
|
77
|
return transmitted;
|
|
78
|
}
|
|
79
|
|
|
80
|
and in sendMessage(Session), immediately after the existing failure check
|
|
81
|
(PushMessagesWorker.java:393-397):
|
|
82
|
|
|
83
|
if (sendMessageException[0] != null)
|
|
84
|
{
|
|
85
|
msgSent = false;
|
|
86
|
LOG.log(Level.WARNING, "Send message failed!", sendMessageException[0]);
|
|
87
|
}
|
|
88
|
+ else if (message != null)
|
|
89
|
+ {
|
|
90
|
+ // the frame reached the peer's socket; this is the outbound liveness evidence the
|
|
91
|
+ // keep-alive timer uses, see getTransmittedCount
|
|
92
|
+ transmitted++;
|
|
93
|
+ }
|
|
94
|
|
|
95
|
The increment is single-writer (only the pushworker thread executes sendMessage), so a plain
|
|
96
|
volatile long is sufficient - no atomic needed.
|
|
97
|
|
|
98
|
2.2 WebClientProtocol: grant a grace while writes keep completing
|
|
99
|
------------------------------------------------------------------
|
|
100
|
|
|
101
|
Replace the queue-depth heuristic in the timer task with a test of that counter. The composed final
|
|
102
|
form of the tick is listed in proposal-issue-major-performance-1 (which owns the removal of the
|
|
103
|
queue-depth logic); the contribution of *this* proposal is the "progressing" branch:
|
|
104
|
|
|
105
|
+ /** Transmitted count seen at the previous tick, used to detect completed outbound writes. */
|
|
106
|
+ private long lastTransmitted;
|
|
107
|
...
|
|
108
|
+ PushMessagesWorker worker = pushWorker;
|
|
109
|
+ long transmitted = (worker != null) ? worker.getTransmittedCount() : 0L;
|
|
110
|
+ boolean progressing = transmitted != lastTransmitted;
|
|
111
|
+ lastTransmitted = transmitted;
|
|
112
|
+
|
|
113
|
+ long quietMs = (System.nanoTime() - lastDataNanos) / 1_000_000L;
|
|
114
|
+
|
|
115
|
+ // A peer that is still taking delivery is alive at the socket level even if its page
|
|
116
|
+ // is not answering: its event loop may be blocked (a native print dialog from
|
|
117
|
+ // HTML-BROWSER:PRINT, a modal file chooser) or it may simply be behind on a burst.
|
|
118
|
+ // That is exactly the tolerance Jetty's idle timeout used to grant through notIdle(),
|
|
119
|
+ // so it is granted here too - but bounded, because "the socket accepts bytes" is not
|
|
120
|
+ // "the page runs code". See MAX_BACKLOG_GRACE_MULTIPLIER.
|
|
121
|
+ if (quietMs > dataIdleMs && (!progressing || quietMs > graceLimitMs))
|
|
122
|
+ {
|
|
123
|
+ LOG.severe(...);
|
|
124
|
+ session.close(SESSION_HEARTBEAT_LOST, "client heartbeat stopped", Callback.NOOP);
|
|
125
|
+ return;
|
|
126
|
+ }
|
|
127
|
|
|
128
|
Note the counter is sampled on *every* tick, not only when the deadline is exceeded; sampling only
|
|
129
|
inside the deadline branch would compare against a stale value from an arbitrarily long time ago and
|
|
130
|
make "progressing" trivially true.
|
|
131
|
|
|
132
|
2.3 Why the half-open case is still detected
|
|
133
|
---------------------------------------------
|
|
134
|
|
|
135
|
On the VPN-drop / laptop-suspend case the javadoc names, the socket send buffer stops draining and
|
|
136
|
the Jetty write callback never completes, so getTransmittedCount() freezes. The very first tick past
|
|
137
|
dataIdleMs then sees progressing == false and closes the session - the same behaviour as the code
|
|
138
|
being reviewed. The grace only ever applies while the kernel is still acknowledging our frames, and
|
|
139
|
even then it is capped (proposal-issue-minor-security-1).
|
|
140
|
|
|
141
|
------------------------------------------------------------------------------------------------
|
|
142
|
3. RELATIONSHIP TO OTHER PROPOSALS
|
|
143
|
------------------------------------------------------------------------------------------------
|
|
144
|
|
|
145
|
proposal-issue-major-functional-2 deadline from the idle tolerance, not the ping cadence
|
|
146
|
(prerequisite for a usable deadline on shipped configs)
|
|
147
|
proposal-issue-major-performance-1 removes the queue-depth heuristic this replaces; owns the
|
|
148
|
composed tick body and the priority push of the ping
|
|
149
|
proposal-issue-minor-security-1 supplies graceLimitMs, the absolute cap on the grace granted
|
|
150
|
here, and the worst-case-teardown javadoc
|
|
151
|
proposal-issue-minor-functional-3 supplies SESSION_HEARTBEAT_LOST used in the close above
|
|
152
|
|
|
153
|
Applying 2.1 + 2.2 without proposal-issue-minor-security-1 would leave the grace unbounded; do not
|
|
154
|
land them separately.
|
|
155
|
|
|
156
|
------------------------------------------------------------------------------------------------
|
|
157
|
4. RISK AND VERIFICATION
|
|
158
|
------------------------------------------------------------------------------------------------
|
|
159
|
|
|
160
|
Risk: low. getTransmittedCount() is additive; the tick only ever gains a reason *not* to close.
|
|
161
|
Worst case a dead peer whose kernel still accepts small frames is reaped at graceLimitMs instead of
|
|
162
|
dataIdleMs - bounded, logged, and still far short of the TCP retransmission window this timer was
|
|
163
|
introduced to avoid.
|
|
164
|
|
|
165
|
Verification
|
|
166
|
1. HTML-BROWSER:PRINT() from a converted procedure in the Web GUI; leave the native print dialog
|
|
167
|
open for >3x pingPongInterval. Expected: session stays open, the keep-alive logs the grace at
|
|
168
|
FINE, no WatchdogTimer System.exit(-1). Reproduces the reported blocker directly.
|
|
169
|
2. Same test with the browser process SIGSTOPped instead (page blocked *and* socket unread):
|
|
170
|
writes wedge, teardown occurs at dataIdleMs, i.e. dead-peer detection is unchanged.
|
|
171
|
3. Drop the network at the OS level (VPN down / iptables DROP): teardown at dataIdleMs.
|
|
172
|
4. Confirm the FINE grace log and the SEVERE teardown log both report quietMs, dataIdleMs and
|
|
173
|
graceLimitMs, so a support engineer can tell the two apart.
|