@@ -52,7 +52,9 @@ public class WebSocketTransport implements ITransport {
5252 private ConnectListener connectListener ;
5353 private WebSocketClient webSocketClient ;
5454 private final WebSocketEngine webSocketEngine ;
55+ private WebSocketHandler webSocketHandler ;
5556 private boolean activityCheckTurnedOff = false ;
57+ private boolean connectHasBeenCalled = false ;
5658
5759 /******************
5860 * protected constructor
@@ -94,8 +96,13 @@ private static WebSocketEngine createWebSocketEngine(TransportParams params) {
9496 * ITransport methods
9597 ******************/
9698
99+ /**
100+ * Connect is called once when we create transport;
101+ * after transport is closed, we never call `connect` again
102+ */
97103 @ Override
98104 public void connect (ConnectListener connectListener ) {
105+ ensureConnectCalledOnce ();
99106 this .connectListener = connectListener ;
100107 try {
101108 boolean isTls = params .options .tls ;
@@ -107,9 +114,8 @@ public void connect(ConnectListener connectListener) {
107114 wsUri = HttpUtils .encodeParams (wsUri , connectParams );
108115
109116 Log .d (TAG , "connect(); wsUri = " + wsUri );
110- synchronized (this ) {
111- webSocketClient = this .webSocketEngine .create (wsUri , new WebSocketHandler (this ::receive ));
112- }
117+ webSocketHandler = new WebSocketHandler (this ::receive );
118+ webSocketClient = this .webSocketEngine .create (wsUri , webSocketHandler );
113119 webSocketClient .connect ();
114120 } catch (AblyException e ) {
115121 Log .e (TAG , "Unexpected exception attempting connection; wsUri = " + wsUri , e );
@@ -120,14 +126,36 @@ public void connect(ConnectListener connectListener) {
120126 }
121127 }
122128
129+ /**
130+ * `connect()` can't be called more than once
131+ */
132+ private synchronized void ensureConnectCalledOnce () {
133+ if (connectHasBeenCalled ) throw new IllegalStateException ("WebSocketTransport is already initialized" );
134+ connectHasBeenCalled = true ;
135+ }
136+
123137 @ Override
124138 public void close () {
125139 Log .d (TAG , "close()" );
126- synchronized (this ) {
127- if (webSocketClient != null ) {
128- webSocketClient .close ();
129- webSocketClient = null ;
130- }
140+ // Take local snapshots of the shared references. Callback threads (e.g., onClose)
141+ // may concurrently set these fields to null.
142+ //
143+ // Intentionally avoid synchronizing here:
144+ // - The WebSocket library may invoke our WebSocketHandler while holding its own
145+ // internal locks.
146+ // - If close() also acquired a lock on WebSocketTransport, we could invert the
147+ // lock order and create a circular wait (deadlock): close() waits for the WS
148+ // library to release its lock, while the WS library waits for a lock on
149+ // WebSocketTransport.
150+ final WebSocketClient client = webSocketClient ;
151+ final WebSocketHandler handler = webSocketHandler ;
152+ if (client != null && handler != null ) {
153+ // Record activity so the activity timer remains armed. If a graceful close
154+ // stalls, the timer can detect inactivity and force-cancel the socket.
155+ handler .flagActivity ();
156+ client .close ();
157+ } else {
158+ Log .w (TAG , "close() called on uninitialized or already closed transport" );
131159 }
132160 }
133161
@@ -217,10 +245,15 @@ class WebSocketHandler implements WebSocketListener {
217245 * WsClient private members
218246 ***************************/
219247
220- private Timer timer = new Timer ();
248+ private final Timer timer = new Timer ();
221249 private TimerTask activityTimerTask = null ;
222250 private long lastActivityTime ;
223251
252+ /**
253+ * Monitor for activity timer events
254+ */
255+ private final Object activityTimerMonitor = new Object ();
256+
224257 WebSocketHandler (WebSocketReceiver receiver ) {
225258 this .receiver = receiver ;
226259 }
@@ -318,66 +351,63 @@ public void onOldJavaVersionDetected(Throwable throwable) {
318351 Log .w (TAG , "Error when trying to set SSL parameters, most likely due to an old Java API version" , throwable );
319352 }
320353
321- private synchronized void dispose () {
322- /* dispose timer */
323- try {
324- timer .cancel ();
325- timer = null ;
326- } catch (IllegalStateException e ) {
327- }
354+ private void dispose () {
355+ timer .cancel ();
328356 }
329357
330- private synchronized void flagActivity () {
358+ private void flagActivity () {
331359 lastActivityTime = System .currentTimeMillis ();
332360 connectionManager .setLastActivity (lastActivityTime );
333- if (activityTimerTask == null && connectionManager .maxIdleInterval != 0 && !activityCheckTurnedOff ) {
334- /* No timer currently running because previously there was no
335- * maxIdleInterval configured, but now there is a
336- * maxIdleInterval configured. Call checkActivity so a timer
337- * gets started. This happens when flagActivity gets called
338- * just after processing the connect message that configures
339- * maxIdleInterval. */
340- checkActivity ();
361+
362+ if (connectionManager .maxIdleInterval == 0 ) {
363+ Log .v (TAG , "checkActivity: turned off because maxIdleInterval is 0" );
364+ return ;
341365 }
366+
367+ if (activityCheckTurnedOff ) {
368+ Log .v (TAG , "checkActivity: turned off for test purpose" );
369+ return ;
370+ }
371+
372+ checkActivity ();
342373 }
343374
344- private synchronized void checkActivity () {
375+ private void checkActivity () {
345376 long timeout = getActivityTimeout ();
377+
346378 if (timeout == 0 ) {
347379 Log .v (TAG , "checkActivity: infinite timeout" );
348380 return ;
349381 }
350382
351- // Check if timer already running
352- if (activityTimerTask != null ) {
353- return ;
383+ synchronized (activityTimerMonitor ) {
384+ // Check if timer already running
385+ if (activityTimerTask == null ) {
386+ // Start the activity timer task
387+ startActivityTimer (timeout + 100 );
388+ }
354389 }
355-
356- // Start the activity timer task
357- startActivityTimer (timeout + 100 );
358390 }
359391
360- private synchronized void startActivityTimer (long timeout ) {
361- if (activityTimerTask == null ) {
362- schedule ((activityTimerTask = new TimerTask () {
363- public void run () {
364- try {
365- onActivityTimerExpiry ();
366- } catch (Throwable t ) {
367- Log .e (TAG , "Unexpected exception in activity timer handler" , t );
368- }
392+ private void startActivityTimer (long timeout ) {
393+ activityTimerTask = new TimerTask () {
394+ public void run () {
395+ try {
396+ onActivityTimerExpiry ();
397+ } catch (Exception exception ) {
398+ Log .e (TAG , "Unexpected exception in activity timer handler" , exception );
399+ webSocketClient .cancel (ABNORMAL_CLOSE , "Activity timer closed unexpectedly" );
369400 }
370- }), timeout );
371- }
401+ }
402+ };
403+ schedule (activityTimerTask , timeout );
372404 }
373405
374- private synchronized void schedule (TimerTask task , long delay ) {
375- if (timer != null ) {
376- try {
377- timer .schedule (task , delay );
378- } catch (IllegalStateException ise ) {
379- Log .e (TAG , "Unexpected exception scheduling activity timer" , ise );
380- }
406+ private void schedule (TimerTask task , long delay ) {
407+ try {
408+ timer .schedule (task , delay );
409+ } catch (IllegalStateException ise ) {
410+ Log .w (TAG , "Timer has already been canceled" , ise );
381411 }
382412 }
383413
@@ -392,7 +422,7 @@ private void onActivityTimerExpiry() {
392422 return ;
393423 }
394424
395- synchronized (this ) {
425+ synchronized (activityTimerMonitor ) {
396426 activityTimerTask = null ;
397427 // Otherwise, we've had some activity, restart the timer for the next timeout
398428 Log .v (TAG , "onActivityTimerExpiry: ok" );
0 commit comments