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