/* * Copyright 2012-2017 the original author or authors. * * 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.springframework.integration.support.leader; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.junit.Before; import org.junit.Test; import org.springframework.beans.DirectFieldAccessor; import org.springframework.core.task.SyncTaskExecutor; import org.springframework.core.task.support.ExecutorServiceAdapter; import org.springframework.integration.leader.Context; import org.springframework.integration.leader.DefaultCandidate; import org.springframework.integration.leader.event.DefaultLeaderEventPublisher; import org.springframework.integration.leader.event.LeaderEventPublisher; import org.springframework.integration.support.locks.DefaultLockRegistry; import org.springframework.integration.support.locks.LockRegistry; /** * @author Dave Syer * @author Artem Bilan * @author Vedran Pavic * * @since 4.3.1 */ public class LockRegistryLeaderInitiatorTests { private CountDownLatch granted; private CountDownLatch revoked; private final LockRegistry registry = new DefaultLockRegistry(); private final LockRegistryLeaderInitiator initiator = new LockRegistryLeaderInitiator(this.registry, new DefaultCandidate()); @Before public void init() { this.granted = new CountDownLatch(1); this.revoked = new CountDownLatch(1); this.initiator.setLeaderEventPublisher(new CountingPublisher(this.granted, this.revoked)); } @Test public void startAndStop() throws Exception { assertThat(this.initiator.getContext().isLeader(), is(false)); this.initiator.start(); assertThat(this.initiator.isRunning(), is(true)); assertTrue(this.granted.await(10, TimeUnit.SECONDS)); assertThat(this.initiator.getContext().isLeader(), is(true)); Thread.sleep(200L); assertThat(this.initiator.getContext().isLeader(), is(true)); this.initiator.stop(); assertTrue(this.revoked.await(10, TimeUnit.SECONDS)); assertThat(this.initiator.getContext().isLeader(), is(false)); } @Test public void yield() throws Exception { assertThat(this.initiator.getContext().isLeader(), is(false)); this.initiator.start(); assertThat(this.initiator.isRunning(), is(true)); assertTrue(this.granted.await(10, TimeUnit.SECONDS)); assertThat(this.initiator.getContext().isLeader(), is(true)); this.initiator.getContext().yield(); assertThat(this.revoked.await(10, TimeUnit.SECONDS), is(true)); this.initiator.stop(); } @Test public void competing() throws Exception { LockRegistryLeaderInitiator another = new LockRegistryLeaderInitiator(this.registry, new DefaultCandidate()); CountDownLatch other = new CountDownLatch(1); another.setLeaderEventPublisher(new CountingPublisher(other)); this.initiator.start(); assertThat(this.granted.await(20, TimeUnit.SECONDS), is(true)); another.start(); this.initiator.stop(); assertThat(other.await(20, TimeUnit.SECONDS), is(true)); assertThat(another.getContext().isLeader(), is(true)); another.stop(); } @Test public void testExceptionFromEvent() throws Exception { CountDownLatch onGranted = new CountDownLatch(1); LockRegistryLeaderInitiator initiator = new LockRegistryLeaderInitiator(this.registry, new DefaultCandidate()); initiator.setLeaderEventPublisher(new DefaultLeaderEventPublisher() { @Override public void publishOnGranted(Object source, Context context, String role) { try { throw new RuntimeException("intentional"); } finally { onGranted.countDown(); } } }); initiator.start(); assertTrue(onGranted.await(10, TimeUnit.SECONDS)); assertTrue(initiator.getContext().isLeader()); initiator.stop(); } @Test public void competingWithLock() throws Exception { // switch used to toggle which registry obtains lock AtomicBoolean firstLocked = new AtomicBoolean(true); // set up first registry instance - this one will be able to obtain lock initially LockRegistry firstRegistry = mock(LockRegistry.class); Lock firstLock = mock(Lock.class); given(firstRegistry.obtain(anyString())).willReturn(firstLock); given(firstLock.tryLock(anyLong(), any(TimeUnit.class))).willAnswer(i -> firstLocked.get()); // set up first initiator instance using first LockRegistry LockRegistryLeaderInitiator first = new LockRegistryLeaderInitiator(firstRegistry, new DefaultCandidate()); CountDownLatch firstGranted = new CountDownLatch(1); CountDownLatch firstRevoked = new CountDownLatch(1); first.setHeartBeatMillis(10); first.setBusyWaitMillis(1); first.setLeaderEventPublisher(new CountingPublisher(firstGranted, firstRevoked)); // set up second registry instance - this one will NOT be able to obtain lock initially LockRegistry secondRegistry = mock(LockRegistry.class); Lock secondLock = mock(Lock.class); given(secondRegistry.obtain(anyString())).willReturn(secondLock); given(secondLock.tryLock(anyLong(), any(TimeUnit.class))).willAnswer(i -> !firstLocked.get()); // set up second initiator instance using second LockRegistry LockRegistryLeaderInitiator second = new LockRegistryLeaderInitiator(secondRegistry, new DefaultCandidate()); CountDownLatch secondGranted = new CountDownLatch(1); CountDownLatch secondRevoked = new CountDownLatch(1); second.setHeartBeatMillis(10); second.setBusyWaitMillis(1); second.setLeaderEventPublisher(new CountingPublisher(secondGranted, secondRevoked)); // start initiators first.start(); second.start(); // first initiator should lead and publish granted event assertThat(firstGranted.await(10, TimeUnit.SECONDS), is(true)); assertThat(first.getContext().isLeader(), is(true)); assertThat(second.getContext().isLeader(), is(false)); // simulate first registry instance unable to obtain lock, for example due to lock timeout firstLocked.set(false); // second initiator should take lead and publish granted event, first initiator should publish revoked event assertThat(secondGranted.await(10, TimeUnit.SECONDS), is(true)); assertThat(firstRevoked.await(10, TimeUnit.SECONDS), is(true)); assertThat(second.getContext().isLeader(), is(true)); assertThat(first.getContext().isLeader(), is(false)); first.stop(); second.stop(); } @Test public void testGracefulLeaderSelectorExit() throws Exception { AtomicReference<Throwable> throwableAtomicReference = new AtomicReference<>(); LockRegistry registry = mock(LockRegistry.class); Lock lock = spy(new ReentrantLock()); willAnswer(invocation -> { try { return invocation.callRealMethod(); } catch (Throwable e) { throwableAtomicReference.set(e); throw e; } }) .given(lock) .unlock(); given(registry.obtain(anyString())) .willReturn(lock); LockRegistryLeaderInitiator initiator = new LockRegistryLeaderInitiator(registry); willAnswer(invocation -> { initiator.stop(); return false; }) .given(lock) .tryLock(anyLong(), eq(TimeUnit.MILLISECONDS)); new DirectFieldAccessor(initiator).setPropertyValue("executorService", new ExecutorServiceAdapter( new SyncTaskExecutor())); initiator.start(); Throwable throwable = throwableAtomicReference.get(); assertNull(throwable); } private static class CountingPublisher implements LeaderEventPublisher { private final CountDownLatch granted; private final CountDownLatch revoked; CountingPublisher(CountDownLatch granted, CountDownLatch revoked) { this.granted = granted; this.revoked = revoked; } CountingPublisher(CountDownLatch granted) { this(granted, new CountDownLatch(1)); } @Override public void publishOnRevoked(Object source, Context context, String role) { this.revoked.countDown(); } @Override public void publishOnGranted(Object source, Context context, String role) { this.granted.countDown(); } } }