/** * Licensed 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.aurora.scheduler.discovery; import java.net.InetSocketAddress; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import org.apache.aurora.common.zookeeper.SingletonService; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent; import org.easymock.Capture; import org.easymock.IMocksControl; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.Timeout; import static org.easymock.EasyMock.capture; import static org.easymock.EasyMock.createControl; import static org.easymock.EasyMock.expectLastCall; import static org.easymock.EasyMock.newCapture; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class CuratorSingletonServiceTest extends BaseCuratorDiscoveryTest { private IMocksControl control; // This test has a lot of blocking, this ensures we don't deadlock. private final Timeout timeout = new Timeout(1, TimeUnit.MINUTES); @Rule public Timeout getTimeout() { return timeout; } @Before public void setUpSingletonService() throws Exception { control = createControl(); addTearDown(control::verify); } private SingletonService.LeadershipListener createMockLeadershipListener() { return control.createMock(SingletonService.LeadershipListener.class); } private void newLeader( CuratorFramework client, String hostName, SingletonService.LeadershipListener listener) throws Exception { CuratorSingletonService singletonService = new CuratorSingletonService(client, GROUP_PATH, MEMBER_TOKEN, CODEC); InetSocketAddress leaderEndpoint = InetSocketAddress.createUnresolved(hostName, PRIMARY_PORT); singletonService.lead(leaderEndpoint, ImmutableMap.of(), listener); } @Test public void testLeadAdvertise() throws Exception { SingletonService.LeadershipListener listener = createMockLeadershipListener(); Capture<SingletonService.LeaderControl> capture = newCapture(); listener.onLeading(capture(capture)); expectLastCall(); control.replay(); startGroupMonitor(); // Can't be leader until we try to lead. assertFalse(capture.hasCaptured()); newLeader(getClient(), "host1", listener); // Wait to become leader. expectGroupEvent(PathChildrenCacheEvent.Type.CHILD_ADDED); awaitCapture(capture); // Leadership nodes should not be seen as service group nodes. assertEquals(ImmutableSet.of(), getGroupMonitor().get()); capture.getValue().advertise(); // Verify we've advertised as leader. expectGroupEvent(PathChildrenCacheEvent.Type.CHILD_ADDED); assertEquals(ImmutableSet.of(serviceInstance("host1")), getGroupMonitor().get()); } @Test public void testAbdicateTransition() throws Exception { SingletonService.LeadershipListener host1Listener = createMockLeadershipListener(); Capture<SingletonService.LeaderControl> host1OnLeadingCapture = newCapture(); host1Listener.onLeading(capture(host1OnLeadingCapture)); expectLastCall(); SingletonService.LeadershipListener host2Listener = createMockLeadershipListener(); Capture<SingletonService.LeaderControl> host2OnLeadingCapture = newCapture(); host2Listener.onLeading(capture(host2OnLeadingCapture)); expectLastCall(); control.replay(); startGroupMonitor(); // Have host1 become leader. newLeader(getClient(), "host1", host1Listener); expectGroupEvent(PathChildrenCacheEvent.Type.CHILD_ADDED); awaitCapture(host1OnLeadingCapture); host1OnLeadingCapture.getValue().advertise(); expectGroupEvent(PathChildrenCacheEvent.Type.CHILD_ADDED); assertEquals(ImmutableSet.of(serviceInstance("host1")), getGroupMonitor().get()); newLeader(getClient(), "host2", host2Listener); expectGroupEvent(PathChildrenCacheEvent.Type.CHILD_ADDED); assertFalse(host2OnLeadingCapture.hasCaptured()); // Now have host1 abdicate. host1OnLeadingCapture.getValue().leave(); // Should see both the leadership and service group member nodes get cleaned up by host1. expectGroupEvent(PathChildrenCacheEvent.Type.CHILD_REMOVED); expectGroupEvent(PathChildrenCacheEvent.Type.CHILD_REMOVED); awaitCapture(host2OnLeadingCapture); } @Test public void testDefeatTransition() throws Exception { SingletonService.LeadershipListener host1Listener = createMockLeadershipListener(); Capture<SingletonService.LeaderControl> host1OnLeadingCapture = newCapture(); host1Listener.onLeading(capture(host1OnLeadingCapture)); expectLastCall(); CountDownLatch host1Defeated = new CountDownLatch(1); host1Listener.onDefeated(); expectLastCall().andAnswer(() -> { host1Defeated.countDown(); return null; }); SingletonService.LeadershipListener host2Listener = createMockLeadershipListener(); Capture<SingletonService.LeaderControl> host2OnLeadingCapture = newCapture(); host2Listener.onLeading(capture(host2OnLeadingCapture)); expectLastCall(); control.replay(); startGroupMonitor(); // Have host1 become leader. newLeader(getClient(), "host1", host1Listener); expectGroupEvent(PathChildrenCacheEvent.Type.CHILD_ADDED); awaitCapture(host1OnLeadingCapture); host1OnLeadingCapture.getValue().advertise(); expectGroupEvent(PathChildrenCacheEvent.Type.CHILD_ADDED); assertEquals(ImmutableSet.of(serviceInstance("host1")), getGroupMonitor().get()); newLeader(startNewClient(), "host2", host2Listener); expectGroupEvent(PathChildrenCacheEvent.Type.CHILD_ADDED); assertFalse(host2OnLeadingCapture.hasCaptured()); // Simulate a session timeout - the ephemeral leader node goes away and host1 should be // defeated. expireSession(getClient()); // Should see both the leadership and service group member nodes go away as part of session // expiration for ephemeral nodes. expectGroupEvent(PathChildrenCacheEvent.Type.CHILD_REMOVED); expectGroupEvent(PathChildrenCacheEvent.Type.CHILD_REMOVED); awaitCapture(host2OnLeadingCapture); // No advertisement by host2 yet, even though it won leadership, but the host1 service group // node should have been cleaned up as tested above. assertEquals(ImmutableSet.of(), getGroupMonitor().get()); // Eventually host1 should notice its been defeated. host1Defeated.await(); } @Test public void testZKDisconnection() throws Exception { CountDownLatch leading = new CountDownLatch(1); AtomicBoolean leader = new AtomicBoolean(false); CountDownLatch defeated = new CountDownLatch(1); // The listener is executed in an internal thread of Curator, where it executes the leader // listener callbacks. The exceptions there are not propagated out, so we have our own // listener to validate behaviour. SingletonService.LeadershipListener listener = new SingletonService.LeadershipListener() { @Override public void onLeading(SingletonService.LeaderControl leaderControl) { leader.set(true); leading.countDown(); } @Override public void onDefeated() { leader.set(false); defeated.countDown(); } }; control.replay(); startGroupMonitor(); CuratorFramework client = getClient(); newLeader(client, "host1", listener); leading.await(); causeDisconnection(); assertTrue(leader.get()); expireSession(client); defeated.await(); assertFalse(leader.get()); } private void awaitCapture(Capture<?> capture) throws InterruptedException { while (!capture.hasCaptured()) { Thread.sleep(1L); } } }