/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.accumulo.tserver.replication; import static org.easymock.EasyMock.createMock; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.verify; import static org.junit.Assert.assertEquals; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.UUID; import org.apache.accumulo.core.conf.AccumuloConfiguration; import org.apache.accumulo.core.conf.ConfigurationCopy; import org.apache.accumulo.core.conf.Property; import org.apache.accumulo.core.data.Mutation; import org.apache.accumulo.core.data.Value; import org.apache.accumulo.core.data.impl.KeyExtent; import org.apache.accumulo.core.replication.ReplicationTarget; import org.apache.accumulo.core.replication.thrift.ReplicationServicer.Client; import org.apache.accumulo.core.replication.thrift.WalEdits; import org.apache.accumulo.core.security.thrift.TCredentials; import org.apache.accumulo.server.data.ServerMutation; import org.apache.accumulo.server.replication.proto.Replication.Status; import org.apache.accumulo.tserver.logger.LogEvents; import org.apache.accumulo.tserver.logger.LogFileKey; import org.apache.accumulo.tserver.logger.LogFileValue; import org.apache.accumulo.tserver.replication.AccumuloReplicaSystem.ReplicationStats; import org.apache.accumulo.tserver.replication.AccumuloReplicaSystem.WalClientExecReturn; import org.apache.accumulo.tserver.replication.AccumuloReplicaSystem.WalReplication; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.Text; import org.junit.Assert; import org.junit.Test; public class AccumuloReplicaSystemTest { @Test public void onlyChooseMutationsForDesiredTableWithOpenStatus() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos); LogFileKey key = new LogFileKey(); LogFileValue value = new LogFileValue(); // What is seq used for? key.seq = 1l; /* * Disclaimer: the following series of LogFileKey and LogFileValue pairs have *no* bearing whatsoever in reality regarding what these entries would actually * look like in a WAL. They are solely for testing that each LogEvents is handled, order is not important. */ key.event = LogEvents.DEFINE_TABLET; key.tablet = new KeyExtent("1", null, null); key.tid = 1; key.write(dos); value.write(dos); key.tablet = null; key.event = LogEvents.MUTATION; key.filename = "/accumulo/wals/tserver+port/" + UUID.randomUUID(); value.mutations = Arrays.<Mutation> asList(new ServerMutation(new Text("row"))); key.write(dos); value.write(dos); key.event = LogEvents.DEFINE_TABLET; key.tablet = new KeyExtent("2", null, null); key.tid = 2; value.mutations = Collections.emptyList(); key.write(dos); value.write(dos); key.event = LogEvents.OPEN; key.tid = LogFileKey.VERSION; key.tserverSession = "foobar"; key.write(dos); value.write(dos); key.tablet = null; key.event = LogEvents.MUTATION; key.filename = "/accumulo/wals/tserver+port/" + UUID.randomUUID(); value.mutations = Arrays.<Mutation> asList(new ServerMutation(new Text("badrow"))); key.write(dos); value.write(dos); key.event = LogEvents.COMPACTION_START; key.tid = 2; key.filename = "/accumulo/tables/1/t-000001/A000001.rf"; value.mutations = Collections.emptyList(); key.write(dos); value.write(dos); key.event = LogEvents.DEFINE_TABLET; key.tablet = new KeyExtent("1", null, null); key.tid = 3; value.mutations = Collections.emptyList(); key.write(dos); value.write(dos); key.event = LogEvents.COMPACTION_FINISH; key.tid = 6; value.mutations = Collections.emptyList(); key.write(dos); value.write(dos); key.tablet = null; key.event = LogEvents.MUTATION; key.tid = 3; key.filename = "/accumulo/wals/tserver+port/" + UUID.randomUUID(); value.mutations = Arrays.<Mutation> asList(new ServerMutation(new Text("row"))); key.write(dos); value.write(dos); dos.close(); Map<String,String> confMap = new HashMap<>(); confMap.put(Property.REPLICATION_NAME.getKey(), "source"); AccumuloConfiguration conf = new ConfigurationCopy(confMap); AccumuloReplicaSystem ars = new AccumuloReplicaSystem(); ars.setConf(conf); Status status = Status.newBuilder().setBegin(0).setEnd(0).setInfiniteEnd(true).setClosed(false).build(); DataInputStream dis = new DataInputStream(new ByteArrayInputStream(baos.toByteArray())); WalReplication repl = ars.getWalEdits(new ReplicationTarget("peer", "1", "1"), dis, new Path("/accumulo/wals/tserver+port/wal"), status, Long.MAX_VALUE, new HashSet<Integer>()); // We stopped because we got to the end of the file Assert.assertEquals(9, repl.entriesConsumed); Assert.assertEquals(2, repl.walEdits.getEditsSize()); Assert.assertEquals(2, repl.sizeInRecords); Assert.assertNotEquals(0, repl.sizeInBytes); } @Test public void onlyChooseMutationsForDesiredTableWithClosedStatus() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos); LogFileKey key = new LogFileKey(); LogFileValue value = new LogFileValue(); // What is seq used for? key.seq = 1l; /* * Disclaimer: the following series of LogFileKey and LogFileValue pairs have *no* bearing whatsoever in reality regarding what these entries would actually * look like in a WAL. They are solely for testing that each LogEvents is handled, order is not important. */ key.event = LogEvents.DEFINE_TABLET; key.tablet = new KeyExtent("1", null, null); key.tid = 1; key.write(dos); value.write(dos); key.tablet = null; key.event = LogEvents.MUTATION; key.filename = "/accumulo/wals/tserver+port/" + UUID.randomUUID(); value.mutations = Arrays.<Mutation> asList(new ServerMutation(new Text("row"))); key.write(dos); value.write(dos); key.event = LogEvents.DEFINE_TABLET; key.tablet = new KeyExtent("2", null, null); key.tid = 2; value.mutations = Collections.emptyList(); key.write(dos); value.write(dos); key.event = LogEvents.OPEN; key.tid = LogFileKey.VERSION; key.tserverSession = "foobar"; key.write(dos); value.write(dos); key.tablet = null; key.event = LogEvents.MUTATION; key.filename = "/accumulo/wals/tserver+port/" + UUID.randomUUID(); value.mutations = Arrays.<Mutation> asList(new ServerMutation(new Text("badrow"))); key.write(dos); value.write(dos); key.event = LogEvents.COMPACTION_START; key.tid = 2; key.filename = "/accumulo/tables/1/t-000001/A000001.rf"; value.mutations = Collections.emptyList(); key.write(dos); value.write(dos); key.event = LogEvents.DEFINE_TABLET; key.tablet = new KeyExtent("1", null, null); key.tid = 3; value.mutations = Collections.emptyList(); key.write(dos); value.write(dos); key.event = LogEvents.COMPACTION_FINISH; key.tid = 6; value.mutations = Collections.emptyList(); key.write(dos); value.write(dos); key.tablet = null; key.event = LogEvents.MUTATION; key.tid = 3; key.filename = "/accumulo/wals/tserver+port/" + UUID.randomUUID(); value.mutations = Arrays.<Mutation> asList(new ServerMutation(new Text("row"))); key.write(dos); value.write(dos); dos.close(); Map<String,String> confMap = new HashMap<>(); confMap.put(Property.REPLICATION_NAME.getKey(), "source"); AccumuloConfiguration conf = new ConfigurationCopy(confMap); AccumuloReplicaSystem ars = new AccumuloReplicaSystem(); ars.setConf(conf); // Setting the file to be closed with the infinite end implies that we need to bump the begin up to Long.MAX_VALUE // If it were still open, more data could be appended that we need to process Status status = Status.newBuilder().setBegin(0).setEnd(0).setInfiniteEnd(true).setClosed(true).build(); DataInputStream dis = new DataInputStream(new ByteArrayInputStream(baos.toByteArray())); WalReplication repl = ars.getWalEdits(new ReplicationTarget("peer", "1", "1"), dis, new Path("/accumulo/wals/tserver+port/wal"), status, Long.MAX_VALUE, new HashSet<Integer>()); // We stopped because we got to the end of the file Assert.assertEquals(Long.MAX_VALUE, repl.entriesConsumed); Assert.assertEquals(2, repl.walEdits.getEditsSize()); Assert.assertEquals(2, repl.sizeInRecords); Assert.assertNotEquals(0, repl.sizeInBytes); } @Test public void mutationsNotReReplicatedToPeers() throws Exception { AccumuloReplicaSystem ars = new AccumuloReplicaSystem(); Map<String,String> confMap = new HashMap<>(); confMap.put(Property.REPLICATION_NAME.getKey(), "source"); AccumuloConfiguration conf = new ConfigurationCopy(confMap); ars.setConf(conf); LogFileValue value = new LogFileValue(); value.mutations = new ArrayList<>(); Mutation m = new Mutation("row"); m.put("", "", new Value(new byte[0])); value.mutations.add(m); m = new Mutation("row2"); m.put("", "", new Value(new byte[0])); m.addReplicationSource("peer"); value.mutations.add(m); ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream out = new DataOutputStream(baos); // Replicate our 2 mutations to "peer", from tableid 1 to tableid 1 ars.writeValueAvoidingReplicationCycles(out, value, new ReplicationTarget("peer", "1", "1")); out.close(); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); DataInputStream in = new DataInputStream(bais); int numMutations = in.readInt(); Assert.assertEquals(1, numMutations); m = new Mutation(); m.readFields(in); Assert.assertEquals("row", new String(m.getRow())); Assert.assertEquals(1, m.getReplicationSources().size()); Assert.assertTrue("Expected source cluster to be listed in mutation replication source", m.getReplicationSources().contains("source")); } @Test public void endOfFileExceptionOnClosedWalImpliesFullyReplicated() throws Exception { Map<String,String> confMap = new HashMap<>(); confMap.put(Property.REPLICATION_NAME.getKey(), "source"); AccumuloConfiguration conf = new ConfigurationCopy(confMap); AccumuloReplicaSystem ars = new AccumuloReplicaSystem(); ars.setConf(conf); // Setting the file to be closed with the infinite end implies that we need to bump the begin up to Long.MAX_VALUE // If it were still open, more data could be appended that we need to process Status status = Status.newBuilder().setBegin(100).setEnd(0).setInfiniteEnd(true).setClosed(true).build(); DataInputStream dis = new DataInputStream(new ByteArrayInputStream(new byte[0])); WalReplication repl = ars.getWalEdits(new ReplicationTarget("peer", "1", "1"), dis, new Path("/accumulo/wals/tserver+port/wal"), status, Long.MAX_VALUE, new HashSet<Integer>()); // We stopped because we got to the end of the file Assert.assertEquals(Long.MAX_VALUE, repl.entriesConsumed); Assert.assertEquals(0, repl.walEdits.getEditsSize()); Assert.assertEquals(0, repl.sizeInRecords); Assert.assertEquals(0, repl.sizeInBytes); } @Test public void endOfFileExceptionOnOpenWalImpliesMoreReplication() throws Exception { Map<String,String> confMap = new HashMap<>(); confMap.put(Property.REPLICATION_NAME.getKey(), "source"); AccumuloConfiguration conf = new ConfigurationCopy(confMap); AccumuloReplicaSystem ars = new AccumuloReplicaSystem(); ars.setConf(conf); // Setting the file to be closed with the infinite end implies that we need to bump the begin up to Long.MAX_VALUE // If it were still open, more data could be appended that we need to process Status status = Status.newBuilder().setBegin(100).setEnd(0).setInfiniteEnd(true).setClosed(false).build(); DataInputStream dis = new DataInputStream(new ByteArrayInputStream(new byte[0])); WalReplication repl = ars.getWalEdits(new ReplicationTarget("peer", "1", "1"), dis, new Path("/accumulo/wals/tserver+port/wal"), status, Long.MAX_VALUE, new HashSet<Integer>()); // We stopped because we got to the end of the file Assert.assertEquals(0, repl.entriesConsumed); Assert.assertEquals(0, repl.walEdits.getEditsSize()); Assert.assertEquals(0, repl.sizeInRecords); Assert.assertEquals(0, repl.sizeInBytes); } @Test public void restartInFileKnowsAboutPreviousTableDefines() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos); LogFileKey key = new LogFileKey(); LogFileValue value = new LogFileValue(); // What is seq used for? key.seq = 1l; /* * Disclaimer: the following series of LogFileKey and LogFileValue pairs have *no* bearing whatsoever in reality regarding what these entries would actually * look like in a WAL. They are solely for testing that each LogEvents is handled, order is not important. */ key.event = LogEvents.DEFINE_TABLET; key.tablet = new KeyExtent("1", null, null); key.tid = 1; key.write(dos); value.write(dos); key.tablet = null; key.event = LogEvents.MUTATION; key.filename = "/accumulo/wals/tserver+port/" + UUID.randomUUID(); value.mutations = Arrays.<Mutation> asList(new ServerMutation(new Text("row"))); key.write(dos); value.write(dos); key.tablet = null; key.event = LogEvents.MUTATION; key.tid = 1; key.filename = "/accumulo/wals/tserver+port/" + UUID.randomUUID(); value.mutations = Arrays.<Mutation> asList(new ServerMutation(new Text("row"))); key.write(dos); value.write(dos); dos.close(); Map<String,String> confMap = new HashMap<>(); confMap.put(Property.REPLICATION_NAME.getKey(), "source"); AccumuloConfiguration conf = new ConfigurationCopy(confMap); AccumuloReplicaSystem ars = new AccumuloReplicaSystem(); ars.setConf(conf); Status status = Status.newBuilder().setBegin(0).setEnd(0).setInfiniteEnd(true).setClosed(false).build(); DataInputStream dis = new DataInputStream(new ByteArrayInputStream(baos.toByteArray())); HashSet<Integer> tids = new HashSet<>(); // Only consume the first mutation, not the second WalReplication repl = ars.getWalEdits(new ReplicationTarget("peer", "1", "1"), dis, new Path("/accumulo/wals/tserver+port/wal"), status, 1l, tids); // We stopped because we got to the end of the file Assert.assertEquals(2, repl.entriesConsumed); Assert.assertEquals(1, repl.walEdits.getEditsSize()); Assert.assertEquals(1, repl.sizeInRecords); Assert.assertNotEquals(0, repl.sizeInBytes); status = Status.newBuilder(status).setBegin(2).build(); // Consume the rest of the mutations repl = ars.getWalEdits(new ReplicationTarget("peer", "1", "1"), dis, new Path("/accumulo/wals/tserver+port/wal"), status, 1l, tids); // We stopped because we got to the end of the file Assert.assertEquals(1, repl.entriesConsumed); Assert.assertEquals(1, repl.walEdits.getEditsSize()); Assert.assertEquals(1, repl.sizeInRecords); Assert.assertNotEquals(0, repl.sizeInBytes); } @Test public void dontSendEmptyDataToPeer() throws Exception { Client replClient = createMock(Client.class); AccumuloReplicaSystem ars = createMock(AccumuloReplicaSystem.class); WalEdits edits = new WalEdits(Collections.<ByteBuffer> emptyList()); WalReplication walReplication = new WalReplication(edits, 0, 0, 0); ReplicationTarget target = new ReplicationTarget("peer", "2", "1"); DataInputStream input = null; Path p = new Path("/accumulo/wals/tserver+port/" + UUID.randomUUID().toString()); Status status = null; long sizeLimit = Long.MAX_VALUE; String remoteTableId = target.getRemoteIdentifier(); TCredentials tcreds = null; Set<Integer> tids = new HashSet<>(); WalClientExecReturn walClientExec = ars.new WalClientExecReturn(target, input, p, status, sizeLimit, remoteTableId, tcreds, tids); expect(ars.getWalEdits(target, input, p, status, sizeLimit, tids)).andReturn(walReplication); replay(replClient, ars); ReplicationStats stats = walClientExec.execute(replClient); verify(replClient, ars); Assert.assertEquals(new ReplicationStats(0l, 0l, 0l), stats); } @Test public void consumedButNotSentDataShouldBeRecorded() throws Exception { Client replClient = createMock(Client.class); AccumuloReplicaSystem ars = createMock(AccumuloReplicaSystem.class); WalEdits edits = new WalEdits(Collections.<ByteBuffer> emptyList()); WalReplication walReplication = new WalReplication(edits, 0, 5, 0); ReplicationTarget target = new ReplicationTarget("peer", "2", "1"); DataInputStream input = null; Path p = new Path("/accumulo/wals/tserver+port/" + UUID.randomUUID().toString()); Status status = null; long sizeLimit = Long.MAX_VALUE; String remoteTableId = target.getRemoteIdentifier(); TCredentials tcreds = null; Set<Integer> tids = new HashSet<>(); WalClientExecReturn walClientExec = ars.new WalClientExecReturn(target, input, p, status, sizeLimit, remoteTableId, tcreds, tids); expect(ars.getWalEdits(target, input, p, status, sizeLimit, tids)).andReturn(walReplication); replay(replClient, ars); ReplicationStats stats = walClientExec.execute(replClient); verify(replClient, ars); Assert.assertEquals(new ReplicationStats(0l, 0l, 5l), stats); } @Test public void testUserPassword() throws Exception { AccumuloReplicaSystem ars = new AccumuloReplicaSystem(); ReplicationTarget target = new ReplicationTarget("peer", "peer_table", "1"); String user = "user", password = "password"; Map<String,String> confMap = new HashMap<>(); confMap.put(Property.REPLICATION_PEER_USER.getKey() + target.getPeerName(), user); confMap.put(Property.REPLICATION_PEER_PASSWORD.getKey() + target.getPeerName(), password); AccumuloConfiguration conf = new ConfigurationCopy(confMap); assertEquals(user, ars.getPrincipal(conf, target)); assertEquals(password, ars.getPassword(conf, target)); } @Test public void testUserKeytab() throws Exception { AccumuloReplicaSystem ars = new AccumuloReplicaSystem(); ReplicationTarget target = new ReplicationTarget("peer", "peer_table", "1"); String user = "user", keytab = "/etc/security/keytabs/replication.keytab"; Map<String,String> confMap = new HashMap<>(); confMap.put(Property.REPLICATION_PEER_USER.getKey() + target.getPeerName(), user); confMap.put(Property.REPLICATION_PEER_KEYTAB.getKey() + target.getPeerName(), keytab); AccumuloConfiguration conf = new ConfigurationCopy(confMap); assertEquals(user, ars.getPrincipal(conf, target)); assertEquals(keytab, ars.getKeytab(conf, target)); } }