2 * Copyright (c) 2017 Pantheon Technologies s.r.o. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.controller.cluster.access.client;
10 import static org.junit.jupiter.api.Assertions.assertEquals;
11 import static org.mockito.ArgumentMatchers.any;
12 import static org.mockito.Mockito.doReturn;
13 import static org.mockito.Mockito.timeout;
14 import static org.mockito.Mockito.verify;
16 import akka.actor.ActorRef;
17 import akka.actor.ActorSystem;
18 import akka.actor.Props;
19 import akka.persistence.Persistence;
20 import akka.persistence.SelectedSnapshot;
21 import akka.persistence.SnapshotMetadata;
22 import akka.testkit.TestProbe;
23 import akka.testkit.javadsl.TestKit;
24 import com.typesafe.config.ConfigFactory;
25 import java.util.Optional;
26 import java.util.concurrent.TimeUnit;
27 import org.junit.jupiter.api.AfterEach;
28 import org.junit.jupiter.api.BeforeEach;
29 import org.junit.jupiter.api.Test;
30 import org.junit.jupiter.api.extension.ExtendWith;
31 import org.mockito.Answers;
32 import org.mockito.Mock;
33 import org.mockito.junit.jupiter.MockitoExtension;
34 import org.opendaylight.controller.cluster.access.concepts.ClientIdentifier;
35 import org.opendaylight.controller.cluster.access.concepts.FrontendIdentifier;
36 import org.opendaylight.controller.cluster.access.concepts.FrontendType;
37 import org.opendaylight.controller.cluster.access.concepts.MemberName;
38 import scala.concurrent.duration.FiniteDuration;
40 @ExtendWith(MockitoExtension.class)
41 class ActorBehaviorTest {
42 private static final String MEMBER_1_FRONTEND_TYPE_1 = "member-1-frontend-type-1";
43 private static final FiniteDuration TIMEOUT = FiniteDuration.create(5, TimeUnit.SECONDS);
46 private InternalCommand<BackendInfo> cmd;
47 @Mock(answer = Answers.CALLS_REAL_METHODS)
48 private ClientActorBehavior<BackendInfo> initialBehavior;
50 private AbstractClientActorContext ctx;
52 private ActorSystem system;
53 private TestProbe probe;
54 private MockedSnapshotStore.SaveRequest saveRequest;
55 private FrontendIdentifier id;
56 private ActorRef mockedActor;
59 void beforeEach() throws Exception {
60 //persistenceId() in AbstractClientActorBehavior is final and can't be mocked
61 //use reflection to work around this
62 final var context = AbstractClientActorBehavior.class.getDeclaredField("context");
63 context.setAccessible(true);
64 context.set(initialBehavior, ctx);
65 final var persistenceId = AbstractClientActorContext.class.getDeclaredField("persistenceId");
66 persistenceId.setAccessible(true);
67 persistenceId.set(ctx, MEMBER_1_FRONTEND_TYPE_1);
69 system = ActorSystem.apply("system1");
70 final ActorRef storeRef = system.registerExtension(Persistence.lookup()).snapshotStoreFor(null,
71 ConfigFactory.empty());
72 probe = new TestProbe(system);
73 storeRef.tell(probe.ref(), ActorRef.noSender());
74 final MemberName name = MemberName.forName("member-1");
75 id = FrontendIdentifier.create(name, FrontendType.forName("type-1"));
76 mockedActor = system.actorOf(MockedActor.props(id, initialBehavior));
77 //handle initial actor recovery
78 saveRequest = handleRecovery(null);
83 TestKit.shutdownActorSystem(system);
87 void testInitialBehavior() {
88 doReturn(initialBehavior).when(cmd).execute(any());
89 mockedActor.tell(cmd, ActorRef.noSender());
90 verify(cmd, timeout(1000)).execute(initialBehavior);
94 void testCommandStashing() {
95 system.stop(mockedActor);
96 mockedActor = system.actorOf(MockedActor.props(id, initialBehavior));
97 doReturn(initialBehavior).when(cmd).execute(any());
98 //send messages before recovery is completed
99 mockedActor.tell(cmd, ActorRef.noSender());
100 mockedActor.tell(cmd, ActorRef.noSender());
101 mockedActor.tell(cmd, ActorRef.noSender());
103 handleRecovery(null);
104 verify(cmd, timeout(1000).times(3)).execute(initialBehavior);
108 void testRecoveryAfterRestart() {
109 system.stop(mockedActor);
110 mockedActor = system.actorOf(MockedActor.props(id, initialBehavior));
111 final MockedSnapshotStore.SaveRequest newSaveRequest =
112 handleRecovery(new SelectedSnapshot(saveRequest.getMetadata(), saveRequest.getSnapshot()));
113 assertEquals(MEMBER_1_FRONTEND_TYPE_1, newSaveRequest.getMetadata().persistenceId());
117 void testRecoveryAfterRestartFrontendIdMismatch() {
118 system.stop(mockedActor);
120 mockedActor = system.actorOf(MockedActor.props(id, initialBehavior));
121 probe.expectMsgClass(MockedSnapshotStore.LoadRequest.class);
122 //offer snapshot with incorrect client id
123 final SnapshotMetadata metadata = saveRequest.getMetadata();
124 final FrontendIdentifier anotherFrontend = FrontendIdentifier.create(MemberName.forName("another"),
125 FrontendType.forName("type-2"));
126 final ClientIdentifier incorrectClientId = ClientIdentifier.create(anotherFrontend, 0);
127 probe.watch(mockedActor);
128 probe.reply(Optional.of(new SelectedSnapshot(metadata, incorrectClientId)));
129 //actor should be stopped
130 probe.expectTerminated(mockedActor, TIMEOUT);
134 void testRecoveryAfterRestartSaveSnapshotFail() {
135 system.stop(mockedActor);
136 mockedActor = system.actorOf(MockedActor.props(id, initialBehavior));
137 probe.watch(mockedActor);
138 probe.expectMsgClass(MockedSnapshotStore.LoadRequest.class);
139 probe.reply(Optional.empty());
140 probe.expectMsgClass(MockedSnapshotStore.SaveRequest.class);
141 probe.reply(new RuntimeException("save failed"));
142 probe.expectMsgClass(MockedSnapshotStore.DeleteByMetadataRequest.class);
143 probe.expectTerminated(mockedActor, TIMEOUT);
147 void testRecoveryAfterRestartDeleteSnapshotsFail() {
148 system.stop(mockedActor);
149 mockedActor = system.actorOf(MockedActor.props(id, initialBehavior));
150 probe.watch(mockedActor);
151 probe.expectMsgClass(MockedSnapshotStore.LoadRequest.class);
152 probe.reply(Optional.empty());
153 probe.expectMsgClass(MockedSnapshotStore.SaveRequest.class);
154 probe.reply(Void.TYPE);
155 probe.expectMsgClass(MockedSnapshotStore.DeleteByCriteriaRequest.class);
156 probe.reply(new RuntimeException("delete failed"));
157 //actor shouldn't terminate
158 probe.expectNoMessage();
161 private MockedSnapshotStore.SaveRequest handleRecovery(final SelectedSnapshot savedState) {
162 probe.expectMsgClass(MockedSnapshotStore.LoadRequest.class);
164 probe.reply(Optional.ofNullable(savedState));
165 final MockedSnapshotStore.SaveRequest nextSaveRequest =
166 probe.expectMsgClass(MockedSnapshotStore.SaveRequest.class);
167 probe.reply(Void.TYPE);
168 //check old snapshots deleted
169 probe.expectMsgClass(MockedSnapshotStore.DeleteByCriteriaRequest.class);
170 probe.reply(Void.TYPE);
171 return nextSaveRequest;
174 private static class MockedActor extends AbstractClientActor {
175 private final ClientActorBehavior<?> initialBehavior;
176 private final ClientActorConfig mockConfig = AccessClientUtil.newMockClientActorConfig();
178 private static Props props(final FrontendIdentifier frontendId, final ClientActorBehavior<?> initialBehavior) {
179 return Props.create(MockedActor.class, () -> new MockedActor(frontendId, initialBehavior));
182 MockedActor(final FrontendIdentifier frontendId, final ClientActorBehavior<?> initialBehavior) {
184 this.initialBehavior = initialBehavior;
188 protected ClientActorBehavior<?> initialBehavior(final ClientActorContext context) {
189 return initialBehavior;
193 protected ClientActorConfig getClientActorConfig() {