Modernize RoleChanged/LeaderStateChanged 16/116016/8
authorRobert Varga <robert.varga@pantheon.tech>
Sun, 23 Mar 2025 04:06:30 +0000 (05:06 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Sun, 23 Mar 2025 11:00:01 +0000 (12:00 +0100)
These are pure DTOs, use a record for that. While we're here, also make
sure to require memberId()/newRole(). The situation is a bit
complicated, because we use subclassing to carry more data.

JIRA: CONTROLLER-2134
Change-Id: I37a486dd75c1a313161908cfb287e61e18a5a401
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
20 files changed:
opendaylight/md-sal/sal-akka-raft/src/main/java/org/opendaylight/controller/cluster/raft/RaftActor.java
opendaylight/md-sal/sal-akka-raft/src/test/java/org/opendaylight/controller/cluster/raft/IsolationScenarioTest.java
opendaylight/md-sal/sal-akka-raft/src/test/java/org/opendaylight/controller/cluster/raft/LeadershipTransferIntegrationTest.java
opendaylight/md-sal/sal-akka-raft/src/test/java/org/opendaylight/controller/cluster/raft/NonVotingFollowerIntegrationTest.java
opendaylight/md-sal/sal-akka-raft/src/test/java/org/opendaylight/controller/cluster/raft/PreLeaderScenarioTest.java
opendaylight/md-sal/sal-akka-raft/src/test/java/org/opendaylight/controller/cluster/raft/RaftActorTest.java
opendaylight/md-sal/sal-akka-raft/src/test/java/org/opendaylight/controller/cluster/raft/utils/MessageCollectorActor.java
opendaylight/md-sal/sal-clustering-commons/src/main/java/org/opendaylight/controller/cluster/notifications/DefaultLeaderStateChanged.java [new file with mode: 0644]
opendaylight/md-sal/sal-clustering-commons/src/main/java/org/opendaylight/controller/cluster/notifications/ForwadingLeaderStateChanged.java [new file with mode: 0644]
opendaylight/md-sal/sal-clustering-commons/src/main/java/org/opendaylight/controller/cluster/notifications/LeaderStateChanged.java
opendaylight/md-sal/sal-clustering-commons/src/main/java/org/opendaylight/controller/cluster/notifications/MemberNotication.java [new file with mode: 0644]
opendaylight/md-sal/sal-clustering-commons/src/main/java/org/opendaylight/controller/cluster/notifications/RegisterRoleChangeListenerReply.java
opendaylight/md-sal/sal-clustering-commons/src/main/java/org/opendaylight/controller/cluster/notifications/RoleChangeNotifier.java
opendaylight/md-sal/sal-clustering-commons/src/main/java/org/opendaylight/controller/cluster/notifications/RoleChanged.java
opendaylight/md-sal/sal-clustering-commons/src/test/java/org/opendaylight/controller/cluster/notifications/RoleChangeNotifierTest.java
opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/Shard.java
opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/messages/ShardLeaderStateChanged.java
opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/shardmanager/ShardManager.java
opendaylight/md-sal/sal-distributed-datastore/src/test/java/org/opendaylight/controller/cluster/datastore/RoleChangeNotifierTest.java
opendaylight/md-sal/sal-distributed-datastore/src/test/java/org/opendaylight/controller/cluster/datastore/shardmanager/ShardManagerTest.java

index 819ca19665825bc0578459260ccfc6702e1397c5..a1784fae8a31de3bf051930ffa6a326a8591b028 100644 (file)
@@ -35,6 +35,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.opendaylight.controller.cluster.common.actor.AbstractUntypedPersistentActor;
 import org.opendaylight.controller.cluster.mgmt.api.FollowerInfo;
+import org.opendaylight.controller.cluster.notifications.DefaultLeaderStateChanged;
 import org.opendaylight.controller.cluster.notifications.LeaderStateChanged;
 import org.opendaylight.controller.cluster.notifications.RoleChanged;
 import org.opendaylight.controller.cluster.raft.base.messages.ApplyState;
@@ -595,9 +596,15 @@ public abstract class RaftActor extends AbstractUntypedPersistentActor {
         self().tell(applyState, self());
     }
 
-    protected LeaderStateChanged newLeaderStateChanged(final String memberId, final String leaderId,
+    @NonNullByDefault
+    final LeaderStateChanged newLeaderStateChanged(final String memberId, final @Nullable String leaderId,
             final short leaderPayloadVersion) {
-        return new LeaderStateChanged(memberId, leaderId, leaderPayloadVersion);
+        return wrapLeaderStateChanged(new DefaultLeaderStateChanged(memberId, leaderId, leaderPayloadVersion));
+    }
+
+    @NonNullByDefault
+    protected LeaderStateChanged wrapLeaderStateChanged(final LeaderStateChanged change) {
+        return change;
     }
 
     @Override
index 39f90d846070d0eb4e30255dde82691112c2140b..3968afb07b09ed3fc0f293c00c79d8d10160c338 100644 (file)
@@ -79,7 +79,7 @@ public class IsolationScenarioTest extends AbstractRaftActorIntegrationTest {
         // The leader should transition to IsolatedLeader.
 
         expectFirstMatching(leaderNotifierActor, RoleChanged.class,
-            rc -> rc.getNewRole().equals(RaftState.IsolatedLeader.name()));
+            rc -> rc.newRole().equals(RaftState.IsolatedLeader.name()));
 
         forceElectionOnFollower1();
 
@@ -107,7 +107,7 @@ public class IsolationScenarioTest extends AbstractRaftActorIntegrationTest {
         // with a higher term.
 
         expectFirstMatching(leaderNotifierActor, RoleChanged.class,
-            rc -> rc.getNewRole().equals(RaftState.Follower.name()));
+            rc -> rc.newRole().equals(RaftState.Follower.name()));
 
         // The previous leader has a conflicting log entry at index 2 with a different term which should get
         // replaced by the new leader's index 1 entry.
@@ -187,7 +187,7 @@ public class IsolationScenarioTest extends AbstractRaftActorIntegrationTest {
         // The leader should transition to IsolatedLeader.
 
         expectFirstMatching(leaderNotifierActor, RoleChanged.class,
-            rc -> rc.getNewRole().equals(RaftState.IsolatedLeader.name()));
+            rc -> rc.newRole().equals(RaftState.IsolatedLeader.name()));
 
         forceElectionOnFollower1();
 
@@ -216,7 +216,7 @@ public class IsolationScenarioTest extends AbstractRaftActorIntegrationTest {
         // with a higher term.
 
         expectFirstMatching(leaderNotifierActor, RoleChanged.class,
-            rc -> rc.getNewRole().equals(RaftState.Follower.name()));
+            rc -> rc.newRole().equals(RaftState.Follower.name()));
 
         // The previous leader has a conflicting log entry at index 2 with a different term which should get
         // replaced by the new leader's entry.
@@ -315,7 +315,7 @@ public class IsolationScenarioTest extends AbstractRaftActorIntegrationTest {
         // The leader should transition to IsolatedLeader.
 
         expectFirstMatching(leaderNotifierActor, RoleChanged.class,
-            rc -> rc.getNewRole().equals(RaftState.IsolatedLeader.name()));
+            rc -> rc.newRole().equals(RaftState.IsolatedLeader.name()));
 
         forceElectionOnFollower1();
 
@@ -346,7 +346,7 @@ public class IsolationScenarioTest extends AbstractRaftActorIntegrationTest {
         // with a higher term.
 
         expectFirstMatching(leaderNotifierActor, RoleChanged.class,
-            rc -> rc.getNewRole().equals(RaftState.Follower.name()));
+            rc -> rc.newRole().equals(RaftState.Follower.name()));
 
         // The previous leader has conflicting log entries starting at index 2 with different terms which should get
         // replaced by the new leader's entries.
@@ -400,7 +400,7 @@ public class IsolationScenarioTest extends AbstractRaftActorIntegrationTest {
         follower1Actor.tell(TimeoutNow.INSTANCE, ActorRef.noSender());
 
         expectFirstMatching(follower1NotifierActor, RoleChanged.class,
-            rc -> rc.getNewRole().equals(RaftState.Leader.name()));
+            rc -> rc.newRole().equals(RaftState.Leader.name()));
 
         currentTerm = follower1Context.currentTerm();
     }
index 8ba9fcf3322168d2ae0cac9885958474a2e636a3..275115c83f619ff87594881261028c73492a8e77 100644 (file)
@@ -209,7 +209,7 @@ public class LeadershipTransferIntegrationTest extends AbstractRaftActorIntegrat
         Collections.reverse(leaderStateChanges);
         final var actual = leaderStateChanges.iterator();
         for (int i = expLeaderIds.length - 1; i >= 0; i--) {
-            assertEquals("getLeaderId", expLeaderIds[i], actual.next().getLeaderId());
+            assertEquals("getLeaderId", expLeaderIds[i], actual.next().leaderId());
         }
     }
 
index d8e81c248adda4399cd46393c91313081032cb5c..46c5eecfc8638b40e7947d9fdfbba8ab9608abac 100644 (file)
@@ -387,14 +387,13 @@ public class NonVotingFollowerIntegrationTest extends AbstractRaftActorIntegrati
 
         LeaderStateChanged leaderStateChanged = MessageCollectorActor.expectFirstMatching(roleChangeNotifier,
                 LeaderStateChanged.class);
-        assertEquals("getLeaderId", null, leaderStateChanged.getLeaderId());
+        assertEquals("leaderId", null, leaderStateChanged.leaderId());
 
         MessageCollectorActor.clearMessages(roleChangeNotifier);
         followerInstance.stopDropMessages(AppendEntries.class);
 
-        leaderStateChanged = MessageCollectorActor.expectFirstMatching(roleChangeNotifier,
-                LeaderStateChanged.class);
-        assertEquals("getLeaderId", leaderId, leaderStateChanged.getLeaderId());
+        leaderStateChanged = MessageCollectorActor.expectFirstMatching(roleChangeNotifier, LeaderStateChanged.class);
+        assertEquals("leaderId", leaderId, leaderStateChanged.leaderId());
     }
 
     private void createNewLeaderActor() {
index 1c47e29070b1278ddba78b82eef3e8f1eb79b4b0..06cdc0f022e77f6538591c0ff82b69c3f0d6d70d 100644 (file)
@@ -75,9 +75,9 @@ public class PreLeaderScenarioTest extends AbstractRaftActorIntegrationTest {
 
         // Verify the expected raft state changes. It should go to PreLeader since it has an uncommitted entry.
         List<RoleChanged> roleChange = expectMatching(follower1NotifierActor, RoleChanged.class, 3);
-        assertEquals("Role change 1", RaftState.Candidate.name(), roleChange.get(0).getNewRole());
-        assertEquals("Role change 2", RaftState.PreLeader.name(), roleChange.get(1).getNewRole());
-        assertEquals("Role change 3", RaftState.Leader.name(), roleChange.get(2).getNewRole());
+        assertEquals("Role change 1", RaftState.Candidate.name(), roleChange.get(0).newRole());
+        assertEquals("Role change 2", RaftState.PreLeader.name(), roleChange.get(1).newRole());
+        assertEquals("Role change 3", RaftState.Leader.name(), roleChange.get(2).newRole());
 
         final long previousTerm = currentTerm;
         currentTerm = follower1Context.currentTerm();
index 6f449b55b2fe855ff96d5041ba57a2a66ffa465d..ebbe28b11f5b198449d6e01b23fa715942500abd 100644 (file)
@@ -423,27 +423,27 @@ public class RaftActorTest extends AbstractActorTest {
 
         // check if the notifier got a role change from null to Follower
         RoleChanged raftRoleChanged = matches.get(0);
-        assertEquals(persistenceId, raftRoleChanged.getMemberId());
-        assertNull(raftRoleChanged.getOldRole());
-        assertEquals(RaftState.Follower.name(), raftRoleChanged.getNewRole());
+        assertEquals(persistenceId, raftRoleChanged.memberId());
+        assertNull(raftRoleChanged.oldRole());
+        assertEquals(RaftState.Follower.name(), raftRoleChanged.newRole());
 
         // check if the notifier got a role change from Follower to Candidate
         raftRoleChanged = matches.get(1);
-        assertEquals(persistenceId, raftRoleChanged.getMemberId());
-        assertEquals(RaftState.Follower.name(), raftRoleChanged.getOldRole());
-        assertEquals(RaftState.Candidate.name(), raftRoleChanged.getNewRole());
+        assertEquals(persistenceId, raftRoleChanged.memberId());
+        assertEquals(RaftState.Follower.name(), raftRoleChanged.oldRole());
+        assertEquals(RaftState.Candidate.name(), raftRoleChanged.newRole());
 
         // check if the notifier got a role change from Candidate to Leader
         raftRoleChanged = matches.get(2);
-        assertEquals(persistenceId, raftRoleChanged.getMemberId());
-        assertEquals(RaftState.Candidate.name(), raftRoleChanged.getOldRole());
-        assertEquals(RaftState.Leader.name(), raftRoleChanged.getNewRole());
+        assertEquals(persistenceId, raftRoleChanged.memberId());
+        assertEquals(RaftState.Candidate.name(), raftRoleChanged.oldRole());
+        assertEquals(RaftState.Leader.name(), raftRoleChanged.newRole());
 
         LeaderStateChanged leaderStateChange = MessageCollectorActor.expectFirstMatching(
                 notifierActor, LeaderStateChanged.class);
 
-        assertEquals(raftRoleChanged.getMemberId(), leaderStateChange.getLeaderId());
-        assertEquals(MockRaftActor.PAYLOAD_VERSION, leaderStateChange.getLeaderPayloadVersion());
+        assertEquals(raftRoleChanged.memberId(), leaderStateChange.leaderId());
+        assertEquals(MockRaftActor.PAYLOAD_VERSION, leaderStateChange.leaderPayloadVersion());
 
         MessageCollectorActor.clearMessages(notifierActor);
 
@@ -462,21 +462,21 @@ public class RaftActorTest extends AbstractActorTest {
         raftActor.newBehavior(follower);
 
         leaderStateChange = MessageCollectorActor.expectFirstMatching(notifierActor, LeaderStateChanged.class);
-        assertEquals(persistenceId, leaderStateChange.getMemberId());
-        assertEquals(null, leaderStateChange.getLeaderId());
+        assertEquals(persistenceId, leaderStateChange.memberId());
+        assertNull(leaderStateChange.leaderId());
 
         raftRoleChanged = MessageCollectorActor.expectFirstMatching(notifierActor, RoleChanged.class);
-        assertEquals(RaftState.Leader.name(), raftRoleChanged.getOldRole());
-        assertEquals(RaftState.Follower.name(), raftRoleChanged.getNewRole());
+        assertEquals(RaftState.Leader.name(), raftRoleChanged.oldRole());
+        assertEquals(RaftState.Follower.name(), raftRoleChanged.newRole());
 
         MessageCollectorActor.clearMessages(notifierActor);
 
         raftActor.handleCommand("any");
 
         leaderStateChange = MessageCollectorActor.expectFirstMatching(notifierActor, LeaderStateChanged.class);
-        assertEquals(persistenceId, leaderStateChange.getMemberId());
-        assertEquals(newLeaderId, leaderStateChange.getLeaderId());
-        assertEquals(newLeaderVersion, leaderStateChange.getLeaderPayloadVersion());
+        assertEquals(persistenceId, leaderStateChange.memberId());
+        assertEquals(newLeaderId, leaderStateChange.leaderId());
+        assertEquals(newLeaderVersion, leaderStateChange.leaderPayloadVersion());
 
         MessageCollectorActor.clearMessages(notifierActor);
 
@@ -517,15 +517,15 @@ public class RaftActorTest extends AbstractActorTest {
 
         // check if the notifier got a role change from null to Follower
         RoleChanged raftRoleChanged = matches.get(0);
-        assertEquals(persistenceId, raftRoleChanged.getMemberId());
-        assertNull(raftRoleChanged.getOldRole());
-        assertEquals(RaftState.Follower.name(), raftRoleChanged.getNewRole());
+        assertEquals(persistenceId, raftRoleChanged.memberId());
+        assertNull(raftRoleChanged.oldRole());
+        assertEquals(RaftState.Follower.name(), raftRoleChanged.newRole());
 
         // check if the notifier got a role change from Follower to Candidate
         raftRoleChanged = matches.get(1);
-        assertEquals(persistenceId, raftRoleChanged.getMemberId());
-        assertEquals(RaftState.Follower.name(), raftRoleChanged.getOldRole());
-        assertEquals(RaftState.Candidate.name(), raftRoleChanged.getNewRole());
+        assertEquals(persistenceId, raftRoleChanged.memberId());
+        assertEquals(RaftState.Follower.name(), raftRoleChanged.oldRole());
+        assertEquals(RaftState.Candidate.name(), raftRoleChanged.newRole());
     }
 
     @Test
@@ -1187,15 +1187,15 @@ public class RaftActorTest extends AbstractActorTest {
                 0L, -1L, (short)1), ActorRef.noSender());
         LeaderStateChanged leaderStateChange = MessageCollectorActor.expectFirstMatching(
                 notifierActor, LeaderStateChanged.class);
-        assertEquals("getLeaderId", "leader", leaderStateChange.getLeaderId());
+        assertEquals("leaderId", "leader", leaderStateChange.leaderId());
 
         MessageCollectorActor.clearMessages(notifierActor);
 
         raftActorRef.tell(new LeaderTransitioning("leader"), ActorRef.noSender());
 
         leaderStateChange = MessageCollectorActor.expectFirstMatching(notifierActor, LeaderStateChanged.class);
-        assertEquals("getMemberId", persistenceId, leaderStateChange.getMemberId());
-        assertEquals("getLeaderId", null, leaderStateChange.getLeaderId());
+        assertEquals(persistenceId, leaderStateChange.memberId());
+        assertNull(leaderStateChange.leaderId());
 
         TEST_LOG.info("testLeaderTransitioning ending");
     }
index 45ba6a010c566699ef480ce06138af65f93542cb..55c9a6e4bef10f446eb8fe4ae32f5785a001a888 100644 (file)
@@ -12,6 +12,7 @@ import com.google.common.base.Predicates;
 import com.google.common.base.Throwables;
 import com.google.common.collect.Iterables;
 import com.google.common.util.concurrent.Uninterruptibles;
+import java.lang.reflect.Modifier;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -80,10 +81,10 @@ public class MessageCollectorActor extends UntypedAbstractActor {
      * @return the first matching message
      */
     public static <T> T getFirstMatching(final ActorRef actor, final Class<T> clazz) {
-        List<Object> allMessages = getAllMessages(actor);
+        final var allMessages = getAllMessages(actor);
 
-        for (Object message : allMessages) {
-            if (message.getClass().equals(clazz)) {
+        for (var message : allMessages) {
+            if (matches(clazz, message)) {
                 return clazz.cast(message);
             }
         }
@@ -215,8 +216,8 @@ public class MessageCollectorActor extends UntypedAbstractActor {
 
         List<T> output = new ArrayList<>();
 
-        for (Object message : allMessages) {
-            if (message.getClass().equals(clazz)) {
+        for (var message : allMessages) {
+            if (matches(clazz, message)) {
                 output.add(clazz.cast(message));
             }
         }
@@ -224,6 +225,10 @@ public class MessageCollectorActor extends UntypedAbstractActor {
         return output;
     }
 
+    private static boolean matches(final Class<?> clazz, final Object obj) {
+        return clazz.equals(obj.getClass()) || Modifier.isAbstract(clazz.getModifiers()) && clazz.isInstance(obj);
+    }
+
     public static void waitUntilReady(final ActorRef actor) throws TimeoutException, InterruptedException {
         long timeout = 500;
         FiniteDuration duration = FiniteDuration.create(timeout, TimeUnit.MILLISECONDS);
diff --git a/opendaylight/md-sal/sal-clustering-commons/src/main/java/org/opendaylight/controller/cluster/notifications/DefaultLeaderStateChanged.java b/opendaylight/md-sal/sal-clustering-commons/src/main/java/org/opendaylight/controller/cluster/notifications/DefaultLeaderStateChanged.java
new file mode 100644 (file)
index 0000000..fb85c3b
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2025 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.controller.cluster.notifications;
+
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Default implementation of {@link LeaderStateChanged}.
+ */
+@NonNullByDefault
+public record DefaultLeaderStateChanged(
+        String memberId,
+        @Nullable String leaderId,
+        short leaderPayloadVersion) implements LeaderStateChanged {
+    public DefaultLeaderStateChanged {
+        requireNonNull(memberId);
+    }
+}
diff --git a/opendaylight/md-sal/sal-clustering-commons/src/main/java/org/opendaylight/controller/cluster/notifications/ForwadingLeaderStateChanged.java b/opendaylight/md-sal/sal-clustering-commons/src/main/java/org/opendaylight/controller/cluster/notifications/ForwadingLeaderStateChanged.java
new file mode 100644 (file)
index 0000000..e9656b0
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2025 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.controller.cluster.notifications;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Bese lass for extended {@link LeaderStateChanged} base implementations.
+ */
+@NonNullByDefault
+public abstract non-sealed class ForwadingLeaderStateChanged implements LeaderStateChanged {
+    @Override
+    public final String memberId() {
+        return  delegate().memberId();
+    }
+
+    @Override
+    public final @Nullable String leaderId() {
+        return delegate().leaderId();
+    }
+
+    @Override
+    public final short leaderPayloadVersion() {
+        return delegate().leaderPayloadVersion();
+    }
+
+    protected abstract LeaderStateChanged delegate();
+}
index 373823ef0ffeaf0ac37140aab1d437c7e4753979..5336184d5ea5b65446431d24b06ca30e0016304d 100644 (file)
@@ -7,9 +7,7 @@
  */
 package org.opendaylight.controller.cluster.notifications;
 
-import static java.util.Objects.requireNonNull;
-
-import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 
 /**
@@ -17,34 +15,11 @@ import org.eclipse.jdt.annotation.Nullable;
  *
  * @author Thomas Pantelis
  */
-public class LeaderStateChanged {
-    private final @NonNull String memberId;
-    private final @Nullable String leaderId;
-    private final short leaderPayloadVersion;
-
-    public LeaderStateChanged(final @NonNull String memberId, final @Nullable String leaderId,
-            final short leaderPayloadVersion) {
-        this.memberId = requireNonNull(memberId);
-        this.leaderId = leaderId;
-        this.leaderPayloadVersion = leaderPayloadVersion;
-    }
-
-    public @NonNull String getMemberId() {
-        return memberId;
-    }
-
-    public @Nullable String getLeaderId() {
-        return leaderId;
-    }
+@NonNullByDefault
+public sealed interface LeaderStateChanged extends MemberNotication
+        permits DefaultLeaderStateChanged, ForwadingLeaderStateChanged {
 
-    public short getLeaderPayloadVersion() {
-        return leaderPayloadVersion;
-    }
+    @Nullable String leaderId();
 
-    @Override
-    public String toString() {
-        return "LeaderStateChanged [memberId=" + memberId
-                + ", leaderId=" + leaderId
-                + ", leaderPayloadVersion=" + leaderPayloadVersion + "]";
-    }
+    short leaderPayloadVersion();
 }
diff --git a/opendaylight/md-sal/sal-clustering-commons/src/main/java/org/opendaylight/controller/cluster/notifications/MemberNotication.java b/opendaylight/md-sal/sal-clustering-commons/src/main/java/org/opendaylight/controller/cluster/notifications/MemberNotication.java
new file mode 100644 (file)
index 0000000..964a1e6
--- /dev/null
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2025 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.controller.cluster.notifications;
+
+/**
+ * A notification about a member.
+ */
+public sealed interface MemberNotication permits LeaderStateChanged, RoleChanged {
+
+
+    String memberId();
+}
index eea87ae9d7cf93cfb63bd49e0556323ba0489203..bbb6004c755c3ed57fc69241dbf28aaa7f829174 100644 (file)
@@ -14,6 +14,9 @@ import java.io.Serializable;
  * Reply message sent from a RoleChangeNotifier to the Role Change Listener.
  * Can be sent to a separate actor system and hence should be made serializable.
  */
+// FIXME: get a cookie or something?
+// FIXME: definitely final
 public class RegisterRoleChangeListenerReply implements Serializable {
+    @java.io.Serial
     private static final long serialVersionUID = -1972061601184451430L;
 }
index 7f0a47b250a4b9481f992b8b7f539756f7d7c916..6d0837a016b4fe27500cb74c9df67ef39473e166 100644 (file)
@@ -81,13 +81,12 @@ public class RoleChangeNotifier extends AbstractUntypedActor implements AutoClos
             // this message is sent by RaftActor. Notify registered listeners when this message is received.
 
             LOG.info("RoleChangeNotifier for {} , received role change from {} to {}", memberId,
-                roleChanged.getOldRole(), roleChanged.getNewRole());
+                roleChanged.oldRole(), roleChanged.newRole());
 
-            latestRoleChangeNotification =
-                new RoleChangeNotification(roleChanged.getMemberId(),
-                    roleChanged.getOldRole(), roleChanged.getNewRole());
+            latestRoleChangeNotification = new RoleChangeNotification(roleChanged.memberId(), roleChanged.oldRole(),
+                roleChanged.newRole());
 
-            for (ActorRef listener : registeredListeners.values()) {
+            for (var listener : registeredListeners.values()) {
                 listener.tell(latestRoleChangeNotification, self());
             }
         } else if (message instanceof LeaderStateChanged leaderStateChanged) {
index 711025bfa9251e3d332adddfef6b5c79b7093ebb..e514848d94036ad39aee52c708bf3de0c24c16bf 100644 (file)
@@ -5,38 +5,21 @@
  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
  * and is available at http://www.eclipse.org/legal/epl-v10.html
  */
-
 package org.opendaylight.controller.cluster.notifications;
 
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
 /**
  * Role Change message initiated internally from the  Raft Actor when a the behavior/role changes.
- * Since its internal , need not be serialized
+ * Since its internal, need not be serialized.
  */
-public class RoleChanged {
-    private final String memberId;
-    private final String oldRole;
-    private final String newRole;
-
-    public RoleChanged(String memberId, String oldRole, String newRole) {
-        this.memberId = memberId;
-        this.oldRole = oldRole;
-        this.newRole = newRole;
-    }
-
-    public String getMemberId() {
-        return memberId;
-    }
-
-    public String getOldRole() {
-        return oldRole;
-    }
-
-    public String getNewRole() {
-        return newRole;
-    }
-
-    @Override
-    public String toString() {
-        return "RoleChanged [memberId=" + memberId + ", oldRole=" + oldRole + ", newRole=" + newRole + "]";
+@NonNullByDefault
+public record RoleChanged(String memberId, @Nullable String oldRole, String newRole) implements MemberNotication {
+    public RoleChanged {
+        requireNonNull(memberId);
+        requireNonNull(newRole);
     }
 }
index 955f9f4d2abad83877df74341802e88e66c3d4af..ce6706975f1b5c8288994c71eaac0ea26e8b2c87 100644 (file)
@@ -7,60 +7,58 @@
  */
 package org.opendaylight.controller.cluster.notifications;
 
-import java.util.ArrayList;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
 import java.util.List;
+import java.util.stream.Stream;
 import org.apache.pekko.actor.ActorRef;
 import org.apache.pekko.actor.ActorSystem;
 import org.apache.pekko.testkit.TestProbe;
 import org.apache.pekko.testkit.javadsl.TestKit;
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-
-public class RoleChangeNotifierTest {
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 
+class RoleChangeNotifierTest {
     private static final String MEMBER_ID = "member-1";
     private static final int LISTENER_COUNT = 3;
-    private ActorSystem system;
+
     private List<TestProbe> listeners;
+    private ActorSystem system;
     private ActorRef notifier;
 
-    @Before
-    public void setUp() {
+    @BeforeEach
+    void beforeEach() {
         system = ActorSystem.apply();
         notifier = system.actorOf(RoleChangeNotifier.getProps(MEMBER_ID));
-        listeners = new ArrayList<>(LISTENER_COUNT);
-        for (int i = 0; i < LISTENER_COUNT; i++) {
-            listeners.add(new TestProbe(system));
-        }
+        listeners = Stream.generate(() -> new TestProbe(system)).limit(LISTENER_COUNT).toList();
     }
 
-    @After
-    public void tearDown() {
+    @AfterEach
+    void afterEach() {
         TestKit.shutdownActorSystem(system);
     }
 
     @Test
-    public void testHandleReceiveRoleChange() {
+    void testHandleReceiveRoleChange() {
         registerListeners();
-        final RoleChanged msg = new RoleChanged(MEMBER_ID, "old", "new");
+        final var msg = new RoleChanged(MEMBER_ID, "old", "new");
         notifier.tell(msg, ActorRef.noSender());
         checkListenerRoleChangeNotification(msg);
     }
 
     @Test
-    public void testHandleReceiveLeaderStateChanged() {
+    void testHandleReceiveLeaderStateChanged() {
         registerListeners();
-        final LeaderStateChanged msg = new LeaderStateChanged(MEMBER_ID, "leader", (short) 0);
+        final var msg = new DefaultLeaderStateChanged(MEMBER_ID, "leader", (short) 0);
         notifier.tell(msg, ActorRef.noSender());
         checkListenerLeaderStateChanged(msg);
     }
 
     @Test
-    public void testHandleReceiveRegistrationAfterRoleChange() {
-        final RoleChanged roleChanged1 = new RoleChanged(MEMBER_ID, "old1", "new1");
-        final RoleChanged lastRoleChanged = new RoleChanged(MEMBER_ID, "old2", "new2");
+    void testHandleReceiveRegistrationAfterRoleChange() {
+        final var roleChanged1 = new RoleChanged(MEMBER_ID, "old1", "new1");
+        final var lastRoleChanged = new RoleChanged(MEMBER_ID, "old2", "new2");
         notifier.tell(roleChanged1, ActorRef.noSender());
         notifier.tell(lastRoleChanged, ActorRef.noSender());
         registerListeners();
@@ -68,9 +66,9 @@ public class RoleChangeNotifierTest {
     }
 
     @Test
-    public void testHandleReceiveRegistrationAfterLeaderStateChange() {
-        final LeaderStateChanged leaderStateChanged1 = new LeaderStateChanged(MEMBER_ID, "leader1", (short) 0);
-        final LeaderStateChanged lastLeaderStateChanged = new LeaderStateChanged(MEMBER_ID, "leader2", (short) 1);
+    void testHandleReceiveRegistrationAfterLeaderStateChange() {
+        final var leaderStateChanged1 = new DefaultLeaderStateChanged(MEMBER_ID, "leader1", (short) 0);
+        final var lastLeaderStateChanged = new DefaultLeaderStateChanged(MEMBER_ID, "leader2", (short) 1);
         notifier.tell(leaderStateChanged1, ActorRef.noSender());
         notifier.tell(lastLeaderStateChanged, ActorRef.noSender());
         registerListeners();
@@ -78,28 +76,24 @@ public class RoleChangeNotifierTest {
     }
 
     private void registerListeners() {
-        for (final TestProbe listener : listeners) {
+        for (var listener : listeners) {
             notifier.tell(new RegisterRoleChangeListener(), listener.ref());
             listener.expectMsgClass(RegisterRoleChangeListenerReply.class);
         }
     }
 
     private void checkListenerRoleChangeNotification(final RoleChanged roleChanged) {
-        for (final TestProbe listener : listeners) {
+        for (var listener : listeners) {
             final RoleChangeNotification received = listener.expectMsgClass(RoleChangeNotification.class);
-            Assert.assertEquals(roleChanged.getMemberId(), received.getMemberId());
-            Assert.assertEquals(roleChanged.getOldRole(), received.getOldRole());
-            Assert.assertEquals(roleChanged.getNewRole(), received.getNewRole());
+            assertEquals(roleChanged.memberId(), received.getMemberId());
+            assertEquals(roleChanged.newRole(), received.getNewRole());
+            assertEquals(roleChanged.oldRole(), received.getOldRole());
         }
     }
 
     private void checkListenerLeaderStateChanged(final LeaderStateChanged leaderStateChanged) {
-        for (final TestProbe listener : listeners) {
-            final LeaderStateChanged received = listener.expectMsgClass(LeaderStateChanged.class);
-            Assert.assertEquals(leaderStateChanged.getMemberId(), received.getMemberId());
-            Assert.assertEquals(leaderStateChanged.getLeaderId(), received.getLeaderId());
-            Assert.assertEquals(leaderStateChanged.getLeaderPayloadVersion(), received.getLeaderPayloadVersion());
+        for (var listener : listeners) {
+            assertEquals(leaderStateChanged, listener.expectMsgClass(LeaderStateChanged.class));
         }
     }
-
 }
\ No newline at end of file
index 90c9e9f3e2a58c131d844684eb606b36036b552a..dde21a8bef8d4874d54930eac08d06ecd24f8c89 100644 (file)
@@ -588,10 +588,8 @@ public class Shard extends RaftActor {
     }
 
     @Override
-    protected final LeaderStateChanged newLeaderStateChanged(final String memberId, final String leaderId,
-            final short leaderPayloadVersion) {
-        return isLeader() ? new ShardLeaderStateChanged(memberId, leaderId, store.getDataTree(), leaderPayloadVersion)
-                : new ShardLeaderStateChanged(memberId, leaderId, leaderPayloadVersion);
+    protected final LeaderStateChanged wrapLeaderStateChanged(final LeaderStateChanged change) {
+        return new ShardLeaderStateChanged(change, isLeader() ? store.getDataTree() : null);
     }
 
     private void onDatastoreContext(final DatastoreContext context) {
index c92670c97138c66060be1d149a0f15a213f4828c..66cb8440783452e25ff47d4388a4e51cb6cb635b 100644 (file)
@@ -9,8 +9,11 @@ package org.opendaylight.controller.cluster.datastore.messages;
 
 import static java.util.Objects.requireNonNull;
 
-import org.eclipse.jdt.annotation.NonNull;
+import com.google.common.annotations.VisibleForTesting;
+import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.controller.cluster.notifications.DefaultLeaderStateChanged;
+import org.opendaylight.controller.cluster.notifications.ForwadingLeaderStateChanged;
 import org.opendaylight.controller.cluster.notifications.LeaderStateChanged;
 import org.opendaylight.yangtools.yang.data.tree.api.ReadOnlyDataTree;
 
@@ -21,22 +24,36 @@ import org.opendaylight.yangtools.yang.data.tree.api.ReadOnlyDataTree;
  *
  * @author Thomas Pantelis
  */
-public final class ShardLeaderStateChanged extends LeaderStateChanged {
+@NonNullByDefault
+public final class ShardLeaderStateChanged extends ForwadingLeaderStateChanged {
     private final @Nullable ReadOnlyDataTree localShardDataTree;
+    private final LeaderStateChanged delegate;
 
-    public ShardLeaderStateChanged(final @NonNull String memberId, final @Nullable String leaderId,
-            final @NonNull ReadOnlyDataTree localShardDataTree, final short leaderPayloadVersion) {
-        super(memberId, leaderId, leaderPayloadVersion);
-        this.localShardDataTree = requireNonNull(localShardDataTree);
+    public ShardLeaderStateChanged(final LeaderStateChanged delegate,
+            final @Nullable ReadOnlyDataTree localShardDataTree) {
+        this.delegate = requireNonNull(delegate);
+        this.localShardDataTree = localShardDataTree;
     }
 
-    public ShardLeaderStateChanged(final @NonNull String memberId, final @Nullable String leaderId,
+    @VisibleForTesting
+    public ShardLeaderStateChanged(final String memberId, final @Nullable String leaderId,
+            final ReadOnlyDataTree localShardDataTree, final short leaderPayloadVersion) {
+        this(new DefaultLeaderStateChanged(memberId, leaderId, leaderPayloadVersion),
+            requireNonNull(localShardDataTree));
+    }
+
+    @VisibleForTesting
+    public ShardLeaderStateChanged(final String memberId, final @Nullable String leaderId,
             final short leaderPayloadVersion) {
-        super(memberId, leaderId, leaderPayloadVersion);
-        localShardDataTree = null;
+        this(new DefaultLeaderStateChanged(memberId, leaderId, leaderPayloadVersion), null);
     }
 
     public @Nullable ReadOnlyDataTree localShardDataTree() {
         return localShardDataTree;
     }
+
+    @Override
+    protected LeaderStateChanged delegate() {
+        return delegate;
+    }
 }
index 891c9e371ee6506f8b61ee81aca1277a03c351d3..2598e11e6936a9c883d1d9cbc4eb35e1dd2a3e14 100644 (file)
@@ -584,11 +584,11 @@ class ShardManager extends AbstractUntypedPersistentActorWithMetering {
     private void onLeaderStateChanged(final ShardLeaderStateChanged leaderStateChanged) {
         LOG.info("{}: Received LeaderStateChanged message: {}", logName(), leaderStateChanged);
 
-        ShardInformation shardInformation = findShardInformation(leaderStateChanged.getMemberId());
+        ShardInformation shardInformation = findShardInformation(leaderStateChanged.memberId());
         if (shardInformation != null) {
             shardInformation.setLocalDataTree(leaderStateChanged.localShardDataTree());
-            shardInformation.setLeaderVersion(leaderStateChanged.getLeaderPayloadVersion());
-            if (shardInformation.setLeaderId(leaderStateChanged.getLeaderId())) {
+            shardInformation.setLeaderVersion(leaderStateChanged.leaderPayloadVersion());
+            if (shardInformation.setLeaderId(leaderStateChanged.leaderId())) {
                 primaryShardInfoCache.remove(shardInformation.getShardName());
 
                 notifyShardAvailabilityCallbacks(shardInformation);
@@ -596,7 +596,7 @@ class ShardManager extends AbstractUntypedPersistentActorWithMetering {
 
             checkReady();
         } else {
-            LOG.debug("No shard found with member Id {}", leaderStateChanged.getMemberId());
+            LOG.debug("No shard found with member Id {}", leaderStateChanged.memberId());
         }
     }
 
index 9db3e744b360a1663ecb53d7596f5abfa33c192e..309741b625e92703f6c6cfa862474007f1c898ec 100644 (file)
@@ -17,6 +17,7 @@ import org.apache.pekko.testkit.TestActorRef;
 import org.apache.pekko.testkit.javadsl.TestKit;
 import org.junit.Before;
 import org.junit.Test;
+import org.opendaylight.controller.cluster.notifications.DefaultLeaderStateChanged;
 import org.opendaylight.controller.cluster.notifications.LeaderStateChanged;
 import org.opendaylight.controller.cluster.notifications.RegisterRoleChangeListener;
 import org.opendaylight.controller.cluster.notifications.RegisterRoleChangeListenerReply;
@@ -90,7 +91,7 @@ public class RoleChangeNotifierTest extends AbstractActorTest {
         TestActorRef<RoleChangeNotifier> notifierTestActorRef = TestActorRef.create(getSystem(),
             RoleChangeNotifier.getProps(actorId), actorId);
 
-        notifierTestActorRef.tell(new LeaderStateChanged("member1", "leader1", (short) 5), ActorRef.noSender());
+        notifierTestActorRef.tell(new DefaultLeaderStateChanged("member1", "leader1", (short) 5), ActorRef.noSender());
 
         // listener registers after the sate has been changed, ensure we
         // sent the latest state change after a reply
@@ -99,15 +100,15 @@ public class RoleChangeNotifierTest extends AbstractActorTest {
         testKit.expectMsgClass(RegisterRoleChangeListenerReply.class);
 
         LeaderStateChanged leaderStateChanged = testKit.expectMsgClass(LeaderStateChanged.class);
-        assertEquals("getMemberId", "member1", leaderStateChanged.getMemberId());
-        assertEquals("getLeaderId", "leader1", leaderStateChanged.getLeaderId());
-        assertEquals("getLeaderPayloadVersion", 5, leaderStateChanged.getLeaderPayloadVersion());
+        assertEquals("getMemberId", "member1", leaderStateChanged.memberId());
+        assertEquals("getLeaderId", "leader1", leaderStateChanged.leaderId());
+        assertEquals("getLeaderPayloadVersion", 5, leaderStateChanged.leaderPayloadVersion());
 
-        notifierTestActorRef.tell(new LeaderStateChanged("member1", "leader2", (short) 6), ActorRef.noSender());
+        notifierTestActorRef.tell(new DefaultLeaderStateChanged("member1", "leader2", (short) 6), ActorRef.noSender());
 
         leaderStateChanged = testKit.expectMsgClass(LeaderStateChanged.class);
-        assertEquals("getMemberId", "member1", leaderStateChanged.getMemberId());
-        assertEquals("getLeaderId", "leader2", leaderStateChanged.getLeaderId());
-        assertEquals("getLeaderPayloadVersion", 6, leaderStateChanged.getLeaderPayloadVersion());
+        assertEquals("getMemberId", "member1", leaderStateChanged.memberId());
+        assertEquals("getLeaderId", "leader2", leaderStateChanged.leaderId());
+        assertEquals("getLeaderPayloadVersion", 6, leaderStateChanged.leaderPayloadVersion());
     }
 }
index 4ea6ed2b2593aeb416e76aa0e0ece06b8d9fa560..c0b7dd62b3434c61906feb455f1f8875de770a97 100644 (file)
@@ -111,7 +111,7 @@ import org.opendaylight.controller.cluster.datastore.utils.ForwardingActor;
 import org.opendaylight.controller.cluster.datastore.utils.MockClusterWrapper;
 import org.opendaylight.controller.cluster.datastore.utils.MockConfiguration;
 import org.opendaylight.controller.cluster.datastore.utils.PrimaryShardInfoFutureCache;
-import org.opendaylight.controller.cluster.notifications.LeaderStateChanged;
+import org.opendaylight.controller.cluster.notifications.DefaultLeaderStateChanged;
 import org.opendaylight.controller.cluster.notifications.RegisterRoleChangeListener;
 import org.opendaylight.controller.cluster.notifications.RoleChangeNotification;
 import org.opendaylight.controller.cluster.raft.RaftState;
@@ -463,7 +463,7 @@ public class ShardManagerTest extends AbstractClusterRefActorTest {
         shardManager.tell(
             new RoleChangeNotification(memberId1, RaftState.Candidate.name(), RaftState.Follower.name()),
             mockShardActor);
-        shardManager.tell(new LeaderStateChanged(memberId1, memberId2, DataStoreVersions.CURRENT_VERSION),
+        shardManager.tell(new DefaultLeaderStateChanged(memberId1, memberId2, DataStoreVersions.CURRENT_VERSION),
             mockShardActor);
 
         shardManager.tell(new FindPrimary(Shard.DEFAULT_NAME, false), kit.getRef());