Browse Source

Avoid locking in WebSocket session "close" callback

When processing a "close" notification from the server make an effort
to cancel any outstanding heartbeat but avoid going as far as acquiring
the responseLock since the server itself may already hold a lock of its
own leading to a potential deadlock.

The heartbeat task is now also further protected with an isClosed()
check in case the heartbeat does not get cancelled in a concurrent
scenario.

Issue: SPR-14917
pull/1244/head
Rossen Stoyanchev 8 years ago
parent
commit
d201a222b8
  1. 9
      spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/AbstractSockJsSession.java
  2. 14
      spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/SockJsSessionTests.java

9
spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/AbstractSockJsSession.java

@ -396,7 +396,12 @@ public abstract class AbstractSockJsSession implements SockJsSession { @@ -396,7 +396,12 @@ public abstract class AbstractSockJsSession implements SockJsSession {
if (!isClosed()) {
try {
updateLastActiveTime();
cancelHeartbeat();
// Avoid cancelHeartbeat() and responseLock within server "close" callback
ScheduledFuture<?> future = this.heartbeatFuture;
if (future != null) {
this.heartbeatFuture = null;
future.cancel(false);
}
}
finally {
this.state = State.CLOSED;
@ -446,7 +451,7 @@ public abstract class AbstractSockJsSession implements SockJsSession { @@ -446,7 +451,7 @@ public abstract class AbstractSockJsSession implements SockJsSession {
@Override
public void run() {
synchronized (responseLock) {
if (!this.expired) {
if (!this.expired && !isClosed()) {
try {
sendHeartbeat();
}

14
spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/SockJsSessionTests.java

@ -31,8 +31,17 @@ import org.springframework.web.socket.sockjs.SockJsMessageDeliveryException; @@ -31,8 +31,17 @@ import org.springframework.web.socket.sockjs.SockJsMessageDeliveryException;
import org.springframework.web.socket.sockjs.SockJsTransportFailureException;
import org.springframework.web.socket.sockjs.frame.SockJsFrame;
import static org.junit.Assert.*;
import static org.mockito.BDDMockito.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.BDDMockito.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.verify;
import static org.mockito.BDDMockito.verifyNoMoreInteractions;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.BDDMockito.willThrow;
/**
* Test fixture for {@link AbstractSockJsSession}.
@ -130,7 +139,6 @@ public class SockJsSessionTests extends AbstractSockJsSessionTests<TestSockJsSes @@ -130,7 +139,6 @@ public class SockJsSessionTests extends AbstractSockJsSessionTests<TestSockJsSes
assertClosed();
assertEquals(1, this.session.getNumberOfLastActiveTimeUpdates());
assertTrue(this.session.didCancelHeartbeat());
verify(this.webSocketHandler).afterConnectionClosed(this.session, CloseStatus.GOING_AWAY);
}

Loading…
Cancel
Save