Bug #11645
improve FWD Web Client ping-pong and timeouts
100%
History
#2 Updated by Constantin Asofiei 22 days ago
- Serban's findings at #11344-8 - the browser can throttle simple
setInterval, thus ping-pong may not be performed.
The old value of 30000ms was dangerous because of this: https://medium.com/@adithyaviswam/overcoming-browser-throttling-of-setinterval-executions-45387853a826
- we need to move the ping-pong to the FWD Java client side, so it does not rely on javascript to send it
- we need to move the other
setIntervalusage (like for reconnect) to Worker threads
#3 Updated by Teodor Gorghe 15 days ago
- Status changed from New to WIP
- Assignee set to Teodor Gorghe
#4 Updated by Teodor Gorghe 13 days ago
- Moved websocket ping-pong timer to server. Fixed additional bugs related with setInterval.
- As I understand about Workers setInterval from https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API, it just separates the work from main thread to a background thread. The main limitation is that it can't access DOM outside main javascript thread.
- The only scenario left which was feasable is the
pingPongWatcher, which has been removed on 11645a due to the logic move on the FWD client. - Other usages of
setIntervallikep2j.sso_reauth.js,p2j.perf_tests.js, etc., has been improved to reduce browser throttling (also some bug fixes onp2j.sso_reauth.js, for example, the case when a auth token expires).
#5 Updated by Teodor Gorghe 13 days ago
Also, I have analyzed the case when these timers overlap and I don't see an issue when this may occur. The alternative was to use setTimeout in a loop to avoid callback overlapping, but it doesn't seem the case.
I have also analyzed the case when the connection gets torn down and as I can see in the PushMessagesWorker, if FWD client fails to send a message, puts it back in front to the message queue. Same instance of PushMessagesWorker is used for the restored websocket connection, so, it seems that nothing is lost.
On the other hand, if the browser client has something to sent, but in the meanwhile, the connection gets dropped, that message is lost.
#6 Updated by Teodor Gorghe 13 days ago
- % Done changed from 0 to 100
#7 Updated by Sergey Ivanovskiy 13 days ago
Have you tested the following scenario: the Java and JS web clients are running on different hosts, and the JS client loses connection due to local network issues (faults, interface changes, switching to Wi-Fi)? The JS client should reconnect to the Java client if the issue clears up within a known time interval.
#8 Updated by Teodor Gorghe 13 days ago
On different hosts no, but I have tested the case when the browser tab enters in sleep and when the actual websocket connection gets torned down (chrome://discards freeze button).
I shall do the test using a remote host (for example, port forwarding the connection through devsrv01 local interface and loop it back using ssh port forwarding). This achieves ~500 ms ping.
#9 Updated by Sergey Ivanovskiy 13 days ago
It makes sense to log in to the web client on the VM in order to simulate local network issues. The reconnect dialog should appear, and the connection should be restored once the simulated network issues are resolved.
#10 Updated by Teodor Gorghe 1 day ago
Rebased 11645a to trunk rev 16695. Some work was done to integrate 11630a changes.
Also, I have committed the revision 16697:- Fixes after rebase. Replaced websocket protocol stream implementation with a chunked approach because we cannot interleave ping messages inside fragmented messages (RFC6455 5.4 page 35).
#11 Updated by Teodor Gorghe 1 day ago
- Status changed from WIP to Review
- reviewer Sergey Ivanovskiy added
Sergey, can you review the changes?
I have done the testing with the ping (checked network interaction in the dev tools with some parameters which allows to monitor the behavior easily), including connection restoration.
#12 Updated by Teodor Gorghe 1 day ago
Also, Constantin, I haven't documented here.
In RFC6455, there is already a ping-pong protocol specification, which reserves some special control frames for ping-pong implementation on protocol level.
- already implemented in Jetty.
- it is handled by the network stack. Doesn't require the javascript to be alive, so a browser tab can be in freeze.
- javascript side doesn't know when a ping message arrives, since this is handled exclusively by network stack. We need this ping-pong system to whatever check if a connection is alive, if not, we need to restore it.
- a proxy might keep the connection alive between server (FWD client) and proxy, but the other end might be closed.
This is the reason why I have kept the ping-pong own implementation, but I have switched the roles: FWD client now send the custom PING message and the browser (javascript application) responds with custom PONG.
#13 Updated by Sergey Ivanovskiy 1 day ago
Teodor Gorghe wrote:
Sergey, can you review the changes?
I have done the testing with the ping (checked network interaction in the dev tools with some parameters which allows to monitor the behavior easily), including connection restoration.
Yes. I can review.
#14 Updated by Sergey Ivanovskiy about 23 hours ago
Teodor, please run fwd-code-review for your changes. There are suspected issues:
MAJOR - functional WebClientProtocol.startKeepAlivePing: the echo comes from the browser's onmessage, so the deadline measures processing lag, not liveness, and outbound writes no longer buy tolerance (they used to reset Jetty's endpoint idle timeout via notIdle()). Deadline is min(3 × pingPongInterval, idleMs) — 90 s at defaults however large socketTimeout is. Reachable from 4GL today: HTML-BROWSER:PRINT() blocks the only thread that can answer, and past deadline + watchdog (~3.5 min) WatchdogTimer exits the client JVM, losing the session and its record locks. - performance same method: draining compares against a single previous sample, so a flat-or-growing queue is never draining, and the fall-through ping is appended to the tail of that same FIFO. On web CHUI, MSG_DRAW is one-way and this revision removed the client's outbound ping, so an idle-input report stream has zero inbound traffic → teardown on tick 6 (~180 s) of a perfectly-receiving peer. Sharper variant: one large in-flight message samples as queued == 0 → teardown at ~90 s. - functional same method: the deadline is hard-coded to pingPongInterval, silently redefining that key from "ping cadence" to "tolerated silence", with no separate knob and no way to disable. Confirmed against shipped configs — hotel_gui (600 s tolerance → 30 s deadline, while the browser still thinks it has ~5 min) and counteract (1 h → 90 s). - functional WebPageHandler.WebPageKeysProvider: pingPongInterval reaches the page unclamped while both Java readers now sanitise <= 0. With 0 (or -1, the convention of every neighbouring key) the page derives 100 ms and maxLostPings=2 → permanent reconnect loop ~300 ms after each connect. No schema validation anywhere. A regression: pre-change the same value was harmless. MINOR — grace path degrades liveness to "socket accepts bytes" and is self-suppressing (~240 s worst case vs the documented 90 s) _security_; MIN_PING_INTERVAL floor applied after the idle clamp, breaking the stated invariant; the keystroke-swallowing fix is incomplete and the disposal itself re-enables swallowing in the one case it changes; StatusCode.SHUTDOWN makes every reap log as "browser tab/window closed" at SEVERE, twice; pushWorker read off-lock without volatile; javadoc names the non-existent client/web/webSocketTimeout; MAX_IDLE_TIME javadoc overgeneralises (/api, network-test sockets still use it as a plain idle timeout); 2 style nits (missing blank line, 109-vs-110 separator). Two adjudications worth your attention: The headline finding was wrong. Two reviewers independently "verified against Jetty bytecode" that session.close(SHUTDOWN) half-closes to OSHUT and never delivers onClose, so no cleanup runs. I checked the bytecode myself: CloseStatus.isOrdinary covers only 1000, 1005 and ≥3000, so 1001 is abnormal — onOutgoingFrame sets CLOSED, returns true, and sendFrame → closeConnection → abort() → onClose. Rejected, along with the "locks linger 5 minutes / forever" claims built on it. Out of scope but material: while refuting the SSO-grace finding, a reviewer traced SsoTokenManager.forceLogout switching into the target session's context and then calling killSession on it — which trips the self-kill guard and throws into a silent catch. If that holds, the server-side SSO force-logout is dead code (and this revision's client-side teardown is genuinely load-bearing, vindicating its javadoc). It's in unchanged files, so I left it out of the report; it deserves its own issue and a second pass, as I haven't verified it independently.
I will review 11654a tomorrow.
#15 Updated by Sergey Ivanovskiy about 13 hours ago
The first major is a real issue. I used again Claude Code:
REVIEW FINDING (abridged) "because the echo is emitted from the browser's onmessage handler, the deadline measures the client's inbound message-processing lag rather than its liveness - a page whose event loop is blocked [...] is declared 'gone or its page is no longer running' and closed. Outbound activity no longer buys any tolerance [...] Concrete blocker reachable from converted 4GL today: HTML-BROWSER:PRINT() [...] blocks the page task queue [...] a dialog left open past deadline + watchdog (~3.5 min at defaults) makes WatchdogTimer System.exit(-1) the client JVM and lose the FWD session and its record locks." FILES src/com/goldencode/p2j/ui/client/driver/web/WebClientProtocol.java src/com/goldencode/p2j/ui/client/driver/web/PushMessagesWorker.java ------------------------------------------------------------------------------------------------ 1. ROOT CAUSE ------------------------------------------------------------------------------------------------ Before r16675 a session survived on *either* direction of traffic. Jetty's EndPoint idle timeout is reset by every successful write (SocketChannelEndPoint.flush() -> notIdle()), so a client the server was still pushing output to was never reaped, no matter how long it had been silent. r16675 replaces that with a strictly inbound deadline: lastDataNanos is stamped only from onMessage (WebClientProtocol.java:463 and :478) and the tick closes the session when (now - lastDataNanos) > dataIdleMs. The only thing that can refresh it is the page's own echo of MSG_SERVER_PING, which is dispatched from the JS onmessage handler - i.e. from the page task queue. Any page whose task queue is blocked therefore looks dead, even though its host, socket and JVM are all healthy and the server's own writes are still completing normally. The queue-depth grace does not cover it: PushMessagesWorker.sendMessage() polls the message off the deque *before* writing (PushMessagesWorker.java:304), so getQueueSize() reads 0 while the write is in flight, stuckTicks never engages, and teardown happens at the bare deadline.
#16 Updated by Teodor Gorghe about 13 hours ago
I will investigate!
#17 Updated by Teodor Gorghe about 13 hours ago
Sergey, this is real and the part is that this is preexisting in trunk, because the browser ping-pong control timer was blocked by this browser print dialog.
I am finding a solution.
#18 Updated by Sergey Ivanovskiy about 13 hours ago
- File proposal-issue-major-functional-3.txt
added - File proposal-issue-major-performance-1.txt
added - File proposal-issue-minor-functional-1.txt
added - File proposal-issue-minor-functional-2.txt
added - File proposal-issue-minor-functional-3.txt
added - File proposal-issue-minor-functional-4.txt
added - File proposal-issue-minor-functional-6.txt
added - File proposal-issue-minor-functional-5.txt
added - File proposal-issue-major-functional-1.txt
added - File proposal-issue-major-functional-2.txt
added
Teodor Gorghe wrote:
I will investigate!
There is another issue like ping pong cadence related to new hard coded constants and java web client logic. Please use Claude Code to assist in this work.
Fix proposals for the r16675 code review¶
Wrote 11 proposals in .tmp/, one per non-style finding of .tmp/code-review.textile. The two [MINOR] style findings - the GuiWebDriver.init blank line and the p2j.sso_reauth.js separator width - were skipped as requested, though the first is incidentally removed by proposal-issue-major-functional-3.
| File | Finding |
|---|---|
proposal-issue-major-functional-1.txt |
inbound-only deadline; outbound activity buys no tolerance |
proposal-issue-major-performance-1.txt |
backlog "draining" heuristic tears down healthy sessions |
proposal-issue-major-functional-2.txt |
deadline derived from pingPongInterval, ignoring configured idle tolerance |
proposal-issue-major-functional-3.txt |
PING_PONG_INTERVAL exported unclamped by WebPageKeysProvider |
proposal-issue-minor-security-1.txt |
backlog grace forges lastDataNanos with no peer evidence |
proposal-issue-minor-functional-1.txt |
MIN_PING_INTERVAL floor applied after the idle clamp |
proposal-issue-minor-functional-2.txt |
p2j.sso_reauth.js keystroke swallowing still occurs |
proposal-issue-minor-functional-3.txt |
StatusCode.SHUTDOWN reused for heartbeat expiry |
proposal-issue-minor-functional-4.txt |
pushWorker read off-lock, field not volatile |
proposal-issue-minor-functional-5.txt |
javadoc names non-existent client/web/webSocketTimeout |
proposal-issue-minor-functional-6.txt |
ConfigItem.MAX_IDLE_TIME javadoc overreaches |
Naming note: the review has no severe findings and one security finding, so that file uses -security- in the category slot rather than forcing it into functional|performance. Numbering restarts per severity+category pair, in review order.
Two things worth knowing before acting on these.
The five keep-alive findings are one rework, not five patches¶
Their fixes interact - one review bullet asks for outbound-write evidence to extend tolerance while another warns that outbound evidence must not substitute for peer liveness. That is resolved with two clocks: lastDataNanos stays inbound-only and sets the hard deadline; completed outbound writes (a new PushMessagesWorker.getTransmittedCount(), which unlike getQueueSize() sees the in-flight message) postpone it by a bounded, absolutely-capped grace. Each file owns a distinct mechanism, proposal-issue-major-performance-1 carries the composed tick body, and each states its dependencies. In particular, major-functional-1 must not land without minor-security-1 - the grace would be unbounded.
Verification is against the code, not the review text¶
Confirmed in the tree: the ConfigItem constructor contract behind the wrong key name; the three-reader split on pingPongInterval (WebPageHandler.java:251 unclamped vs. the two drivers); all three addWebSocketHandler registrations plus ReportWebServer.java:148 setting maxIdleTime to 36000000; that client/web/ slash notation exists only on the three lines this diff added; and that p2j.keyboard.js:319-336 already contains the exact listener-removal code needed for the SSO fix - inlined in init() with no way to call it, which is why extracting it is a refactor rather than new logic.
Nothing was compiled or run, so the code hunks are proposals rather than verified patches. Each file ends with concrete verification steps, and several note that the interval/deadline arithmetic has no test coverage at all today and would be worth extracting into a testable static helper.
The following changes
=== modified file 'src/com/goldencode/p2j/ui/client/driver/web/res/p2j.js'
--- old/src/com/goldencode/p2j/ui/client/driver/web/res/p2j.js 2026-05-26 07:34:53 +0000
+++ new/src/com/goldencode/p2j/ui/client/driver/web/res/p2j.js 2026-07-30 10:44:18 +0000
@@ -4,7 +4,7 @@
**
** Copyright (c) 2014-2026, Golden Code Development Corporation.
**
-** -#- -I- --Date-- ------------------------------Description----------------------------------
+** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 MAG 20140110 First version.
** 002 MAG 20140204 Prevent back navigation. Disable browser context menu.
** 003 MAG 20140702 Add tty.clipboard module.
@@ -90,6 +90,7 @@
** changeDisplayStyle/changeVisibilityStyle. Removed unnecessary async from
** overrideDialogStyle/overrideTitleStyle. Added cachedTitle guard to setTitle.
** Added buttonsDiv null guard to setButtons.
+** 035 TG 20260728 Drove the loading progress bar easing from elapsed time instead of a tick count.
*/
/*
** This program is free software: you can redistribute it and/or modify
@@ -686,7 +687,9 @@
}
loadingProgressBar.set({"value" : value});
var v0 = loadingProgressBar.get("value");
- var time = 1;
+ // the easing below is driven by elapsed wall-clock time rather than by a tick count, so a
+ // throttled or delayed timer still yields the same curve instead of advancing more slowly
+ var started = Date.now();
if (progressTimer)
{
clearInterval(progressTimer);
@@ -722,8 +725,8 @@
progressTimer = null;
return;
}
+ var time = Math.max(1, (Date.now() - started) / 100);
var v = v0 + (v1 - v0) * ( 1 - 1 /(1 + Math.log2(time)));
- time++;
loadingProgressBar.set({"value" : v});
},
100);
are good!
#19 Updated by Sergey Ivanovskiy about 13 hours ago
Updated the previous note as it was broken partly.
#20 Updated by Teodor Gorghe about 11 hours ago
Sergey, this is my bad.
I have rebased the branch, but I have forgot to push it.
I have pushed it and please check 11645a/r16697.