Project

General

Profile

proposal-major-functional-2.txt

Sergey Ivanovskiy, 08/14/2026 05:13 AM

Download (8.34 KB)

 
1
================================================================================
2
PROPOSAL major-functional-2
3
onConnect discards the whole push backlog, but the GUI driver never resyncs
4
================================================================================
5

    
6
Branch      : 11645a (task #11645), trunk base r16695
7
Files        : src/com/goldencode/p2j/ui/client/driver/web/WebClientProtocol.java
8
               src/com/goldencode/p2j/ui/client/driver/web/PushMessagesWorker.java
9
               src/com/goldencode/p2j/ui/client/gui/driver/web/GuiWebDriver.java
10
Severity     : MAJOR (functional) - confirmed in the challenge pass
11
Category     : regression introduced by this branch
12

    
13

    
14
1. PROBLEM
15
--------------------------------------------------------------------------------
16
WebClientProtocol.onConnect:480-486 now calls pushWorker.discardQueued(), throwing
17
away all pending server-to-browser output. The in-code justification is:
18

    
19
    "initRemoteClient below resyncs"
20

    
21
That is true for CHUI and false for GUI. GuiWebDriver.initRemoteClient() is
22
literally a no-op:
23

    
24
    GuiWebDriver.java:1033-1036      // no-op
25

    
26
Only ChuiWebSimulator.initRemoteClient() resyncs (clearCache(),
27
sendColorPalette(), sendSwitchMode(), triggerRepaint(true) -
28
ChuiWebSimulator.java:200-215).
29

    
30
So on the GUI driver every draw batch produced during an outage is silently
31
dropped and nothing regenerates it. The canvas is left permanently stale.
32

    
33

    
34
2. EVIDENCE / TRIGGER
35
--------------------------------------------------------------------------------
36
Trigger: a half-open socket. The browser's silence watchdog
37
(checkLostPings / onServerSilenceExpired -> attemptToRestoreConnection() ->
38
connect() -> new WebSocket(url)) reconnects before Jetty delivers onClose for the
39
dead session, so onConnect takes its session-replacement branch:
40

    
41
    WebClientProtocol.java:464   if (this.session != null && this.session.isOpen())
42

    
43
with pushWorker != null and a full backlog - the worker is parked in
44
latch.await() on the dead socket while producers keep enqueuing up to the 32 MB
45
high-water mark.
46

    
47
Two facts make this specifically bad rather than merely lossy:
48

    
49
  a) The discard has effect ONLY on this path. On a first connect pushWorker is
50
     null; after a real onClose, stopPushWorker() nulls it. So on every other
51
     reconnect discardQueued() is a no-op. The change therefore takes effect
52
     exactly where it is unsafe.
53

    
54
  b) The page was never reloaded, so the browser sends MSG_PING_PONG, not
55
     MSG_PAGE_LOADED (p2j.socket.js:5762). Nothing else requests a repaint. The
56
     canvas is intact and stale, and there is no path that regenerates the lost
57
     drawing operations.
58

    
59
Pre-change behaviour: the backlog survived and drained onto the new socket via
60
startPushWorker -> setCurrentSession (which clears sessionInvalid and releases the
61
worker parked in getCurrentSession); failed sends were re-queued at the head
62
(PushMessagesWorker.java:659-668).
63

    
64

    
65
3. ROOT CAUSE
66
--------------------------------------------------------------------------------
67
The discard was added to protect against cross-generation MSG_PARTIAL tails
68
arriving on a new connection. But that hazard is already handled elsewhere:
69

    
70
  - queuePartial's generation check (WebClientProtocol.java:1063-1067) refuses to
71
    enqueue a piece whose generation no longer matches;
72
  - the client calls resetChunkTransfers() in handleOpenEvent, and collectChunk
73
    drops any piece arriving without PARTIAL_FLAG_FIRST.
74

    
75
So discarding *whole* messages buys no safety that is not already provided, while
76
costing all pending GUI output. Note also that pre-change the queue held only
77
whole, self-contained messages - streamed transfers went through
78
session.sendPartialBinary under sendLock, bypassing the queue - so replaying the
79
retained backlog was well-formed. Routing MSG_PARTIAL through the same queue is
80
what created the appearance of a new hazard.
81

    
82

    
83
4. PROPOSED FIX
84
--------------------------------------------------------------------------------
85
RECOMMENDED - drop the discard, keep the generation guard.
86

    
87
  Remove the pushWorker.discardQueued() call from onConnect and restore the
88
  pre-change retain-and-replay behaviour, relying on the mechanisms in section 3
89
  for partial-transfer safety. Concretely, in onConnect's session-replacement
90
  branch:
91

    
92
    - delete the discardQueued() call and the misleading
93
      "initRemoteClient below resyncs" comment;
94
    - replace the comment with one stating why the backlog is retained: whole
95
      messages are connection-agnostic, and stale partial pieces are already
96
      excluded by the generation check in queuePartial plus resetChunkTransfers()
97
      on the client.
98

    
99
  Keep discardQueued() as a method only if another caller needs it; otherwise
100
  remove it so it cannot be reintroduced casually. If it is kept, its accounting
101
  race must be fixed first - see proposal-major-performance-1, which is a
102
  prerequisite for any retained use of this method.
103

    
104
ALTERNATIVE A - keep the discard, make the GUI actually resync.
105

    
106
  Implement GuiWebDriver.initRemoteClient() to force a full repaint (invalidate
107
  all windows and retransmit). This is the semantically complete option but it is
108
  considerably larger, it makes every stale-session reconnect pay a full-screen
109
  repaint, and drawing ops are not the only queued state (font definitions,
110
  streamed images, cursor state) - so a repaint alone may not be sufficient. Only
111
  worth doing if a resync is wanted for its own sake.
112

    
113
ALTERNATIVE B - discard selectively.
114

    
115
  Tag each queued message with the connectionGeneration at push time and have
116
  discardQueued(generation) remove only stale MSG_PARTIAL pieces, retaining whole
117
  messages. This preserves the stated intent with no output loss, at the cost of
118
  one int per queue entry. Reasonable if the team wants belt-and-braces on top of
119
  the existing generation guard.
120

    
121
Recommendation: the RECOMMENDED option. It is the smallest change, restores known-
122
good behaviour, and the hazard it removes protection against is already covered
123
twice over. ALTERNATIVE B is the fallback if reviewers want an explicit discard
124
retained.
125

    
126

    
127
5. RELATED DEFECT THAT MUST BE SETTLED TOGETHER
128
--------------------------------------------------------------------------------
129
discardQueued() also has an accounting race and re-queues one stale message after
130
clearing the deque (see proposal-major-performance-1). If the RECOMMENDED option
131
is taken, that defect becomes unreachable from onConnect but the method remains
132
broken; fix or remove it in the same change so it is not reintroduced later.
133

    
134

    
135
6. RISK
136
--------------------------------------------------------------------------------
137
Low-to-moderate. Removing the discard restores the behaviour that shipped before
138
this branch, so the risk profile is "back to known state" rather than new.
139

    
140
Watch for: a stale message at the head of the queue being written to the new socket
141
ahead of anything the reconnect sends. That is pre-existing behaviour and was
142
correct before, but confirm it still is now that MSG_PARTIAL shares the queue -
143
specifically that a partial piece enqueued under the old generation cannot be at
144
the head when the new session starts draining. The generation check is at enqueue
145
time, so a piece enqueued just before the bump can still be queued; verify the
146
client's orphan-piece drop absorbs it (it should - the piece will lack
147
PARTIAL_FLAG_FIRST from the new page's point of view after resetChunkTransfers()).
148

    
149

    
150
7. TEST PLAN
151
--------------------------------------------------------------------------------
152
1. GUI web client. Force a half-open socket (drop packets on the websocket port
153
   with the process still alive, or SIGSTOP the browser tab's network) long enough
154
   for the client watchdog to reconnect but short of Jetty's idle close. Drive
155
   drawing activity throughout. Expect after reconnect: the canvas is current, no
156
   missing regions.
157
2. Same scenario on the CHUI web client. Expect: unchanged behaviour (it resyncs
158
   via triggerRepaint either way).
159
3. Reconnect while a streamed image (defineImageStreamed) is mid-transfer. Expect:
160
   no corrupt image, no orphaned transfer, either a clean re-send or a dropped
161
   transfer - but never a partially applied one.
162
4. Reconnect while a custom font is being streamed. Expect: no hung drawing thread
163
   (this is the MINOR queuePartial/createFont finding; verify it did not get worse).
164
5. Confirm queuedBytes returns to 0 after activity settles, both before and after
165
   a reconnect (guards against the accounting skew in performance-1).