package org.corfudb.runtime.view; import com.google.common.reflect.TypeToken; import lombok.Getter; import org.corfudb.infrastructure.TestLayoutBuilder; import org.corfudb.infrastructure.ServerContext; import org.corfudb.infrastructure.ServerContextBuilder; import org.corfudb.infrastructure.PurgeFailurePolicy; import org.corfudb.infrastructure.TestServerRouter; import org.corfudb.protocols.wireprotocol.CorfuMsgType; import org.corfudb.protocols.wireprotocol.TokenResponse; import org.corfudb.runtime.CorfuRuntime; import org.corfudb.runtime.clients.ManagementClient; import org.corfudb.runtime.clients.TestRule; import org.corfudb.runtime.collections.ISMRMap; import org.corfudb.runtime.collections.SMRMap; import org.corfudb.runtime.exceptions.TransactionAbortedException; import org.corfudb.runtime.view.stream.IStreamView; import org.junit.Test; import java.util.Collections; import java.util.Map; import java.util.UUID; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; /** * Test to verify the Management Server functionality. * <p> * Created by zlokhandwala on 11/9/16. */ public class ManagementViewTest extends AbstractViewTest { @Getter protected CorfuRuntime corfuRuntime = null; /** * Sets aggressive timeouts for all the router endpoints on all the runtimes. * <p> * @param layout Layout to get all server endpoints. * @param corfuRuntimes All runtimes whose routers' timeouts are to be set. */ public void setAggressiveTimeouts(Layout layout, CorfuRuntime... corfuRuntimes) { layout.getAllServers().forEach(routerEndpoint -> { for (CorfuRuntime runtime : corfuRuntimes) { runtime.getRouter(routerEndpoint).setTimeoutConnect(PARAMETERS.TIMEOUT_VERY_SHORT.toMillis()); runtime.getRouter(routerEndpoint).setTimeoutResponse(PARAMETERS.TIMEOUT_VERY_SHORT.toMillis()); runtime.getRouter(routerEndpoint).setTimeoutRetry(PARAMETERS.TIMEOUT_VERY_SHORT.toMillis()); } }); } /** * Scenario with 2 nodes: SERVERS.PORT_0 and SERVERS.PORT_1. * We fail SERVERS.PORT_0 and then listen to intercept the message * sent by SERVERS.PORT_1's client to the server to handle the failure. * * @throws Exception */ @Test public void invokeFailureHandler() throws Exception { // Boolean flag turned to true when the MANAGEMENT_FAILURE_DETECTED message // is sent by the Management client to its server. final Semaphore failureDetected = new Semaphore(1,true); addServer(SERVERS.PORT_0); addServer(SERVERS.PORT_1); Layout l = new TestLayoutBuilder() .setEpoch(1L) .addLayoutServer(SERVERS.PORT_0) .addLayoutServer(SERVERS.PORT_1) .addSequencer(SERVERS.PORT_0) .buildSegment() .buildStripe() .addLogUnit(SERVERS.PORT_0) .addLogUnit(SERVERS.PORT_1) .addToSegment() .addToLayout() .build(); bootstrapAllServers(l); CorfuRuntime corfuRuntime = new CorfuRuntime(); l.getLayoutServers().forEach(corfuRuntime::addLayoutServer); corfuRuntime.connect(); corfuRuntime.getRouter(SERVERS.ENDPOINT_1).getClient(ManagementClient.class).initiateFailureHandler().get(); // Set aggressive timeouts. setAggressiveTimeouts(l, corfuRuntime, getManagementServer(SERVERS.PORT_0).getCorfuRuntime(), getManagementServer(SERVERS.PORT_1).getCorfuRuntime()); failureDetected.acquire(); // Adding a rule on SERVERS.PORT_0 to drop all packets addServerRule(SERVERS.PORT_0, new TestRule().always().drop()); getManagementServer(SERVERS.PORT_0).shutdown(); // Adding a rule on SERVERS.PORT_1 to toggle the flag when it sends the // MANAGEMENT_FAILURE_DETECTED message. addClientRule(getManagementServer(SERVERS.PORT_1).getCorfuRuntime(), new TestRule().matches(corfuMsg -> { if (corfuMsg.getMsgType().equals(CorfuMsgType .MANAGEMENT_FAILURE_DETECTED)) { failureDetected.release(); } return true; })); assertThat(failureDetected.tryAcquire(PARAMETERS.TIMEOUT_NORMAL .toNanos(), TimeUnit.NANOSECONDS)).isEqualTo(true); } /** * Scenario with 3 nodes: SERVERS.PORT_0, SERVERS.PORT_1 and SERVERS.PORT_2. * We fail SERVERS.PORT_1 and then wait for one of the other two servers to * handle this failure, propose a new layout and we assert on the epoch change. * The failure is handled by removing the failed node. * * @throws Exception */ @Test public void removeSingleNodeFailure() throws Exception{ // Creating server contexts with PurgeFailurePolicies. ServerContext sc0 = new ServerContextBuilder().setSingle(false).setServerRouter(new TestServerRouter(SERVERS.PORT_0)).setPort(SERVERS.PORT_0).build(); ServerContext sc1 = new ServerContextBuilder().setSingle(false).setServerRouter(new TestServerRouter(SERVERS.PORT_1)).setPort(SERVERS.PORT_1).build(); ServerContext sc2 = new ServerContextBuilder().setSingle(false).setServerRouter(new TestServerRouter(SERVERS.PORT_2)).setPort(SERVERS.PORT_2).build(); sc0.setFailureHandlerPolicy(new PurgeFailurePolicy()); sc1.setFailureHandlerPolicy(new PurgeFailurePolicy()); sc2.setFailureHandlerPolicy(new PurgeFailurePolicy()); addServer(SERVERS.PORT_0, sc0); addServer(SERVERS.PORT_1, sc1); addServer(SERVERS.PORT_2, sc2); Layout l = new TestLayoutBuilder() .setEpoch(1L) .addLayoutServer(SERVERS.PORT_0) .addLayoutServer(SERVERS.PORT_1) .addLayoutServer(SERVERS.PORT_2) .addSequencer(SERVERS.PORT_0) .buildSegment() .buildStripe() .addLogUnit(SERVERS.PORT_0) .addLogUnit(SERVERS.PORT_2) .addToSegment() .addToLayout() .build(); bootstrapAllServers(l); CorfuRuntime corfuRuntime = new CorfuRuntime(); l.getLayoutServers().forEach(corfuRuntime::addLayoutServer); corfuRuntime.connect(); // Initiating all failure handlers. for (String server: l.getAllServers()) { corfuRuntime.getRouter(server).getClient(ManagementClient.class).initiateFailureHandler().get(); } // Setting aggressive timeouts setAggressiveTimeouts(l, corfuRuntime, getManagementServer(SERVERS.PORT_0).getCorfuRuntime(), getManagementServer(SERVERS.PORT_1).getCorfuRuntime(), getManagementServer(SERVERS.PORT_2).getCorfuRuntime()); // Adding a rule on SERVERS.PORT_1 to drop all packets addServerRule(SERVERS.PORT_1, new TestRule().always().drop()); getManagementServer(SERVERS.PORT_1).shutdown(); for (int i=0; i<PARAMETERS.NUM_ITERATIONS_LOW; i++){ corfuRuntime.invalidateLayout(); if (corfuRuntime.getLayoutView().getLayout().getEpoch() == 2L) {break;} Thread.sleep(PARAMETERS.TIMEOUT_VERY_SHORT.toMillis()); } Layout l2 = corfuRuntime.getLayoutView().getLayout(); assertThat(l2.getEpoch()).isEqualTo(2L); assertThat(l2.getLayoutServers().size()).isEqualTo(2); assertThat(l2.getLayoutServers().contains(SERVERS.ENDPOINT_1)).isFalse(); } protected void getManagementTestLayout() throws Exception { addServer(SERVERS.PORT_0); addServer(SERVERS.PORT_1); addServer(SERVERS.PORT_2); Layout l = new TestLayoutBuilder() .setEpoch(1L) .addLayoutServer(SERVERS.PORT_0) .addLayoutServer(SERVERS.PORT_1) .addLayoutServer(SERVERS.PORT_2) .addSequencer(SERVERS.PORT_1) .addSequencer(SERVERS.PORT_0) .addSequencer(SERVERS.PORT_2) .buildSegment() .buildStripe() .addLogUnit(SERVERS.PORT_0) .addLogUnit(SERVERS.PORT_2) .addToSegment() .addToLayout() .build(); bootstrapAllServers(l); corfuRuntime = getRuntime(l).connect(); // Initiating all failure handlers. for (String server: corfuRuntime.getLayoutView().getLayout().getAllServers()) { corfuRuntime.getRouter(server).getClient(ManagementClient.class).initiateFailureHandler().get(); } // Setting aggressive timeouts setAggressiveTimeouts(l, corfuRuntime, getManagementServer(SERVERS.PORT_0).getCorfuRuntime(), getManagementServer(SERVERS.PORT_1).getCorfuRuntime(), getManagementServer(SERVERS.PORT_2).getCorfuRuntime()); } /** * Scenario with 3 nodes: SERVERS.PORT_0, SERVERS.PORT_1 and SERVERS.PORT_2. * Simulate transient failure of a server leading to a partial seal. * Allow the management server to detect the partial seal and correct this. * <p> * Part 1. * The partial seal causes SERVERS.PORT_0 to be at epoch 2 whereas, * SERVERS.PORT_1 & SERVERS.PORT_2 fail to receive this message and are stuck at epoch 1. * <p> * Part 2. * All the 3 servers are now functional and receive all messages. * <p> * Part 3. * The PING message gets rejected by the partially sealed router (WrongEpoch) * and the management server realizes of the partial seal and corrects this * by issuing another failure detected message. * * @throws Exception */ @Test public void handleTransientFailure() throws Exception { // Boolean flag turned to true when the MANAGEMENT_FAILURE_DETECTED message // is sent by the Management client to its server. final Semaphore failureDetected = new Semaphore(2,true); addServer(SERVERS.PORT_0); addServer(SERVERS.PORT_1); addServer(SERVERS.PORT_2); Layout l = new TestLayoutBuilder() .setEpoch(1L) .addLayoutServer(SERVERS.PORT_0) .addLayoutServer(SERVERS.PORT_1) .addLayoutServer(SERVERS.PORT_2) .addSequencer(SERVERS.PORT_0) .buildSegment() .setReplicationMode(Layout.ReplicationMode.QUORUM_REPLICATION) .buildStripe() .addLogUnit(SERVERS.PORT_0) .addLogUnit(SERVERS.PORT_1) .addLogUnit(SERVERS.PORT_2) .addToSegment() .addToLayout() .build(); bootstrapAllServers(l); CorfuRuntime corfuRuntime = getRuntime(l).connect(); // Initiate SERVERS.ENDPOINT_0 failureHandler corfuRuntime.getRouter(SERVERS.ENDPOINT_0).getClient(ManagementClient.class).initiateFailureHandler().get(); // Set aggressive timeouts. setAggressiveTimeouts(l, corfuRuntime, getManagementServer(SERVERS.PORT_0).getCorfuRuntime(), getManagementServer(SERVERS.PORT_1).getCorfuRuntime(), getManagementServer(SERVERS.PORT_2).getCorfuRuntime()); failureDetected.acquire(2); // Only allow SERVERS.PORT_0 to manage failures. getManagementServer(SERVERS.PORT_1).shutdown(); getManagementServer(SERVERS.PORT_2).shutdown(); // PART 1. // Prevent ENDPOINT_1 from sealing. addClientRule(getManagementServer(SERVERS.PORT_0).getCorfuRuntime(), SERVERS.ENDPOINT_1, new TestRule().matches(corfuMsg -> corfuMsg.getMsgType().equals(CorfuMsgType.SET_EPOCH)).drop()); // Simulate ENDPOINT_2 failure from ENDPOINT_0 (only Management Server) addClientRule(getManagementServer(SERVERS.PORT_0).getCorfuRuntime(), SERVERS.ENDPOINT_2, new TestRule().matches(corfuMsg -> true).drop()); // Adding a rule on SERVERS.PORT_1 to toggle the flag when it sends the // MANAGEMENT_FAILURE_DETECTED message. addClientRule(getManagementServer(SERVERS.PORT_0).getCorfuRuntime(), new TestRule().matches(corfuMsg -> { if (corfuMsg.getMsgType().equals(CorfuMsgType.MANAGEMENT_FAILURE_DETECTED)) { failureDetected.release(); } return true; })); // Go ahead when sealing of ENDPOINT_0 takes place. for (int i = 0; i < PARAMETERS.NUM_ITERATIONS_LOW; i++) { if (getServerRouter(SERVERS.PORT_0).getServerEpoch() == 2L) { failureDetected.release(); break; } Thread.sleep(PARAMETERS.TIMEOUT_VERY_SHORT.toMillis()); } assertThat(failureDetected.tryAcquire(2, PARAMETERS.TIMEOUT_NORMAL.toNanos(), TimeUnit.NANOSECONDS)).isEqualTo(true); addClientRule(getManagementServer(SERVERS.PORT_0).getCorfuRuntime(), new TestRule().matches(corfuMsg -> corfuMsg.getMsgType().equals(CorfuMsgType.MANAGEMENT_FAILURE_DETECTED) ).drop()); // Assert that only a partial seal was successful. // ENDPOINT_0 sealed. ENDPOINT_1 & ENDPOINT_2 not sealed. assertThat(getServerRouter(SERVERS.PORT_0).getServerEpoch()).isEqualTo(2L); assertThat(getServerRouter(SERVERS.PORT_1).getServerEpoch()).isEqualTo(1L); assertThat(getServerRouter(SERVERS.PORT_2).getServerEpoch()).isEqualTo(1L); assertThat(getLayoutServer(SERVERS.PORT_0).getCurrentLayout().getEpoch()).isEqualTo(1L); assertThat(getLayoutServer(SERVERS.PORT_1).getCurrentLayout().getEpoch()).isEqualTo(1L); assertThat(getLayoutServer(SERVERS.PORT_2).getCurrentLayout().getEpoch()).isEqualTo(1L); // PART 2. // Simulate normal operations for all servers and clients. clearClientRules(getManagementServer(SERVERS.PORT_0).getCorfuRuntime()); // PART 3. // Allow management server to detect partial seal and correct this issue. addClientRule(getManagementServer(SERVERS.PORT_0).getCorfuRuntime(), new TestRule().matches(corfuMsg -> { if (corfuMsg.getMsgType().equals(CorfuMsgType.MANAGEMENT_FAILURE_DETECTED)) { failureDetected.release(2); } return true; })); assertThat(failureDetected.tryAcquire(2, PARAMETERS.TIMEOUT_NORMAL.toNanos(), TimeUnit.NANOSECONDS)).isEqualTo(true); for (int i = 0; i < PARAMETERS.NUM_ITERATIONS_LOW; i++) { Thread.sleep(PARAMETERS.TIMEOUT_VERY_SHORT.toMillis()); // Assert successful seal of all servers. if (getServerRouter(SERVERS.PORT_0).getServerEpoch() == 2L || getServerRouter(SERVERS.PORT_1).getServerEpoch() == 2L || getServerRouter(SERVERS.PORT_2).getServerEpoch() == 2L || getLayoutServer(SERVERS.PORT_0).getCurrentLayout().getEpoch() == 2L || getLayoutServer(SERVERS.PORT_1).getCurrentLayout().getEpoch() == 2L || getLayoutServer(SERVERS.PORT_2).getCurrentLayout().getEpoch() == 2L) { return; } } fail(); } protected void induceSequencerFailureAndWait() throws Exception { long currentEpoch = getCorfuRuntime().getLayoutView().getLayout().getEpoch(); // induce a failure to the server on PORT_1, where the current sequencer is active // getManagementServer(SERVERS.PORT_1).shutdown(); addServerRule(SERVERS.PORT_1, new TestRule().always().drop()); // wait for failover to install a new epoch (and a new layout) // while (getCorfuRuntime().getLayoutView().getLayout().getEpoch() == currentEpoch) { getCorfuRuntime().invalidateLayout(); Thread.sleep(PARAMETERS.TIMEOUT_VERY_SHORT.toMillis()); } } /** * Scenario with 3 nodes: SERVERS.PORT_0, SERVERS.PORT_1 and SERVERS.PORT_2. * We fail SERVERS.PORT_1 and then wait for one of the other two servers to * handle this failure, propose a new layout and we assert on the epoch change. * The failure is handled by ConserveFailureHandlerPolicy. * No nodes are removed from the layout, but are marked unresponsive. * A sequencer failover takes place where the next working sequencer is reset * and made the primary. * * @throws Exception */ @Test public void testSequencerFailover() throws Exception { getManagementTestLayout(); final long beforeFailure = 5L; final long afterFailure = 10L; IStreamView sv = getCorfuRuntime().getStreamsView().get(CorfuRuntime.getStreamID("streamA")); byte[] testPayload = "hello world".getBytes(); sv.append(testPayload); sv.append(testPayload); sv.append(testPayload); sv.append(testPayload); sv.append(testPayload); assertThat(getSequencer(SERVERS.PORT_1).getGlobalLogTail().get()).isEqualTo(beforeFailure); assertThat(getSequencer(SERVERS.PORT_0).getGlobalLogTail().get()).isEqualTo(0L); induceSequencerFailureAndWait(); // verify that a failover sequencer was started with the correct starting-tail // assertThat(getSequencer(SERVERS.PORT_0).getGlobalLogTail().get()).isEqualTo(beforeFailure); sv.append(testPayload); sv.append(testPayload); sv.append(testPayload); sv.append(testPayload); sv.append(testPayload); // verify the failover layout // Layout expectedLayout = new TestLayoutBuilder() .setEpoch(2L) .addLayoutServer(SERVERS.PORT_0) .addLayoutServer(SERVERS.PORT_1) .addLayoutServer(SERVERS.PORT_2) .addSequencer(SERVERS.PORT_0) .addSequencer(SERVERS.PORT_1) .addSequencer(SERVERS.PORT_2) .buildSegment() .buildStripe() .addLogUnit(SERVERS.PORT_0) .addLogUnit(SERVERS.PORT_2) .addToSegment() .addToLayout() .addUnresponsiveServer(SERVERS.PORT_1) .build(); assertThat(getCorfuRuntime().getLayoutView().getLayout()).isEqualTo(expectedLayout); // verify that the new sequencer is advancing the tail properly assertThat(getSequencer(SERVERS.PORT_0).getGlobalLogTail().get()).isEqualTo(afterFailure); // sanity check that no other sequencer is active assertThat(getSequencer(SERVERS.PORT_2).getGlobalLogTail().get()).isEqualTo(0L); } protected <T> Object instantiateCorfuObject(TypeToken<T> tType, String name) { return (T) getCorfuRuntime().getObjectsView() .build() .setStreamName(name) // stream name .setTypeToken(tType) // a TypeToken of the specified class .open(); // instantiate the object! } protected ISMRMap<Integer, String> getMap() { ISMRMap<Integer, String> testMap; testMap = (ISMRMap<Integer, String>) instantiateCorfuObject( new TypeToken<SMRMap<Integer, String>>() { }, "test stream" ) ; return testMap; } protected void TXBegin() { getCorfuRuntime().getObjectsView().TXBegin(); } protected void TXEnd() { getCorfuRuntime().getObjectsView().TXEnd(); } /** * check that transcation conflict resolution works properly in face of sequencer failover */ @Test public void ckSequencerFailoverTXResolution() throws Exception { getManagementTestLayout(); Map<Integer, String> map = getMap(); // start a transaction and force it to obtain snapshot timestamp // preceding the sequencer failover t(0, () -> { TXBegin(); map.get(0); }); final String payload = "hello"; final int nUpdates = 5; // in another thread, fill the log with a few entries t(1, () -> { for (int i = 0; i < nUpdates; i++) map.put(i, payload); }); // now, the tail of the log is at nUpdates; // kill the sequencer, wait for a failover, // and then resume the transaction above; it should abort // (unnecessarily, but we are being conservative) // induceSequencerFailureAndWait(); t(0, () -> { boolean commit = true; map.put(nUpdates+1, payload); // should not conflict try { TXEnd(); } catch (TransactionAbortedException ta) { commit = false; } assertThat(commit) .isFalse(); }); // now, check that the same scenario, starting anew, can succeed t(0, () -> { TXBegin(); map.get(0); }); // in another thread, fill the log with a few entries t(1, () -> { for (int i = 0; i < nUpdates; i++) map.put(i, payload+1); }); t(0, () -> { boolean commit = true; map.put(nUpdates+1, payload); // should not conflict try { TXEnd(); } catch (TransactionAbortedException ta) { commit = false; } assertThat(commit) .isTrue(); }); } /** * small variant on the above : don't start the first TX at the start of the log. */ @Test public void ckSequencerFailoverTXResolution1() throws Exception { getManagementTestLayout(); Map<Integer, String> map = getMap(); final String payload = "hello"; final int nUpdates = 5; for (int i = 0; i < nUpdates; i++) map.put(i, payload); // start a transaction and force it to obtain snapshot timestamp // preceding the sequencer failover t(0, () -> { TXBegin(); map.get(0); }); // in another thread, fill the log with a few entries t(1, () -> { for (int i = 0; i < nUpdates; i++) map.put(i, payload+1); }); // now, the tail of the log is at nUpdates; // kill the sequencer, wait for a failover, // and then resume the transaction above; it should abort // (unnecessarily, but we are being conservative) // induceSequencerFailureAndWait(); t(0, () -> { boolean commit = true; map.put(nUpdates+1, payload); // should not conflict try { TXEnd(); } catch (TransactionAbortedException ta) { commit = false; } assertThat(commit) .isFalse(); }); // now, check that the same scenario, starting anew, can succeed t(0, () -> { TXBegin(); map.get(0); }); // in another thread, fill the log with a few entries t(1, () -> { for (int i = 0; i < nUpdates; i++) map.put(i, payload+2); }); t(0, () -> { boolean commit = true; map.put(nUpdates+1, payload); // should not conflict try { TXEnd(); } catch (TransactionAbortedException ta) { commit = false; } assertThat(commit) .isTrue(); }); } /** * When a stream is seen for the first time by the sequencer it returns a -1 * in the backpointer map. * After failover, the new sequencer returns a null in the backpointer map * forcing it to single step backwards and get the last backpointer for the * given stream. * An example is shown below: * <p> * Index : 0 1 2 3 | | 4 5 6 7 8 * Stream : A B A B | failover | A C A B B * B.P : -1 -1 0 1 | | X X 4 X 7 * <p> * -1 : New StreamID so empty backpointers * X : (null) Unknown backpointers as this is a failed-over sequencer. * <p> * @throws Exception */ @Test public void sequencerFailoverBackpointerCheck() throws Exception { getManagementTestLayout(); UUID streamA = UUID.nameUUIDFromBytes("stream A".getBytes()); UUID streamB = UUID.nameUUIDFromBytes("stream B".getBytes()); UUID streamC = UUID.nameUUIDFromBytes("stream C".getBytes()); final long streamA_backpointer = 4L; final long streamB_backpointer = 7L; getTokenWriteAndAssertBackPointer(streamA, Address.NON_EXIST); getTokenWriteAndAssertBackPointer(streamB, Address.NON_EXIST); getTokenWriteAndAssertBackPointer(streamA, 0L); getTokenWriteAndAssertBackPointer(streamB, 1L); induceSequencerFailureAndWait(); getTokenWriteAndAssertBackPointer(streamA, Address.NO_BACKPOINTER); getTokenWriteAndAssertBackPointer(streamC, Address.NO_BACKPOINTER); getTokenWriteAndAssertBackPointer(streamA, streamA_backpointer); getTokenWriteAndAssertBackPointer(streamB, Address.NO_BACKPOINTER); getTokenWriteAndAssertBackPointer(streamB, streamB_backpointer); } /** * Requests for a token for the given stream ID. * Asserts the backpointer map in the token response with the specified backpointer location. * Writes test data to the log unit servers using the tokenResponse. * @param streamID Stream ID to request token for. * @param expectedBackpointerValue Expected backpointer for given stream. */ private void getTokenWriteAndAssertBackPointer(UUID streamID, Long expectedBackpointerValue) { TokenResponse tokenResponse = corfuRuntime.getSequencerView().nextToken(Collections.singleton(streamID), 1); if (expectedBackpointerValue == null) { assertThat(tokenResponse.getBackpointerMap()).isEmpty(); } else { assertThat(tokenResponse.getBackpointerMap()).containsEntry(streamID, expectedBackpointerValue); } corfuRuntime.getAddressSpaceView().write(tokenResponse, "test".getBytes()); } }