IGNITE-15178 Check for clashing message group IDs in AbstractMessagingService (#242)
diff --git a/modules/network-api/src/main/java/org/apache/ignite/network/AbstractMessagingService.java b/modules/network-api/src/main/java/org/apache/ignite/network/AbstractMessagingService.java
index 33df0ed..f65762f 100644
--- a/modules/network-api/src/main/java/org/apache/ignite/network/AbstractMessagingService.java
+++ b/modules/network-api/src/main/java/org/apache/ignite/network/AbstractMessagingService.java
@@ -27,22 +27,46 @@
* Base class for {@link MessagingService} implementations.
*/
public abstract class AbstractMessagingService implements MessagingService {
+ /**
+ * Class holding a pair of a message group class and corresponding handlers.
+ */
+ private static class Handler {
+ /** */
+ final Class<?> messageGroup;
+
+ /** */
+ final List<NetworkMessageHandler> handlers;
+
+ /** */
+ Handler(Class<?> messageGroup, List<NetworkMessageHandler> handlers) {
+ this.messageGroup = messageGroup;
+ this.handlers = handlers;
+ }
+ }
+
/** Mapping from group type (array index) to a list of registered message handlers. */
- private final AtomicReferenceArray<List<NetworkMessageHandler>> handlersByGroupType =
- new AtomicReferenceArray<>(Short.MAX_VALUE + 1);
+ private final AtomicReferenceArray<Handler> handlersByGroupType = new AtomicReferenceArray<>(Short.MAX_VALUE + 1);
/** {@inheritDoc} */
@Override public void addMessageHandler(Class<?> messageGroup, NetworkMessageHandler handler) {
- handlersByGroupType.getAndUpdate(getMessageGroupType(messageGroup), handlers -> {
- if (handlers == null)
- return List.of(handler);
+ handlersByGroupType.getAndUpdate(getMessageGroupType(messageGroup), oldHandler -> {
+ if (oldHandler == null)
+ return new Handler(messageGroup, List.of(handler));
- var result = new ArrayList<NetworkMessageHandler>(handlers.size() + 1);
+ if (oldHandler.messageGroup != messageGroup) {
+ throw new IllegalArgumentException(String.format(
+ "Handlers are already registered for a message group with the same group ID " +
+ "but different class. Group ID: %d, given message group: %s, existing message group: %s",
+ getMessageGroupType(messageGroup), messageGroup, oldHandler.messageGroup
+ ));
+ }
- result.addAll(handlers);
- result.add(handler);
+ var handlers = new ArrayList<NetworkMessageHandler>(oldHandler.handlers.size() + 1);
- return result;
+ handlers.addAll(oldHandler.handlers);
+ handlers.add(handler);
+
+ return new Handler(messageGroup, handlers);
});
}
@@ -67,8 +91,8 @@
protected final Collection<NetworkMessageHandler> getMessageHandlers(short groupType) {
assert groupType >= 0 : "Group type must not be negative";
- List<NetworkMessageHandler> result = handlersByGroupType.get(groupType);
+ Handler result = handlersByGroupType.get(groupType);
- return result == null ? List.of() : result;
+ return result == null ? List.of() : result.handlers;
}
}
diff --git a/modules/network-api/src/main/java/org/apache/ignite/network/MessagingService.java b/modules/network-api/src/main/java/org/apache/ignite/network/MessagingService.java
index c524856..6ee2cfb 100644
--- a/modules/network-api/src/main/java/org/apache/ignite/network/MessagingService.java
+++ b/modules/network-api/src/main/java/org/apache/ignite/network/MessagingService.java
@@ -97,6 +97,8 @@
*
* @param messageGroup Message group descriptor.
* @param handler Message handler.
+ * @throws IllegalArgumentException If some handlers have already been registered for a different message group
+ * class that has the same ID as the given {@code messageGroup}.
*/
void addMessageHandler(Class<?> messageGroup, NetworkMessageHandler handler);
}
diff --git a/modules/network/src/test/java/org/apache/ignite/network/AbstractMessagingServiceTest.java b/modules/network/src/test/java/org/apache/ignite/network/AbstractMessagingServiceTest.java
new file mode 100644
index 0000000..2e2e166
--- /dev/null
+++ b/modules/network/src/test/java/org/apache/ignite/network/AbstractMessagingServiceTest.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.network;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReferenceArray;
+import org.apache.ignite.network.annotations.MessageGroup;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.withSettings;
+
+/**
+ * Test suite for the {@link AbstractMessagingService} class.
+ */
+public class AbstractMessagingServiceTest {
+ /**
+ * Tests a situation when multiple modules declare message group descriptors with the same group ID.
+ * Adding handlers for both of these groups should result in an exception being thrown.
+ * <p>
+ * Since we can't declare multiple message groups in a single module, we have to reside to hacks using reflection.
+ */
+ @Test
+ public void testGroupIdClash() throws Exception {
+ var messagingService = mock(
+ AbstractMessagingService.class,
+ withSettings().useConstructor().defaultAnswer(CALLS_REAL_METHODS)
+ );
+
+ // get the static inner class that is stored inside the handlers list
+ Class<?> handlerClass = AbstractMessagingService.class.getDeclaredClasses()[0];
+
+ Constructor<?> constructor = handlerClass.getDeclaredConstructor(Class.class, List.class);
+ constructor.setAccessible(true);
+ // create a dummy handler
+ Object dummyHandler = constructor.newInstance(Object.class, List.of());
+
+ short groupType = TestMessageTypes.class.getAnnotation(MessageGroup.class).groupType();
+
+ // get the array of handlers and inject the dummy handler
+ Field handlersField = AbstractMessagingService.class.getDeclaredField("handlersByGroupType");
+ handlersField.setAccessible(true);
+
+ var handlers = (AtomicReferenceArray) handlersField.get(messagingService);
+ // use the groupType of the TestMessageTypes class to get a clash
+ handlers.set(groupType, dummyHandler);
+
+ Exception e = assertThrows(
+ IllegalArgumentException.class,
+ () -> messagingService.addMessageHandler(TestMessageTypes.class, (m, s, c) -> {})
+ );
+
+ assertThat(e.getMessage(), startsWith("Handlers are already registered"));
+ }
+}