Project

General

Profile

Bug #11645

improve FWD Web Client ping-pong and timeouts

Added by Constantin Asofiei about 1 month ago. Updated 19 days ago.

Status:
Review
Priority:
Normal
Assignee:
Target version:
-
Start date:
Due date:
% Done:

100%

billable:
No
vendor_id:
GCD
case_num:
version_reported:
version_resolved:
reviewer:
production:
No
env_name:
topics:

proposal-issue-major-functional-1.txt Magnifier (9.95 KB) Sergey Ivanovskiy, 08/12/2026 02:56 AM

proposal-issue-major-functional-2.txt Magnifier (10.7 KB) Sergey Ivanovskiy, 08/12/2026 02:56 AM

proposal-issue-major-functional-3.txt Magnifier (8.96 KB) Sergey Ivanovskiy, 08/12/2026 02:56 AM

proposal-issue-major-performance-1.txt Magnifier (12.3 KB) Sergey Ivanovskiy, 08/12/2026 02:56 AM

proposal-issue-minor-functional-1.txt Magnifier (8.8 KB) Sergey Ivanovskiy, 08/12/2026 02:56 AM

proposal-issue-minor-functional-2.txt Magnifier (11.3 KB) Sergey Ivanovskiy, 08/12/2026 02:56 AM

proposal-issue-minor-functional-3.txt Magnifier (8.12 KB) Sergey Ivanovskiy, 08/12/2026 02:56 AM

proposal-issue-minor-functional-4.txt Magnifier (9.67 KB) Sergey Ivanovskiy, 08/12/2026 02:56 AM

proposal-issue-minor-functional-5.txt Magnifier (8.06 KB) Sergey Ivanovskiy, 08/12/2026 02:56 AM

proposal-issue-minor-functional-6.txt Magnifier (8.37 KB) Sergey Ivanovskiy, 08/12/2026 02:56 AM

proposal-issue-critical-functional-1.txt Magnifier (12 KB) Sergey Ivanovskiy, 08/12/2026 04:15 PM

proposal-issue-major-functional-1.txt Magnifier (8.6 KB) Sergey Ivanovskiy, 08/12/2026 04:15 PM

proposal-issue-minor-functional-1.txt Magnifier (8.27 KB) Sergey Ivanovskiy, 08/12/2026 04:15 PM

proposal-issue-minor-functional-2.txt Magnifier (8.55 KB) Sergey Ivanovskiy, 08/12/2026 04:15 PM

proposal-issue-minor-functional-3.txt Magnifier (7.52 KB) Sergey Ivanovskiy, 08/12/2026 04:15 PM

proposal-issue-minor-functional-4.txt Magnifier (6.21 KB) Sergey Ivanovskiy, 08/12/2026 04:15 PM

proposal-issue-minor-functional-5.txt Magnifier (5.94 KB) Sergey Ivanovskiy, 08/12/2026 04:15 PM

proposal-issue-minor-functional-6.txt Magnifier (5.16 KB) Sergey Ivanovskiy, 08/12/2026 04:15 PM

proposal-issue-minor-functional-7.txt Magnifier (6.14 KB) Sergey Ivanovskiy, 08/12/2026 04:15 PM

proposal-issue-minor-performance-1.txt Magnifier (10.4 KB) Sergey Ivanovskiy, 08/12/2026 04:15 PM

proposal-major-functional-2.txt Magnifier (8.34 KB) Sergey Ivanovskiy, 08/14/2026 05:13 AM

proposal-major-functional-1.txt Magnifier (9.41 KB) Sergey Ivanovskiy, 08/14/2026 05:13 AM

proposal-major-security-1.txt Magnifier (8.92 KB) Sergey Ivanovskiy, 08/14/2026 05:13 AM

proposal-major-performance-1.txt Magnifier (10.1 KB) Sergey Ivanovskiy, 08/14/2026 05:13 AM

code-review.textile Magnifier (25.1 KB) Sergey Ivanovskiy, 08/14/2026 05:13 AM

History

#2 Updated by Constantin Asofiei about 1 month ago

In #11630 we found FWD can be improved when using cloudflare in front of the FWD Web client. Some notes:
  • 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 setInterval usage (like for reconnect) to Worker threads

#3 Updated by Teodor Gorghe about 1 month ago

  • Assignee set to Teodor Gorghe
  • Status changed from New to WIP

#4 Updated by Teodor Gorghe about 1 month ago

Committed revision 16675 on task branch 11645a:
  • Moved websocket ping-pong timer to server. Fixed additional bugs related with setInterval.
Additional notes to 11645a:
  • 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 setInterval like p2j.sso_reauth.js, p2j.perf_tests.js, etc., has been improved to reduce browser throttling (also some bug fixes on p2j.sso_reauth.js, for example, the case when a auth token expires).

#5 Updated by Teodor Gorghe about 1 month 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 about 1 month ago

  • % Done changed from 0 to 100

#7 Updated by Sergey Ivanovskiy about 1 month 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 about 1 month 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 about 1 month 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 22 days 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 22 days 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 22 days 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.

Pros:
  • 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.
Cons:
  • 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 22 days 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 22 days 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 21 days 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 21 days ago

I will investigate!

#17 Updated by Teodor Gorghe 21 days 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 21 days ago

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 21 days ago

Updated the previous note as it was broken partly.

#20 Updated by Teodor Gorghe 21 days 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.

#21 Updated by Sergey Ivanovskiy 21 days ago

Code review and fix proposals - r16675 (server-driven websocket keep-alive)

Scope

Reviewed the branch diff against trunk: 15 files, ~830 added and ~300 removed lines. The revision does three things: it moves the websocket keep-alive heartbeat from the JS client to the server (new MSG_SERVER_PING, a server-side Timer in WebClientProtocol, dead-peer detection closing with private code 4001); it replaces websocket continuation frames with application-level MSG_CHUNK messages reassembled in the browser; and it hardens the SSO re-auth countdown (deadline-based ticks, a grace window, and a page reload on expiry in place of the deleted showSessionExpiredPage).

Modified areas: com.goldencode.p2j.ui.client.driver.web (5 java + 3 js), com.goldencode.p2j.ui.client.gui.driver.web (2 java + 1 js), com.goldencode.p2j.ui.client.chui.driver.web (1 java), com.goldencode.p2j.web (1), com.goldencode.p2j.util (1), com.goldencode.p2j.main (1).

Domains reviewed: fwd-web-gui-driver, fwd-chui-driver-streams, fwd-server-infrastructure, fwd-web-login-process.

Method

Seven parallel review passes (style, security, performance, and one functional pass per matched domain) produced 21 candidate findings. Each of the 16 non-style findings was then handed to an independent adversarial challenge pass instructed to identify the concrete trigger in the current code, or reject. Ten were confirmed, six rejected as theoretical. The challenge pass also downgraded three findings and rewrote one whose causal narrative was wrong.

Nothing was compiled or run. The proposals are code hunks with verification steps, not verified patches.

Confirmed findings

Severity Category Location Finding
CRITICAL functional WebClientProtocol.sendBinaryMessageStreamed A MSG_CHUNK transfer is not bound to a session; its tail is delivered to the next one
MAJOR functional p2j.socket.handleCloseEvent resetChunkTransfers() is unreachable on every client-initiated close
MINOR functional p2j.socket.collectChunk The MAX_CHUNK_BYTES abandon path poisons the transfer it abandons
MINOR functional p2j.sso_reauth.handleSsoReauth The expiry reload terminates the session it claims to re-attach to
MINOR functional p2j.sso_reauth.handleSsoReauth The reload raises the leave-confirmation; "Stay" freezes the overlay
MINOR functional p2j.sso_reauth.handleSsoReauth The expiry path leaves the IdP popup open as an orphan
MINOR functional PushMessagesWorker.getQueueSize Dead method whose javadoc documents a guard that was never built
MINOR functional WebClientProtocol.sendLock Field javadoc still documents the removed partial-frame contract
MINOR functional ConfigItem.MAX_IDLE_TIME Javadoc describes a page export that no shipped profile performs
MINOR performance WebClientProtocol.sendBinaryMessageStreamed The streamed payload accumulates in the push queue

Five [MINOR] style findings (javadoc line length, two blank-line issues, header separator width in five files, one stray blank line) were reported without challenge, as style is judged against the rulebook rather than reachability.

The critical finding

queueChunk validates each piece against the current session and keeps no record of which session the transfer began on. Two independent routes carry a transfer's tail onto a later connection: the push queue survives the swap, because startPushWorker reuses the existing worker and onConnect never calls stopPushWorker (the sole caller is onClose's active-session branch, which the session != this.session guard skips for a replaced session); and the producer thread still inside the streaming loop simply observes the new session on its next call. This is the designed ordering on a half-open link - the page gives up after maxLostPings periods, roughly half the server idle timeout, so onConnect(new) precedes onClose(old) - and the endpoint really is the same object across a reconnect, since GenericWebSocketCreator returns the one GuiWebSocket instance for every upgrade.

On the page, collectChunk keys only on the transfer id with no notion of where a transfer starts, so an arriving tail begins a fresh reassembly and the piece carrying the last-flag re-enters messageHandler with whole[0] set to raw pixel or font data. 0x85 is MSG_QUIT, which calls doRedirectToLogoutPage() - a spurious logout roughly one time in 256.

Rejected findings

Recorded so they are not re-raised. Each was rejected after an attempt to construct a concrete trigger failed.

Finding Why rejected
64 KiB CHUNK_PAYLOAD_SIZE caps WAN throughput Jetty 12 completes the send callback on flush into the kernel socket buffer, not on peer ACK, and queueChunk does not block per chunk - so the "128 serialized round trips" mechanism does not exist. Restoring 1 MiB would also mean per-piece humongous allocations
Per-chunk synchronized (lock) contention The block holds only a null check, isOpen(), six byte stores and a non-blocking offerLast; the socket write happens on the worker thread under a different monitor
JS reassembly costs 2x memory The previous continuation-frame path had the browser buffer and materialize the whole message internally - the same peak and the same copy, just below the JS boundary
JS per-message preamble runs 128x per image Measured at 0.18 ms per image for the whole timer-op mix; feeding the silence watchdog per piece is the deliberate design
ConfigItem.PING_PONG_INTERVAL javadoc omits clamping No ConfigItem javadoc in the file documents clamping, including two with the identical rule; the clamping is fully documented where it is implemented
effectivePingPongInterval can return below MIN_PING_PONG_INTERVAL Requires webSocketTimeout under 3000 ms, which is below CLIENT_RESPONSE_TIMEOUT and self-defeating; the code warns for exactly this case, so it is signposted acceptance

Fix proposals

One file per non-style finding, in .tmp/. Each states root cause with verified line references, concrete code hunks, dependencies, and verification steps.

File Severity Finding
proposal-issue-critical-functional-1.txt CRITICAL MSG_CHUNK transfer not bound to a session
proposal-issue-major-functional-1.txt MAJOR resetChunkTransfers() unreachable on client-initiated closes
proposal-issue-minor-functional-1.txt MINOR MAX_CHUNK_BYTES abandon path poisons the transfer
proposal-issue-minor-functional-2.txt MINOR SSO expiry reload terminates the session
proposal-issue-minor-functional-3.txt MINOR Leave-confirmation on the expiry reload
proposal-issue-minor-functional-4.txt MINOR Orphaned IdP popup
proposal-issue-minor-functional-5.txt MINOR getQueueSize dead code
proposal-issue-minor-functional-6.txt MINOR sendLock javadoc
proposal-issue-minor-functional-7.txt MINOR ConfigItem.MAX_IDLE_TIME javadoc
proposal-issue-minor-performance-1.txt MINOR Streamed payload accumulates in the push queue

The three chunk findings are one change

The critical finding, the major one, and minor-functional-1 all end at the same place: a piece that arrives with no matching reassembly in progress is dispatched with an arbitrary payload byte as its message type. The common remedy is a first-piece flag on the chunk header, so the peer refuses to begin a reassembly without it.

   /** MSG_CHUNK flags bit 1: this piece opens the transfer. */
   public static final byte CHUNK_FLAG_FIRST = 0x02;
   if (!parts)
   {
      if (!first)
      {
         p2j.logger.error("Discarding orphaned message piece, transfer id " + id);
         return null;
      }
      parts = chunkTransfers[id] = [];
   }

The other two proposals state this as a hard dependency and are not safe to land alone: clearing state on close, or abandoning an oversized transfer, both leave a suffix that then starts a new reassembly - the same defect wearing a different hat. If only one change from this review is taken, take the first-piece flag.

The critical proposal additionally binds each transfer to a connection generation, which stops the producer feeding a dead transfer, and optionally purges queued output on session replacement.

The three SSO findings are one edit

minor-functional-2, -3 and -4 modify the same five lines of the countdown expiry branch and share one internalReload flag exported from p2j.socket. That flag makes onpagehide skip the quit-on-reload sessionStorage write and makes onbeforeunload skip the leave-confirmation, which together are what turn the reload into the re-attach the code comment already claims it performs. Apply them as a single change.

Risks worth reading before applying

Two proposals carry real risk and each offers a lower-risk variant alongside.

  • critical-functional-1 section 2.3 - purging the push queue on session replacement drops draw output the peer has not yet received. onConnect resynchronizes through initRemoteClient, but this is the one part that changes behaviour for non-chunk traffic. Sections 2.1 and 2.2 are sufficient for correctness without it.
  • minor-performance-1 section 2.1 - the in-flight chunk semaphore introduces permit-lifecycle obligations on teardown and abandon paths, and a leak would hang the producer, which is worse than the footprint it fixes. Section 2.2 is an approximate fix at a fraction of the complexity, and is the recommended option unless a measurement shows the footprint matters.

minor-functional-2 section 2.1 also puts a deliberate hole in the long-standing quit-on-reload invariant. The hole is narrow - one caller, set immediately before an unload the page itself requested - but a future caller that sets the flag without unloading would leave a GUI page that no longer arms quit-on-reload.

Housekeeping

.tmp/ also contains proposal-issue-major-functional-2.txt, -major-functional-3.txt, -major-performance-1.txt and -minor-security-1.txt from an earlier review round. That round has already been applied - the code now has SESSION_HEARTBEAT_LOST = 4001 rather than a reused StatusCode.SHUTDOWN, a volatile pushWorker, client:web: key notation and clamping in WebPageHandler, which were exactly its subjects. Those four filenames imply membership in the current set and do not belong to it; delete or move them aside.

Fix proposals for the current r16675 code review

Wrote 10 proposals in .tmp/, one per non-style finding of .tmp/code-review.textile. The five [MINOR] style findings (javadoc line length, blank lines, header separator width) were skipped as requested.

File Severity Finding
proposal-issue-critical-functional-1.txt CRITICAL MSG_CHUNK transfer not bound to a session; its tail is delivered to the next one
proposal-issue-major-functional-1.txt MAJOR resetChunkTransfers() unreachable on every client-initiated close
proposal-issue-minor-functional-1.txt MINOR MAX_CHUNK_BYTES abandon path poisons the transfer it abandons
proposal-issue-minor-functional-2.txt MINOR SSO expiry reload terminates the session it claims to re-attach to
proposal-issue-minor-functional-3.txt MINOR Expiry reload raises the leave-confirmation; "Stay" freezes the overlay
proposal-issue-minor-functional-4.txt MINOR Expiry path leaves the IdP popup open as an orphan
proposal-issue-minor-functional-5.txt MINOR PushMessagesWorker.getQueueSize dead, javadoc documents a guard never built
proposal-issue-minor-functional-6.txt MINOR WebClientProtocol.sendLock javadoc still documents partial frames
proposal-issue-minor-functional-7.txt MINOR ConfigItem.MAX_IDLE_TIME javadoc describes an export no profile performs
proposal-issue-minor-performance-1.txt MINOR Streamed payload accumulates in the push queue

Numbering restarts per severity+category pair, in review order.

Stale files from the previous round

.tmp/ also contains proposal-issue-major-functional-2.txt, -major-functional-3.txt, -major-performance-1.txt and -minor-security-1.txt, which belong to an earlier review round (all written 2026-08-12 ~09:2x) and are not part of this set. That round appears to have been applied already - the code now has SESSION_HEARTBEAT_LOST = 4001 rather than a reused StatusCode.SHUTDOWN, a volatile pushWorker, client:web: key notation, and clamping in WebPageHandler, which were exactly its subjects. Delete them or move them aside before working from this directory; their filenames imply membership in the current set and they do not belong to it.

The chunk findings are one change, not four

critical-functional-1, major-functional-1 and minor-functional-1 all end at the same place: a piece that arrives with no matching reassembly in progress is dispatched with an arbitrary payload byte as its message type. The common remedy is the CHUNK_FLAG_FIRST bit specified in critical-functional-1 section 2.2 - the peer refuses to begin a reassembly without it. The other two proposals state that dependency and are not safe to land alone: clearing state on close, or abandoning an oversized transfer, both leave a suffix that then starts a new reassembly, which is the same defect wearing a different hat.

Likewise minor-functional-2, -3 and -4 edit the same five lines of the SSO expiry branch and share the internalReload flag; apply them as one edit.

Verification is against the code, not the review text

Confirmed in the tree while writing these: startPushWorker reuses the worker and onConnect never calls stopPushWorker, so the queue genuinely survives a session swap; closeWebSocketSafely nulls ws synchronously before the close event lands, so handleCloseEvent's this != ws early return really does skip the new cleanup; onpagehide writes the exitTheApplication sessionStorage object on a scripted reload and isRequiredToRedirect honours it for the whole watchdogTimeout window; getQueueSize has no caller anywhere in src/, test/ or tools/; and cleanupReauthUi is the only code path that closes reauthPopup.

Nothing was compiled or run, so the code hunks are proposals rather than verified patches. Each file ends with concrete verification steps. Two carry real risk and say so: critical-functional-1 section 2.3 (purging queued output on reconnect drops draw output not yet delivered) and minor-performance-1 section 2.1 (a permit leak would hang the producer, which is worse than the footprint it fixes) - both offer a lower-risk variant alongside.

#22 Updated by Sergey Ivanovskiy 21 days ago

There is a hard-coded constant that should be fixed:

-            put(ConfigItem.PING_PONG_INTERVAL, String.valueOf(20 * 60 * 1000));
+            put(ConfigItem.PING_PONG_INTERVAL, String.valueOf(20 * 60 * 1000 / 3));

It looks like WebConfigurationConstants.PING_PONG_INTERVAL can be used here instead.

Could you please explain the purpose of MSG_CHUNK sent from the Java web client to the browser web client? A similar message, MSG_PARTIAL, is sent in the opposite direction (from the browser web client to the Java web client). Why does sendPartial(ByteBuffer frame, boolean isLast) need to be changed?

#23 Updated by Sergey Ivanovskiy 20 days ago

  • Status changed from Review to WIP
  • % Done changed from 100 to 90

I changed the task state to WIP - 90%.

#24 Updated by Teodor Gorghe 20 days ago

I will take through the automated generated review finding.

Sergey, that section of sendPartial came from #11327, which splits that entire byte[] (like a big image), into chunks. At that time, I thought that using RFC6455 websocket partial frame specification (by Jetty's sendPartial method) is ideal, since there was just one main producer.

But now, when we have an async thread, the one which sends ping from the server side (from FWD client to browser), and we want to use a custom implemented PING-PONG to detect connection drops on browser-side, we can't have a send PING message and a big file transfer occurring in the same time because of websocket protocol limitation. The commit message also says it: "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)."

When using partial frames, the javascript side will notice these just when t*his ressembles, when the entire transmission finishes* (order of GB, or a very slow connection, < 2GB). In that time, the watchdog could trigger and kill the connection. By refactoring to use MSG_CHUNK, we make this being handled on application level, which allowed more control, especially a more accurate lastServerMessageAt measurement.

#25 Updated by Teodor Gorghe 20 days ago

  • % Done changed from 90 to 100
  • Status changed from WIP to Review

Sergey Ivanovskiy wrote:

There is a hard-coded constant that should be fixed:
[...]
It looks like WebConfigurationConstants.PING_PONG_INTERVAL can be used here instead.

Right.
Done in 11645a/r16698.

#26 Updated by Sergey Ivanovskiy 20 days ago

Teodor Gorghe wrote:

I will take through the automated generated review finding.

Sergey, that section of sendPartial came from #11327, which splits that entire byte[] (like a big image), into chunks. At that time, I thought that using RFC6455 websocket partial frame specification (by Jetty's sendPartial method) is ideal, since there was just one main producer.

But now, when we have an async thread, the one which sends ping from the server side (from FWD client to browser), and we want to use a custom implemented PING-PONG to detect connection drops on browser-side, we can't have a send PING message and a big file transfer occurring in the same time because of websocket protocol limitation. The commit message also says it: "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)."

When using partial frames, the javascript side will notice these just when t*his ressembles, when the entire transmission finishes* (order of GB, or a very slow connection, < 2GB). In that time, the watchdog could trigger and kill the connection. By refactoring to use MSG_CHUNK, we make this being handled on application level, which allowed more control, especially a more accurate lastServerMessageAt measurement.

Thanks for the detailed explanation, that clears it up. It makes sense to repeat here:
  1. At the pure WebSocket Protocol Level (RFC 6455): Control frames (like PING / PONG) are explicitly allowed to be interleaved inside a fragmented message (FIN=0 sequence).
  2. At the Jetty / Application Level: While the RFC allows it, many higher-level frameworks or application-level protocol parsers cannot handle control messages interrupting an in-flight byte-stream without throwing an exception, dropping state, or corrupting client-side frame assembly.

Therefore the browser client parser couldn't cleanly handle a PING arriving in the middle of an RFC-level fragmented stream sendPartial, you chose to switch from protocol-level fragmentation to application-level chunking.

#27 Updated by Sergey Ivanovskiy 20 days ago

Does it make sense to use a unified message type for chunks, replacing MSG_PARTIAL with MSG_CHUNK? The name MSG_PARTIAL was used because browsers lack APIs for protocol-level fragmentation (like Jetty's sendPartial).

#28 Updated by Teodor Gorghe 19 days ago

Sergey Ivanovskiy wrote:

Thanks for the detailed explanation, that clears it up. It makes sense to repeat here:
  1. At the pure WebSocket Protocol Level (RFC 6455): Control frames (like PING / PONG) are explicitly allowed to be interleaved inside a fragmented message (FIN=0 sequence).
  2. At the Jetty / Application Level: While the RFC allows it, many higher-level frameworks or application-level protocol parsers cannot handle control messages interrupting an in-flight byte-stream without throwing an exception, dropping state, or corrupting client-side frame assembly.

Therefore the browser client parser couldn't cleanly handle a PING arriving in the middle of an RFC-level fragmented stream sendPartial, you chose to switch from protocol-level fragmentation to application-level chunking.

I know about PING / PONG control frames, but I can repeat the same note as #11645-12:
  • the main reason that we have PING-PONG is to detect if the connection has dropped.
  • The protocol level PING-PONG makes the connection to not be dropped when in idle, but you can't see that from application.

#29 Updated by Teodor Gorghe 19 days ago

Sergey Ivanovskiy wrote:

Does it make sense to use a unified message type for chunks, replacing MSG_PARTIAL with MSG_CHUNK? The name MSG_PARTIAL was used because browsers lack APIs for protocol-level fragmentation (like Jetty's sendPartial).

Didn't knew that there was an actual implementation, let me check.

#30 Updated by Teodor Gorghe 19 days ago

Done in 11645a/r16699. MSG_PARTIAL implements the chunked transfer, but the direction was from browser to client. MSG_CHUNK was from client to browser, so it made sense to unify in a single name.

#31 Updated by Sergey Ivanovskiy 19 days ago

Teodor Gorghe wrote:

Done in 11645a/r16699. MSG_PARTIAL implements the chunked transfer, but the direction was from browser to client. MSG_CHUNK was from client to browser, so it made sense to unify in a single name.

Agreed. Please check that you committed these revisions and applied the idea to use MS_CHUNK bidirectionally.
From my network view
Tree is up to date at revision 16697 of branch /home/sbi/secure/code/p2j_repo/p2j/active/11645a

#32 Updated by Teodor Gorghe 19 days ago

I have already committed and the branch is at revision 16699:

------------------------------------------------------------
revno: 16699
committer: Teodor Gorghe <tg@goldencode.com>
branch nick: 11645a
timestamp: Fri 2026-08-14 06:05:20 +0000
message:
  Renamed MSG_CHUNK as MSG_PARTIAL.
------------------------------------------------------------
revno: 16698
committer: Teodor Gorghe <tg@goldencode.com>
branch nick: 11645a
timestamp: Thu 2026-08-13 10:37:58 +0000
message:
  Addressed code review #11645-21.
------------------------------------------------------------
revno: 16697
committer: Teodor Gorghe <tg@goldencode.com>
branch nick: 11645a
timestamp: Wed 2026-08-12 08:39:42 +0000
message:
  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).

Checkout (format: 2a)
Location:
       checkout root: .
  checkout of branch: bzr+ssh://localhost:2224/opt/secure/code/p2j_repo/p2j/active/11645a/

Related branches:
    push branch: bzr+ssh://localhost:2224/opt/secure/code/p2j_repo/p2j/active/11645a/

#33 Updated by Sergey Ivanovskiy 19 days ago

Yes, thanks, I got them. Please update to rev 16700, where I applied minor style fixes from the AI review.

#34 Updated by Sergey Ivanovskiy 19 days ago

The committed rev 16699, 16700 looks good but the AI review found 4 major issues that seems worth to be taken into account.

Task #11645 - code review of r16696..r16699

Scope reviewed

Branch 11645a, diff r16695..r16699 - r16695 is the most recent trunk-nicked ancestor, so this is the full branch delta. 15 files, 2437 diff lines. The review was performed against r16699; the style fixes it produced are committed as r16700.

Subject of the change: the websocket keep-alive heartbeat moves from the JS client to the server (new MSG_SERVER_PING), large messages are reframed onto MSG_PARTIAL in both directions with first/last flags instead of native continuation frames, and the SSO re-auth expiry becomes an internal reload.

Package Files
com.goldencode.p2j.ui.client.driver.web 5 Java + 3 JS
com.goldencode.p2j.ui.client.gui.driver.web 2 Java + 1 JS
com.goldencode.p2j.ui.client.chui.driver.web 1 Java
com.goldencode.p2j.main / com.goldencode.p2j.util / com.goldencode.p2j.web 3 Java

Method

Seven parallel review passes - style, security, performance, and one functional pass per matched domain (fwd-web-gui-driver, fwd-web-login-process, fwd-server-infrastructure, fwd-chui-driver-streams) - followed by 17 independent adversarial challenge passes, one per non-style finding. Each challenger was required to identify a concrete trigger in the current code or reject the finding.

Results at a glance

Stage Count
Raw findings from the review passes 27
After dedup (5 reviewers converged on MessagesCollector.reset) 27 to 23 non-style + 10 style
Non-style findings challenged 17
Confirmed 13
Rejected 4
Final: confirmed findings 23 (5 MAJOR, 18 MINOR)
Category MAJOR MINOR Total
functional 2 5 7
security 0 2 2
performance 0 4 4
style 3 7 10

No MAJOR security or performance finding survived the challenge pass: the path-traversal finding was scoped down because its sink is pre-existing and untouched here, and the discardQueued accounting finding was downgraded because its impact is bounded.

Style fixes applied - committed as r16700

All 10 style findings are fixed. Verified: 0 violations remain among the lines this branch adds, both JS files pass node --check, and no double blank lines involving added lines remain.

  • WebClientProtocol.java:1860 - javadoc line was 130 characters; rewrapped to four lines under 110.
  • WebClientProtocol.java:1055 - wrapped parameter continuation had 30 leading spaces where 32 are needed to align with int generation.
  • WebClientProtocol.java:1968 - two consecutive blank lines before stopKeepAlivePing's javadoc.
  • WebClientProtocol.java:312 - added line was whitespace-only rather than empty (found by a second scan pass; the first missed whitespace-only lines).
  • WebClientMessageTypes.java:501 - two consecutive blank lines before MSG_SERVER_PING's javadoc.
  • p2j.socket.js:644-648 - the new MSG_PARTIAL wire-format comment was orphaned above the unrelated MSG_INVALIDATE_SELECTION field, with a whitespace-only line after it; moved onto MSG_PARTIAL itself, replacing the stale one-line comment there, and completed to document flags bit 1 (first piece) as well as bit 0 (last).
  • p2j.socket.js:1897 - for( was missing the required space after the keyword.
  • p2j.socket.js:7032 - whitespace-only line plus a double blank line after beginInternalReload.
  • p2j.sso_reauth.js:435-452 - cleanupReauthUi's javadoc, with its @param removeOverlay, had been separated from its subject by the newly inserted closeReauthPopup; moved closeReauthPopup above the javadoc so each comment sits on its own subject.
  • p2j.sso_reauth.js:16 - history-entry line was 113 characters; rewrapped onto a continuation line.

History entries and copyright years were checked across all 15 files and are correct - every file has an entry, sequence IDs are successive, and where a file gained several entries only the first carries the ID.

MAJOR findings

Both are regressions introduced by this branch, and both have a written proposal.

  • [MAJOR] functional p2j.sso_reauth handleSsoReauth - the expiry tick destroys the server-driven logout redirect. SsoTokenManager.handleInvalidToken arms forceLogout at SsoTokenManager.java:518 before calling sendSsoReauth at :539, while the module sets reauthDeadline only on receipt, so the server always kills the session first and the client tick always fires inside the shutdown. Nothing cancels countdownInterval on that path - cleanupReauthUi is reachable only from this module's own handlers, and there is no hook on MSG_SHUTTINGDOWN, MSG_QUIT or doRedirectToLogoutPage - so the tick runs beginInternalReload() + window.location.reload() mid-handshake. The MSG_SHUTTINGDOWN ack is lost so quit() burns the full clientResponseTimeout, the pending window.top.location.replace(logoutPage) is aborted, and the reloaded iframe points at an embedded server shutdownServer() has just torn down. The user lands on a browser network-error page instead of the login page. Before this branch the expiry path only replaced the document body, leaving the socket module intact to finish the redirect. See proposal-major-functional-1.txt.
  • [MAJOR] functional WebClientProtocol onConnect - the new pushWorker.discardQueued() at :480-486 throws away all pending server-to-browser output, justified by the comment "initRemoteClient below resyncs", but GuiWebDriver.initRemoteClient() is literally a no-op at GuiWebDriver.java:1033-1036; only ChuiWebSimulator resyncs. Trigger: a half-open socket where the browser's silence watchdog reconnects before Jetty delivers onClose, so onConnect takes its session-replacement branch with a full backlog. The page was never reloaded, so the browser sends MSG_PING_PONG not MSG_PAGE_LOADED and nothing requests a repaint - the canvas is left permanently stale. Note the discard has effect only on this path (elsewhere pushWorker is null), so it takes effect exactly where it is unsafe, and the cross-generation hazard it guards against is already covered twice by queuePartial's generation check and the client's resetChunkTransfers(). See proposal-major-functional-2.txt.

MINOR findings

Full text with verified triggers is in code-review.textile. Summarised here by area.

Reconnect and session-replacement races - one root cause, four faces

Five of the seven review passes converged independently on onConnect's session-replacement branch. It runs on the Jetty thread while the previous session's webWorker and the collector's asynchIOExecutor are still live, because startWebWorker no-ops when the worker exists and the replaced session's onClose is skipped by the session != this.session gate.

  • WebClientProtocol.MessagesCollector.reset - clears the plain unsynchronized HashMap@s @partialMessages and payloadMessagesTypes while two other threads mutate them; a queued AppendMessageTask then gets null from partialMessages.get(msgId) and NPEs on channel.write, and the catch handles only IOException so submit()'s FutureTask swallows it - a silently truncated upload. Because nextMsgId is page-scoped and restarts at 0, a stale queued task can also write into the new page's transfer id 0; PARTIAL_FLAG_FIRST blocks that through processPartialMesssage but not through the executor queue.
  • WebClientProtocol.onConnect - reads the non-volatile, unguarded collector field from the Jetty thread while it is lazily assigned on the webtaskworker thread via the one MSG_PARTIAL switch case that never takes lock. pushWorker was made volatile in this same change; collector was not. A reconnect can see null, skip the reset, and silently leak the open @FileChannel@s and temp files the reset exists to drop.
  • PushMessagesWorker.discardQueued - queuedBytes.set(0L) races the worker's in-flight send, leaving the counter negative by one message size, and messages.offerFirst runs after messages.clear() so a message from the superseded generation survives the discard. Downgraded from MAJOR: one worker thread means at most one message races per discard, every discard re-bases with an absolute set, and pieces are capped at 64 KB - so the 32 MB guard is perturbed by tens of KB, not progressively raised. See proposal-major-performance-1.txt.
  • WebClientProtocol.queuePartial - an abandoned streamed transfer is dropped with no notification to the waiter, so GuiWebSocket.createFont blocks forever: waitForResult(msgId) passes timeout 0, which is implemented as "indefinitely", and nothing releases pending waiters on reconnect or close. Reachable through FontManager's lazy cache-miss path for any font under deploy/server/fonts. The same hang applies to non-streamed sendBinaryMessage + waitForResult(msgId) pairs - timeout 0 waits are systemically unsafe across a reconnect.

Security

  • GuiWebSocket.processChannel - the MSG_FILE_UPLOADING filename reaches Paths.get() and Files.newByteChannel with no normalize(), no getFileName() and no containment check, and tmpDir ends with a separator so concat yields a traversable prefix. This is a real privilege-boundary crossing, not a user writing their own files: the spawned web client runs server-side, its websocket endpoint has no Origin check, and under OS_USER_OVERRIDE every user's client runs as the shared DEFAULT_OS_USER. FWD's own UploadHandler:231-233 already does the missing check. Scoped MINOR for this diff because the sink and its two twins are pre-existing and untouched - this branch only reworks the delivery route. See proposal-major-security-1.txt; raise as its own ticket.
  • WebClientProtocol.MessagesCollector.processPartialMesssage - inbound reassembly has no aggregate size cap and no per-transfer expiry, the mirror image of the MAX_TRANSFER_BYTES / MAX_CHUNK_BYTES caps this same branch added on the JS side. A page that sets PARTIAL_FLAG_FIRST and never sets PARTIAL_FLAG_LAST grows a temp file in the shared java.io.tmpdir until ENOSPC; onClose does not reset the collector, so while the socket is held open the window is attacker-controlled. Files are 0600 and DELETE_ON_CLOSE, so nothing survives process death. Related: processBinaryMessage gates on length > PARTIAL_HEADER_SIZE, so a header-only 6-byte piece is dropped even carrying PARTIAL_FLAG_LAST - and the Java sender emits exactly such a terminator, so the guard should be >=.

Performance

  • WebClientProtocol.sendBinaryMessageStreamed - PARTIAL_PAYLOAD_SIZE is 64 KB where the previous path used 1 MB with a reused buffer, so a 3840x2160 image becomes 508 messages instead of 33 frames, each paying a lock acquisition on the same monitor as GuiWebEmulatedWindow.offer, two CAS ops, a deque node, a notify, and a latch plus park/unpark round trip. Nothing forces 64 KB - the server's setMaxBinaryMessageSize is inbound-only and defaults to -1. Separately: Jetty 12.0.34 defaults DEFAULT_MAX_FRAME_SIZE to 65536 with auto-fragment on, and a piece is 6 + 65536 = 65542 bytes, so every piece is split into a full frame plus a 6-byte continuation frame. The payload should be (64 * 1024) - PARTIAL_HEADER_SIZE.
  • p2j.socket.js handleMessageEvent - five timer operations per inbound message (new Date(), idleTimer.reset()'s clearInterval + setInterval, and a setTimeout / clearTimeout pair) now run once per 64 KB piece instead of once per logical message, because the browser no longer reassembles natively. A 33 MB streamed image means ~2640 timer create/destroy operations and 528 extra macrotasks on the main thread. The code is unchanged; only the number of times it runs regressed.
  • p2j.socket.js collectChunk - holds every piece and then allocates the full-size Uint8Array and copies into it, releasing pieces only after the copy loop, so peak transient main-thread memory is ~2x the message and the code's own limits permit ~128 MB. Also total is recomputed with a loop over parts although parts.bytes already holds exactly that value. The original "extra copy the browser did not need" claim was dropped on verification - a browser reassembling fragments does the same accumulate-then-copy.
  • p2j.socket.js sendPartialMessage - the payload is copied one byte at a time instead of msg.set(data.subarray(position, limit), PARTIAL_HEADER_SIZE); both operands are confirmed typed arrays so the replacement is exact. Measured on V8: 13.6 ms versus 1.2 ms per 10 MB. Low impact, but a correct one-line change on a line this branch already touches. The same loop exists in p2j.network_socket.js:294-298.

Documentation

  • ConfigItem.MAX_IDLE_TIME - the new javadoc says the setting applies "only while client:web:webSocketTimeout is not positive", but no such key exists in any resolution path. The bootstrap option is client:web:socketTimeout and the directory node is webClient/webSocketTimeout; BootstrapConfig resolves only category():group():key() with no alias table. An administrator following the javadoc gets a silently ignored key while believing the timeout was raised. Especially misleading because neighbouring comments use the same client:web: form for keys that do exist.

Proposals

Four written proposals, in .tmp/rev16699/. Each covers problem, evidence and trigger, root cause, a recommended fix with code sketches, alternatives considered with reasons, risk, and a test plan.

File Finding Status
proposal-major-functional-1.txt SSO re-auth expiry tick destroys the logout redirect MAJOR confirmed
proposal-major-functional-2.txt onConnect discards the push backlog with no GUI resync MAJOR confirmed
proposal-major-security-1.txt Unvalidated upload filename reaches a file-write sink reported MAJOR, scoped MINOR here; pre-existing, own ticket
proposal-major-performance-1.txt discardQueued breaks the queued-bytes accounting reported MAJOR, downgraded MINOR

Because no MAJOR security or performance finding survived the challenge pass, the security and performance proposals cover the highest-ranked finding in each category - both of which were reported MAJOR before being adjusted.

Two proposals are coupled and should be settled together: functional-2 recommends removing the discardQueued() call entirely, which makes the performance-1 defect unreachable from onConnect but leaves the method broken. Fix or remove the method in the same change.

Rejected in the challenge pass

Recorded so they are not raised again. Full reasoning is at the end of code-review.textile.

  • WebClientProtocol.startKeepAlivePing - "the - tickMs slack narrows the configured tolerance to 60 s". The slack is load-bearing: because effectivePingPongInterval caps the interval at idleMs/PING_INTERVAL_DIVISOR, the heartbeat deadline and Jetty's own idle deadline are the same instant, and the client echoes synchronously from onmessage so the last inbound stamp sits one round trip after a tick. A strict >= dataIdleMs compare would miss that tick and fire a whole interval later - past the configured tolerance and past Jetty's idle close, losing the diagnostic 4001. The close actually lands at about 90 s with two unanswered pings; the proposed fix would be a regression. Residual below reporting threshold: the javadoc and WARNING describe the deadline as a single point, so the log can print "No websocket message received for 60000 ms (deadline=90000 ms)".
  • p2j.socket messageHandler - "the removed inbound MSG_PING_PONG case leaves dead protocol traffic". The facts hold but the consequence does not: handleMessageEvent unconditionally stamps lastServerMessageAt, resets idleTimer, zeroes lostPings and calls reportPingRecovery() for any inbound frame, and the server stamps lastDataNanos generically in onMessage - so the reply already delivers the side effect the handler existed for. A vestigial one-byte reply in unchanged code; hygiene, not a defect. Incidental: the comment at p2j.socket.js:5881 is now stale.
  • p2j.sso_reauth handleSsoReauth - "the expiry path has no session-gone guard and no login-page fallback". Rejected as a duplicate of the MAJOR finding: same defect, same trigger, same one-line fix. Its supporting evidence was folded into that finding.
  • PushMessagesWorker.awaitCapacity - "the per-limit wait is never notified between lowWater and STREAM_QUEUE_LIMIT, collapsing throughput to about 13 KB/s". The thresholds really are uncoupled, but the wait is timed at 5 s and on each wake the producer re-queues everything that drained, so the rate is socket-bound rather than notify-starved; holding the counter inside the un-notified band for a full 5 s needs a second unthrottled producer, meaning the socket is already saturated. The web-push-queue-low-bytes key also appears nowhere outside the Java source. Worth noting in passing: the javadoc invariant "a limit well below the high-water mark" is asserted in both files but unenforced, so STREAM_QUEUE_LIMIT would be better derived from highWater() / lowWater() than hard-coded at 1 MB.

Artefacts

All under .tmp/rev16699/.

File Contents
review-summary.textile this page
proposal-major-functional-1.txt proposal - SSO re-auth expiry
proposal-major-functional-2.txt proposal - push backlog discard
proposal-major-security-1.txt proposal - upload filename traversal
proposal-major-performance-1.txt proposal - queued-bytes accounting
code-review.textile curated findings with full verified triggers
code-review_uncurated.textile pre-challenge findings, for audit
branch-r16695-r16700.diff the reviewed delta plus the style fixes, r16695..r16700

The reviewed diff was r16695..r16699; branch-r16695-r16700.diff additionally contains the r16700 style fixes. The two are identical apart from those fixes, and byte-identical to the reviewed diff apart from file mtimes in the +++ headers.

#35 Updated by Sergey Ivanovskiy 19 days ago

  • reviewer Hynek Cihlar added
  • reviewer deleted (Sergey Ivanovskiy)

Hynek, please review rev 16700. The changes looks good.

Also available in: Atom PDF