Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import java.util.Map.Entry;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
Expand All @@ -71,7 +72,7 @@ public class ConnectionManager {
// noteId -> connection
final Map<String, Set<NotebookSocket>> noteSocketMap = Metrics.gaugeMapSize("zeppelin_note_sockets", Tags.empty(), new HashMap<>());
// user -> connection
final Map<String, Queue<NotebookSocket>> userSocketMap = Metrics.gaugeMapSize("zeppelin_user_sockets", Tags.empty(), new HashMap<>());
final Map<String, Queue<NotebookSocket>> userSocketMap = Metrics.gaugeMapSize("zeppelin_user_sockets", Tags.empty(), new ConcurrentHashMap<>());

/**
* This is a special endpoint in the notebook websocket, Every connection in this Queue
Expand Down Expand Up @@ -153,24 +154,23 @@ public void removeConnectionFromAllNote(NotebookSocket socket) {
public void addUserConnection(String user, NotebookSocket conn) {
LOGGER.debug("Add user connection {} for user: {}", conn, user);
conn.setUser(user);
if (userSocketMap.containsKey(user)) {
userSocketMap.get(user).add(conn);
} else {
Queue<NotebookSocket> socketQueue = new ConcurrentLinkedQueue<>();
socketQueue.add(conn);
userSocketMap.put(user, socketQueue);
}
userSocketMap.compute(user, (k, connections) -> {
Queue<NotebookSocket> queue =
(connections == null) ? new ConcurrentLinkedQueue<>() : connections;
queue.add(conn);
return queue;
});
}

public void removeUserConnection(String user, NotebookSocket conn) {
LOGGER.debug("Remove user connection {} for user: {}", conn, user);
if (userSocketMap.containsKey(user)) {
Queue<NotebookSocket> connections = userSocketMap.get(user);
boolean[] wasPresent = {false};
userSocketMap.computeIfPresent(user, (k, connections) -> {
wasPresent[0] = true;
connections.remove(conn);
if (connections.isEmpty()) {
userSocketMap.remove(user);
}
} else {
return connections.isEmpty() ? null : connections;
});
if (!wasPresent[0]) {
LOGGER.warn("Closing connection that is absent in user connections");
}
}
Expand Down Expand Up @@ -330,12 +330,13 @@ public Set<String> getConnectedUsers() {


public void multicastToUser(String user, Message m) {
if (!userSocketMap.containsKey(user)) {
Queue<NotebookSocket> connections = userSocketMap.get(user);
if (connections == null) {
LOGGER.warn("Multicasting to user {} that is not in connections map", user);
return;
}

for (NotebookSocket conn : userSocketMap.get(user)) {
for (NotebookSocket conn : connections) {
unicast(m, conn);
}
}
Expand All @@ -354,12 +355,13 @@ public void unicastParagraph(Note note, Paragraph p, String user, String msgId)
return;
}

if (!userSocketMap.containsKey(user)) {
Queue<NotebookSocket> connections = userSocketMap.get(user);
if (connections == null) {
LOGGER.warn("Failed to send unicast. user {} that is not in connections map", user);
return;
}

for (NotebookSocket conn : userSocketMap.get(user)) {
for (NotebookSocket conn : connections) {
Message m = new Message(Message.OP.PARAGRAPH).withMsgId(msgId).put("paragraph", p);
unicast(m, conn);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,22 @@

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.zeppelin.conf.ZeppelinConfiguration;
import org.apache.zeppelin.notebook.AuthorizationService;
Expand Down Expand Up @@ -142,6 +148,122 @@ void removeWatcherConnectionConcurrentTest() throws InterruptedException {
assertEquals(0, manager.watcherSockets.size());
}

@Test
void userSocketMapConcurrentAccessTest() throws InterruptedException {
AuthorizationService authService = mock(AuthorizationService.class);
when(authService.getRoles(anyString())).thenAnswer(invocation -> new HashSet<>());
ConnectionManager manager = new ConnectionManager(authService, ZeppelinConfiguration.load());

int writerCount = 8;
int readerCount = 2;
int iterations = 1000;

List<String> users = new ArrayList<>();
List<NotebookSocket> sockets = new ArrayList<>();
for (int i = 0; i < writerCount; i++) {
users.add("user-" + i);
sockets.add(mock(NotebookSocket.class));
}

AtomicReference<Throwable> failure = new AtomicReference<>();
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch doneLatch = new CountDownLatch(writerCount + readerCount);
ExecutorService executor = Executors.newFixedThreadPool(writerCount + readerCount);

for (int i = 0; i < writerCount; i++) {
String user = users.get(i);
NotebookSocket socket = sockets.get(i);
executor.submit(() -> {
try {
startLatch.await();
for (int j = 0; j < iterations; j++) {
manager.addUserConnection(user, socket);
manager.removeUserConnection(user, socket);
}
} catch (Throwable t) {
failure.compareAndSet(null, t);
} finally {
doneLatch.countDown();
}
});
}

for (int i = 0; i < readerCount; i++) {
executor.submit(() -> {
try {
startLatch.await();
for (int j = 0; j < iterations; j++) {
manager.forAllUsers((user, userAndRoles) -> { });
}
} catch (Throwable t) {
failure.compareAndSet(null, t);
} finally {
doneLatch.countDown();
}
});
}

startLatch.countDown();
assertTrue(doneLatch.await(30, TimeUnit.SECONDS));
executor.shutdown();

assertNull(failure.get(),
"Concurrent access to userSocketMap should not throw, but got: " + failure.get());
}

@Test
void userSocketMapConcurrentAddPreservesAllConnectionsTest() throws InterruptedException {
AuthorizationService authService = mock(AuthorizationService.class);
ConnectionManager manager = new ConnectionManager(authService, ZeppelinConfiguration.load());

String user = "shared-user";
int threadCount = 16;
int iterationsPerThread = 500;

AtomicReference<Throwable> failure = new AtomicReference<>();
List<NotebookSocket> addedSockets = new CopyOnWriteArrayList<>();

ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch doneLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
executor.submit(() -> {
try {
startLatch.await();
for (int j = 0; j < iterationsPerThread; j++) {
NotebookSocket socket = mock(NotebookSocket.class);
manager.addUserConnection(user, socket);
addedSockets.add(socket);
}
} catch (Throwable t) {
failure.compareAndSet(null, t);
} finally {
doneLatch.countDown();
}
});
}

startLatch.countDown();
assertTrue(doneLatch.await(30, TimeUnit.SECONDS));
executor.shutdown();

assertNull(failure.get(), "Concurrent add should not throw, but got: " + failure.get());

Queue<NotebookSocket> finalConnections = manager.userSocketMap.get(user);
assertEquals(threadCount * iterationsPerThread, addedSockets.size());
List<NotebookSocket> missing = new ArrayList<>();
for (NotebookSocket socket : addedSockets) {
if (finalConnections == null || !finalConnections.contains(socket)) {
missing.add(socket);
}
}

assertTrue(missing.isEmpty(),
missing.size() + " of " + addedSockets.size()
+ " concurrently added connections were lost from userSocketMap");
assertEquals(addedSockets.size(), finalConnections.size());
}

@Test
void switchConnectionToWatcherAndRemove() {
AuthorizationService authService = mock(AuthorizationService.class);
Expand Down
Loading