/*
* Copyright © 2014 Cask Data, Inc.
*
* 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 co.cask.cdap.data2.transaction.stream;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.namespace.NamespacedLocationFactory;
import co.cask.cdap.data.stream.StreamFileOffset;
import co.cask.cdap.data.stream.StreamFileType;
import co.cask.cdap.data.stream.StreamUtils;
import co.cask.cdap.proto.Id;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.apache.twill.filesystem.Location;
import org.junit.Assert;
import org.junit.Test;
import java.io.IOException;
import java.util.List;
import java.util.Set;
/**
*
*/
public abstract class StreamConsumerStateTestBase {
protected abstract StreamConsumerStateStore createStateStore(StreamConfig streamConfig) throws Exception;
protected abstract StreamAdmin getStreamAdmin();
protected static CConfiguration cConf = CConfiguration.create();
protected static final Id.Namespace TEST_NAMESPACE = Id.Namespace.from("streamConsumerStateTestNamespace");
protected static final Id.Namespace OTHER_NAMESPACE = Id.Namespace.from("otherNamespace");
protected static void setupNamespaces(NamespacedLocationFactory namespacedLocationFactory) throws IOException {
namespacedLocationFactory.get(TEST_NAMESPACE).mkdirs();
namespacedLocationFactory.get(OTHER_NAMESPACE).mkdirs();
}
@Test
public void testStateExists() throws Exception {
StreamAdmin streamAdmin = getStreamAdmin();
String streamName = "testStateExists";
Id.Stream streamId = Id.Stream.from(TEST_NAMESPACE, streamName);
streamAdmin.create(streamId);
StreamConfig config = streamAdmin.getConfig(streamId);
StreamConsumerStateStore stateStore = createStateStore(config);
streamAdmin.configureInstances(Id.Stream.from(TEST_NAMESPACE, streamName), 0L, 1);
// Get a consumer state that is configured
StreamConsumerState state = stateStore.get(0L, 0);
Assert.assertNotNull(state);
// Try to get a consumer state that not configured yet.
state = stateStore.get(0L, 1);
Assert.assertNull(state);
}
@Test
public void testStore() throws Exception {
StreamAdmin streamAdmin = getStreamAdmin();
String streamName = "testStore";
Id.Stream streamId = Id.Stream.from(TEST_NAMESPACE, streamName);
streamAdmin.create(streamId);
StreamConfig config = streamAdmin.getConfig(streamId);
// Creates a state with 4 offsets
StreamConsumerState state = generateState(0L, 0, config, 0L, 4);
StreamConsumerStateStore stateStore = createStateStore(config);
// Save the state.
stateStore.save(state);
// Read the state back
StreamConsumerState readState = stateStore.get(0, 0);
Assert.assertEquals(state, readState);
}
@Test
public void testNamespacedStore() throws Exception {
// Store different offsets for two streams using the same StateStoreFactory to show that
// StateStoreFactory is capable of storing distinct states for streams with same name but different namespace
StreamAdmin streamAdmin = getStreamAdmin();
String streamName = "testNamespacedStore";
Id.Stream streamId = Id.Stream.from(TEST_NAMESPACE, streamName);
Id.Stream otherStreamId = Id.Stream.from(OTHER_NAMESPACE, streamName);
streamAdmin.create(streamId);
streamAdmin.create(otherStreamId);
StreamConfig config = streamAdmin.getConfig(streamId);
StreamConfig otherConfig = streamAdmin.getConfig(otherStreamId);
// Creates a state with 4 offsets
StreamConsumerState state = generateState(0L, 0, config, 0L, 4);
StreamConsumerStateStore stateStore = createStateStore(config);
// Create another state with more offsets for stream in different namespace
StreamConsumerState otherState = generateState(0L, 0, otherConfig, 0L, 8);
StreamConsumerStateStore otherStateStore = createStateStore(otherConfig);
// Save the states.
stateStore.save(state);
otherStateStore.save(otherState);
// Read the state back
StreamConsumerState readState = stateStore.get(0, 0);
StreamConsumerState otherReadState = otherStateStore.get(0, 0);
Assert.assertEquals(state, readState);
Assert.assertEquals(otherState, otherReadState);
Assert.assertNotEquals(state, otherState);
}
@Test
public void testMultiStore() throws Exception {
StreamAdmin streamAdmin = getStreamAdmin();
String streamName = "testMultiStore";
Id.Stream streamId = Id.Stream.from(TEST_NAMESPACE, streamName);
streamAdmin.create(streamId);
StreamConfig config = streamAdmin.getConfig(streamId);
// Creates 4 states of 2 groups, each with 4 offsets
Set<StreamConsumerState> states = Sets.newHashSet();
for (int i = 0; i < 4; i++) {
states.add(generateState(i % 2, i, config, 0L, 4));
}
StreamConsumerStateStore stateStore = createStateStore(config);
stateStore.save(states);
// Read all states back
Set<StreamConsumerState> readStates = Sets.newHashSet();
stateStore.getAll(readStates);
Assert.assertEquals(states, readStates);
}
@Test
public void testRemove() throws Exception {
StreamAdmin streamAdmin = getStreamAdmin();
String streamName = "testRemove";
Id.Stream streamId = Id.Stream.from(TEST_NAMESPACE, streamName);
streamAdmin.create(streamId);
StreamConfig config = streamAdmin.getConfig(streamId);
// Creates 4 states of 2 groups, each with 4 offsets
Set<StreamConsumerState> states = Sets.newHashSet();
for (int i = 0; i < 4; i++) {
states.add(generateState(i % 2, i, config, 0L, 4));
}
StreamConsumerStateStore stateStore = createStateStore(config);
stateStore.save(states);
// Read all states back
Set<StreamConsumerState> readStates = Sets.newHashSet();
stateStore.getAll(readStates);
Assert.assertEquals(states, readStates);
// Remove groupId 0
Set<StreamConsumerState> removeStates = Sets.newHashSet();
for (StreamConsumerState state : readStates) {
if (state.getGroupId() == 0) {
removeStates.add(state);
}
}
stateStore.remove(removeStates);
// Read all states back
readStates.clear();
stateStore.getAll(readStates);
Assert.assertEquals(2, readStates.size());
for (StreamConsumerState state : readStates) {
Assert.assertEquals(1L, state.getGroupId());
}
}
@Test
public void testChangeInstance() throws Exception {
StreamAdmin streamAdmin = getStreamAdmin();
String streamName = "testChangeInstance";
Id.Stream streamId = Id.Stream.from(TEST_NAMESPACE, streamName);
streamAdmin.create(streamId);
StreamConfig config = streamAdmin.getConfig(streamId);
// Creates a state with 4 offsets
StreamConsumerState state = generateState(0L, 0, config, 0L, 4);
StreamConsumerStateStore stateStore = createStateStore(config);
// Save the state.
stateStore.save(state);
// Increase the number of instances
streamAdmin.configureInstances(streamId, 0L, 2);
StreamConsumerState newState = stateStore.get(0L, 1);
// Get the state of the new instance, should be the same as the existing one
Assert.assertTrue(Iterables.elementsEqual(state.getState(), newState.getState()));
// Change the state of instance 0 to higher offset.
List<StreamFileOffset> fileOffsets = Lists.newArrayList(state.getState());
StreamFileOffset fileOffset = fileOffsets.get(0);
long oldOffset = fileOffset.getOffset();
long newOffset = oldOffset + 100000;
fileOffsets.set(0, new StreamFileOffset(fileOffset, newOffset));
state.setState(fileOffsets);
stateStore.save(state);
// Verify the change
state = stateStore.get(0L, 0);
Assert.assertEquals(newOffset, Iterables.get(state.getState(), 0).getOffset());
// Increase the number of instances again
streamAdmin.configureInstances(streamId, 0L, 3);
// Verify that instance 0 has offset getting resetted to lowest
state = stateStore.get(0L, 0);
Assert.assertEquals(oldOffset, Iterables.get(state.getState(), 0).getOffset());
// Verify that no new file offsets state is being introduced (test a bug in the configureInstance implementation)
Assert.assertEquals(4, Iterables.size(state.getState()));
// Verify that all offsets are the same
List<StreamConsumerState> states = Lists.newArrayList();
stateStore.getByGroup(0L, states);
Assert.assertEquals(3, states.size());
Assert.assertTrue(Iterables.elementsEqual(states.get(0).getState(), states.get(1).getState()));
Assert.assertTrue(Iterables.elementsEqual(states.get(0).getState(), states.get(2).getState()));
}
private StreamConsumerState generateState(long groupId, int instanceId, StreamConfig config,
long partitionBaseTime, int numOffsets) throws IOException {
List<StreamFileOffset> offsets = Lists.newArrayList();
long partitionDuration = config.getPartitionDuration();
for (int i = 0; i < numOffsets; i++) {
Location partitionLocation = StreamUtils.createPartitionLocation(config.getLocation(),
(partitionBaseTime + i) * partitionDuration,
config.getPartitionDuration());
offsets.add(new StreamFileOffset(StreamUtils.createStreamLocation(partitionLocation,
"file", 0, StreamFileType.EVENT), i * 1000, 0));
}
return new StreamConsumerState(groupId, instanceId, offsets);
}
}