/*
* Copyright © 2014-2015 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.data.stream;
import co.cask.cdap.api.flow.flowlet.StreamEvent;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.namespace.NamespacedLocationFactory;
import co.cask.cdap.data.file.FileWriter;
import co.cask.cdap.data2.transaction.stream.StreamAdmin;
import co.cask.cdap.data2.transaction.stream.StreamConfig;
import co.cask.cdap.proto.Id;
import co.cask.cdap.proto.NamespaceMeta;
import co.cask.cdap.store.NamespaceStore;
import org.apache.twill.filesystem.Location;
import org.apache.twill.filesystem.LocationFactory;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.util.Properties;
/**
* Base test class for stream file janitor.
*/
public abstract class StreamFileJanitorTestBase {
@ClassRule
public static TemporaryFolder tmpFolder = new TemporaryFolder();
protected static CConfiguration cConf = CConfiguration.create();
protected abstract LocationFactory getLocationFactory();
protected abstract NamespacedLocationFactory getNamespacedLocationFactory();
protected abstract StreamAdmin getStreamAdmin();
protected abstract NamespaceStore getNamespaceStore();
protected abstract CConfiguration getCConfiguration();
protected abstract FileWriter<StreamEvent> createWriter(Id.Stream streamId) throws IOException;
@Before
public void setup() throws Exception {
// FileStreamAdmin expects namespace directory to exist.
// Simulate namespace create, since its an inmemory-namespace admin
getNamespaceStore().create(NamespaceMeta.DEFAULT);
getNamespacedLocationFactory().get(Id.Namespace.DEFAULT).mkdirs();
}
@Test
public void testCleanupGeneration() throws Exception {
// Create a stream and performs couple truncate
String streamName = "testCleanupGeneration";
Id.Stream streamId = Id.Stream.from(Id.Namespace.DEFAULT, streamName);
StreamAdmin streamAdmin = getStreamAdmin();
streamAdmin.create(streamId);
StreamConfig streamConfig = streamAdmin.getConfig(streamId);
StreamFileJanitor janitor = new StreamFileJanitor(getCConfiguration(), getStreamAdmin(),
getNamespacedLocationFactory(), getNamespaceStore());
for (int i = 0; i < 5; i++) {
FileWriter<StreamEvent> writer = createWriter(streamId);
writer.append(StreamFileTestUtils.createEvent(System.currentTimeMillis(), "Testing"));
writer.close();
// Call cleanup before truncate. The current generation should stand.
janitor.clean(streamConfig.getLocation(), streamConfig.getTTL(), System.currentTimeMillis());
verifyGeneration(streamConfig, i);
streamAdmin.truncate(streamId);
}
int generation = StreamUtils.getGeneration(streamConfig);
Assert.assertEquals(5, generation);
janitor.clean(streamConfig.getLocation(), streamConfig.getTTL(), System.currentTimeMillis());
// Verify the stream directory should only contains the generation directory
for (Location location : streamConfig.getLocation().list()) {
if (location.isDirectory()) {
Assert.assertEquals(generation, Integer.parseInt(location.getName()));
}
}
}
@Test
public void testCleanupTTL() throws Exception {
// Create a stream with 5 seconds TTL, partition duration of 2 seconds
String streamName = "testCleanupTTL";
Id.Stream streamId = Id.Stream.from(Id.Namespace.DEFAULT, streamName);
StreamAdmin streamAdmin = getStreamAdmin();
StreamFileJanitor janitor = new StreamFileJanitor(getCConfiguration(), getStreamAdmin(),
getNamespacedLocationFactory(), getNamespaceStore());
Properties properties = new Properties();
properties.setProperty(Constants.Stream.PARTITION_DURATION, "2000");
properties.setProperty(Constants.Stream.TTL, "5000");
streamAdmin.create(streamId, properties);
// Truncate to increment generation to 1. This make verification condition easier (won't affect correctness).
streamAdmin.truncate(streamId);
StreamConfig config = streamAdmin.getConfig(streamId);
// Write data with different timestamps that spans across 5 partitions
FileWriter<StreamEvent> writer = createWriter(streamId);
for (int i = 0; i < 10; i++) {
writer.append(StreamFileTestUtils.createEvent(i * 1000, "Testing " + i));
}
writer.close();
// Should see 5 partitions
Location generationLocation = StreamUtils.createGenerationLocation(config.getLocation(), 1);
Assert.assertEquals(5, generationLocation.list().size());
// Perform clean with current time = 10000 (10 seconds since epoch).
// Since TTL = 5 seconds, 2 partitions will be remove (Ends at 2000 and ends at 4000).
janitor.clean(config.getLocation(), config.getTTL(), 10000);
Assert.assertEquals(3, generationLocation.list().size());
// Cleanup again with current time = 16000, all partitions should be deleted.
janitor.clean(config.getLocation(), config.getTTL(), 16000);
Assert.assertTrue(generationLocation.list().isEmpty());
}
@Test
public void testCleanupDeletedStream() throws Exception {
Id.Stream streamId = Id.Stream.from(Id.Namespace.DEFAULT, "cleanupDelete");
StreamAdmin streamAdmin = getStreamAdmin();
StreamFileJanitor janitor = new StreamFileJanitor(getCConfiguration(), streamAdmin, getNamespacedLocationFactory(),
getNamespaceStore());
streamAdmin.create(streamId);
// Write some data
try (FileWriter<StreamEvent> writer = createWriter(streamId)) {
for (int i = 0; i < 10; i++) {
writer.append(StreamFileTestUtils.createEvent(i * 1000, "Testing " + i));
}
}
// Delete the stream
streamAdmin.drop(streamId);
// Run janitor. Should be running fine without exception.
janitor.cleanAll();
}
private void verifyGeneration(StreamConfig config, int generation) throws IOException {
Location generationLocation = StreamUtils.createGenerationLocation(config.getLocation(), generation);
Assert.assertTrue(generationLocation.isDirectory());
// There should be a partition directory inside
for (Location location : generationLocation.list()) {
if (location.isDirectory() && location.getName().indexOf('.') > 0) {
return;
}
}
throw new IOException("Not a valid generation directory");
}
}