/*
* 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 gobblin.util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Queue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.SequenceFile;
import org.apache.hadoop.io.Text;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import com.google.common.base.Optional;
import com.google.common.collect.Queues;
import com.google.common.io.Closer;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import gobblin.configuration.WorkUnitState;
import gobblin.source.extractor.Watermark;
import gobblin.source.extractor.WatermarkSerializerHelper;
import gobblin.source.workunit.WorkUnit;
import gobblin.util.io.SeekableFSInputStream;
/**
* Unit tests for {@link ParallelRunner}.
*
* @author Yinan Li
*/
@Test(groups = { "gobblin.util" })
public class ParallelRunnerTest {
private FileSystem fs;
private Path outputPath;
@BeforeClass
public void setUp() throws IOException {
this.fs = FileSystem.getLocal(new Configuration());
this.outputPath = new Path(ParallelRunnerTest.class.getSimpleName());
}
@Test
public void testSerializeToFile() throws IOException {
try (ParallelRunner parallelRunner = new ParallelRunner(2, this.fs)) {
WorkUnit workUnit1 = WorkUnit.createEmpty();
workUnit1.setProp("foo", "bar");
workUnit1.setProp("a", 10);
parallelRunner.serializeToFile(workUnit1, new Path(this.outputPath, "wu1"));
WorkUnit workUnit2 = WorkUnit.createEmpty();
workUnit2.setProp("foo", "baz");
workUnit2.setProp("b", 20);
parallelRunner.serializeToFile(workUnit2, new Path(this.outputPath, "wu2"));
}
}
@Test(dependsOnMethods = "testSerializeToFile")
public void testDeserializeFromFile() throws IOException {
WorkUnit workUnit1 = WorkUnit.createEmpty();
WorkUnit workUnit2 = WorkUnit.createEmpty();
try (ParallelRunner parallelRunner = new ParallelRunner(2, this.fs)) {
parallelRunner.deserializeFromFile(workUnit1, new Path(this.outputPath, "wu1"));
parallelRunner.deserializeFromFile(workUnit2, new Path(this.outputPath, "wu2"));
}
Assert.assertEquals(workUnit1.getPropertyNames().size(), 2);
Assert.assertEquals(workUnit1.getProp("foo"), "bar");
Assert.assertEquals(workUnit1.getPropAsInt("a"), 10);
Assert.assertEquals(workUnit2.getPropertyNames().size(), 2);
Assert.assertEquals(workUnit2.getProp("foo"), "baz");
Assert.assertEquals(workUnit2.getPropAsInt("b"), 20);
}
@Test
@SuppressWarnings("deprecation")
public void testSerializeToSequenceFile() throws IOException {
Closer closer = Closer.create();
Configuration conf = new Configuration();
WritableShimSerialization.addToHadoopConfiguration(conf);
try {
SequenceFile.Writer writer1 = closer.register(SequenceFile.createWriter(this.fs, conf,
new Path(this.outputPath, "seq1"), Text.class, WorkUnitState.class));
Text key = new Text();
WorkUnitState workUnitState = new WorkUnitState();
TestWatermark watermark = new TestWatermark();
watermark.setLongWatermark(10L);
workUnitState.setActualHighWatermark(watermark);
writer1.append(key, workUnitState);
SequenceFile.Writer writer2 = closer.register(SequenceFile.createWriter(this.fs, conf,
new Path(this.outputPath, "seq2"), Text.class, WorkUnitState.class));
watermark.setLongWatermark(100L);
workUnitState.setActualHighWatermark(watermark);
writer2.append(key, workUnitState);
} catch (Throwable t) {
throw closer.rethrow(t);
} finally {
closer.close();
}
}
@Test(dependsOnMethods = "testSerializeToSequenceFile")
public void testDeserializeFromSequenceFile() throws IOException {
Queue<WorkUnitState> workUnitStates = Queues.newConcurrentLinkedQueue();
Path seqPath1 = new Path(this.outputPath, "seq1");
Path seqPath2 = new Path(this.outputPath, "seq2");
try (ParallelRunner parallelRunner = new ParallelRunner(2, this.fs)) {
parallelRunner.deserializeFromSequenceFile(Text.class, WorkUnitState.class, seqPath1, workUnitStates, true);
parallelRunner.deserializeFromSequenceFile(Text.class, WorkUnitState.class, seqPath2, workUnitStates, true);
}
Assert.assertFalse(this.fs.exists(seqPath1));
Assert.assertFalse(this.fs.exists(seqPath2));
Assert.assertEquals(workUnitStates.size(), 2);
for (WorkUnitState workUnitState : workUnitStates) {
TestWatermark watermark = new Gson().fromJson(workUnitState.getActualHighWatermark(), TestWatermark.class);
Assert.assertTrue(watermark.getLongWatermark() == 10L || watermark.getLongWatermark() == 100L);
}
}
@Test
public void testMovePath() throws IOException, URISyntaxException {
String expected = "test";
ByteArrayOutputStream actual = new ByteArrayOutputStream();
Path src = new Path("/src/file.txt");
Path dst = new Path("/dst/file.txt");
FileSystem fs1 = Mockito.mock(FileSystem.class);
Mockito.when(fs1.exists(src)).thenReturn(true);
Mockito.when(fs1.isFile(src)).thenReturn(true);
Mockito.when(fs1.getUri()).thenReturn(new URI("fs1:////"));
Mockito.when(fs1.getFileStatus(src)).thenReturn(new FileStatus(1, false, 1, 1, 1, src));
Mockito.when(fs1.open(src))
.thenReturn(new FSDataInputStream(new SeekableFSInputStream(new ByteArrayInputStream(expected.getBytes()))));
Mockito.when(fs1.delete(src, true)).thenReturn(true);
FileSystem fs2 = Mockito.mock(FileSystem.class);
Mockito.when(fs2.exists(dst)).thenReturn(false);
Mockito.when(fs2.getUri()).thenReturn(new URI("fs2:////"));
Mockito.when(fs2.getConf()).thenReturn(new Configuration());
Mockito.when(fs2.create(dst, false)).thenReturn(new FSDataOutputStream(actual, null));
try (ParallelRunner parallelRunner = new ParallelRunner(1, fs1)) {
parallelRunner.movePath(src, fs2, dst, Optional.<String>absent());
}
Assert.assertEquals(actual.toString(), expected);
}
@Test(groups = { "ignore" })
public void testWaitsForFuturesWhenClosing() throws IOException, InterruptedException {
final AtomicBoolean flag = new AtomicBoolean();
final CountDownLatch latch1 = new CountDownLatch(1);
final CountDownLatch latch2 = new CountDownLatch(1);
Path src1 = new Path("/src/file1.txt");
Path src2 = new Path("/src/file2.txt");
FileSystem fs = Mockito.mock(FileSystem.class);
Mockito.when(fs.exists(src1)).thenReturn(true);
Mockito.when(fs.delete(src1, true)).thenAnswer(new Answer<Boolean>() {
@Override
public Boolean answer(InvocationOnMock invocation) throws Throwable {
latch1.countDown();
return false;
}
});
Mockito.when(fs.exists(src2)).thenReturn(true);
Mockito.when(fs.delete(src2, true)).thenAnswer(new Answer<Boolean>() {
@Override
public Boolean answer(InvocationOnMock invocation) throws Throwable {
latch1.await();
long end = System.currentTimeMillis() + 70000;
while (System.currentTimeMillis() < end) {
try {
Thread.sleep(Math.max(1, end - System.currentTimeMillis()));
} catch (Exception ignored) {
}
}
flag.set(true);
latch2.countDown();
return true;
}
});
boolean caughtException = false;
ParallelRunner parallelRunner = new ParallelRunner(2, fs);
try {
parallelRunner.deletePath(src1, true);
parallelRunner.deletePath(src2, true);
System.out.println(System.currentTimeMillis() + ": START - ParallelRunner.close()");
parallelRunner.close();
} catch (IOException e) {
caughtException = true;
}
Assert.assertTrue(caughtException);
System.out.println(System.currentTimeMillis() + ": END - ParallelRunner.close()");
System.out.println(System.currentTimeMillis() + ": Waiting for unkillable task to finish...");
latch2.await();
System.out.println(System.currentTimeMillis() + ": Unkillable task completed.");
Assert.assertTrue(flag.get());
}
@AfterClass
public void tearDown() throws IOException {
if (this.fs != null && this.outputPath != null) {
this.fs.delete(this.outputPath, true);
}
}
public static class TestWatermark implements Watermark {
private long watermark = -1;
@Override
public JsonElement toJson() {
return WatermarkSerializerHelper.convertWatermarkToJson(this);
}
@Override
public short calculatePercentCompletion(Watermark lowWatermark, Watermark highWatermark) {
return 0;
}
public void setLongWatermark(long watermark) {
this.watermark = watermark;
}
public long getLongWatermark() {
return this.watermark;
}
}
}