/**
* Licensed to Cloudera, Inc. under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Cloudera, Inc. 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 com.cloudera.flume.agent.durability;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.junit.Before;
import org.junit.Test;
import com.cloudera.flume.agent.FlumeNode;
import com.cloudera.flume.conf.Context;
import com.cloudera.flume.conf.FlumeBuilder;
import com.cloudera.flume.conf.FlumeSpecException;
import com.cloudera.flume.conf.ReportTestingContext;
import com.cloudera.flume.conf.SinkFactory.SinkDecoBuilder;
import com.cloudera.flume.core.Event;
import com.cloudera.flume.core.EventImpl;
import com.cloudera.flume.core.EventSink;
import com.cloudera.flume.core.EventSinkDecorator;
import com.cloudera.flume.core.EventSource;
import com.cloudera.flume.core.EventUtil;
import com.cloudera.flume.handlers.debug.MemorySinkSource;
import com.cloudera.flume.handlers.debug.NullSink;
import com.cloudera.flume.handlers.endtoend.AckListener;
import com.cloudera.flume.handlers.rolling.ProcessTagger;
import com.cloudera.flume.handlers.rolling.SizeTrigger;
import com.cloudera.flume.handlers.rolling.TimeTrigger;
import com.cloudera.flume.reporter.ReportManager;
import com.cloudera.flume.reporter.aggregator.CounterSink;
import com.cloudera.util.BenchmarkHarness;
import com.cloudera.util.FileUtil;
/**
* This tests the naive file WAL decorator.
*/
public class TestNaiveFileWALDeco {
static final Logger LOG = Logger.getLogger(NaiveFileWALManager.class
.getName());
@Before
public void setUp() {
LOG.setLevel(Level.DEBUG);
}
/**
* This test shows that we properly recover and eventually delete walfiles
* from previous failed sessions.
*/
@Test
public void testRecoveredIsDeleted() throws IOException, FlumeSpecException,
InterruptedException {
BenchmarkHarness.setupLocalWriteDir();
File tmp = BenchmarkHarness.tmpdir;
// file with ack begin, data, and end messages
File acked = new File("src/data/acked.00000000.20100204-015814430-0800.seq");
// Assumes the NaiveFileWALManager!
File writing = new File(new File(tmp, BenchmarkHarness.node
.getPhysicalNodeName()), "writing");
writing.mkdirs();
// Must rename file because that name is in the meta data of the event
// inside the file!
// TODO (jon) make this restriction less strict in the future.
FileUtil.dumbfilecopy(acked, new File(writing,
"writeahead.00000000.20100204-015814F430-0800.seq"));
// EventSource src = FlumeBuilder.buildSource("");
EventSink snk = FlumeBuilder.buildSink(new ReportTestingContext(),
"{ ackedWriteAhead => { ackChecker => counter(\"count\") } }");
EventSource src = MemorySinkSource.cannedData("foo foo foo ", 5);
snk.open();
src.open();
EventUtil.dumpAll(src, snk);
src.close();
snk.close(); // this should block until recovery complete.
// agent checks for ack registrations.
BenchmarkHarness.node.getAckChecker().checkAcks();
CounterSink cnt = (CounterSink) ReportManager.get().getReportable("count");
// 1032 in file + 5 from silly driver
assertEquals(1037, cnt.getCount());
// check to make sure wal file is gone
assertFalse(new File(new File(tmp, "import"), acked.getName()).exists());
assertFalse(new File(new File(tmp, "writing"), acked.getName()).exists());
assertFalse(new File(new File(tmp, "logged"), acked.getName()).exists());
assertFalse(new File(new File(tmp, "sending"), acked.getName()).exists());
assertFalse(new File(new File(tmp, "sent"), acked.getName()).exists());
assertFalse(new File(new File(tmp, "error"), acked.getName()).exists());
assertFalse(new File(new File(tmp, "done"), acked.getName()).exists());
BenchmarkHarness.cleanupLocalWriteDir();
}
/**
* This specifically illustrates the case where an acktag doesn't match the
* name of the wal file. I believe that we should just make the walfile match
* its acktag an invariant.
*/
@Test
public void testMismatchedWalFilenameAckTag() throws IOException,
FlumeSpecException, InterruptedException {
// FlumeConfiguration.get().setLong(FlumeConfiguration.AGENT_LOG_MAX_AGE,
// 50);
BenchmarkHarness.setupLocalWriteDir();
File tmp = BenchmarkHarness.tmpdir;
// file with ack begin, data, and end messages
File acked = new File("src/data/acked.00000000.20100204-015814430-0800.seq");
// Assumes the NaiveFileWALManager!
File writing = new File(new File(tmp, BenchmarkHarness.node
.getPhysicalNodeName()), "writing");
writing.mkdirs();
// /////////////////////
// This illustrates the problems from the previous test.
FileUtil.dumbfilecopy(acked, new File(writing, acked.getName()));
// /////////////////////
EventSink snk = FlumeBuilder.buildSink(new ReportTestingContext(),
"{ ackedWriteAhead => { ackChecker => counter(\"count\") } }");
EventSource src = MemorySinkSource.cannedData("foo foo foo ", 5);
snk.open();
src.open();
EventUtil.dumpAll(src, snk);
src.close();
snk.close(); // this should block until recovery complete.
// agent checks for ack registrations.
BenchmarkHarness.node.getAckChecker().checkAcks();
CounterSink cnt = (CounterSink) ReportManager.get().getReportable("count");
// 1032 in file + 5 from silly driverx
assertEquals(1037, cnt.getCount());
// check to make sure wal file is gone
assertFalse(new File(new File(tmp, "import"), acked.getName()).exists());
assertFalse(new File(new File(tmp, "writing"), acked.getName()).exists());
assertFalse(new File(new File(tmp, "logged"), acked.getName()).exists());
assertFalse(new File(new File(tmp, "sending"), acked.getName()).exists());
assertFalse(new File(new File(tmp, "error"), acked.getName()).exists());
assertFalse(new File(new File(tmp, "done"), acked.getName()).exists());
// TODO (jon) is this the right behavior? I think assuming no name changes
// locally is reasonable for now.
assertTrue(new File(new File(new File(tmp, BenchmarkHarness.node
.getPhysicalNodeName()), "sent"), acked.getName()).exists());
BenchmarkHarness.cleanupLocalWriteDir();
}
/**
* This "recovers" a truncated wal file as best as it can and moves it to the
* err directory. Generally truncated files this would only happen on events
* such as kill -9, power out, or out of disk space.
*
* @throws IOException
* @throws FlumeSpecException
*/
@Test
public void testRecoveredMovesToErr() throws IOException, FlumeSpecException {
BenchmarkHarness.setupLocalWriteDir();
File tmp = BenchmarkHarness.tmpdir;
// Assumes the NaiveFileWALManager!
// file with ack begin, data and then truncated
File truncated = new File(
"src/data/truncated.00000000.20100204-015814430-0800.seq");
File writing = new File(new File(tmp, BenchmarkHarness.node
.getPhysicalNodeName()), "writing");
writing.mkdirs();
FileUtil.dumbfilecopy(truncated, new File(writing, truncated.getName()));
EventSink snk = FlumeBuilder.buildSink(new ReportTestingContext(),
"{ ackedWriteAhead => { ackChecker => counter(\"count\") } }");
EventSource src = MemorySinkSource.cannedData("foo foo foo ", 5);
snk.open();
src.open();
EventUtil.dumpAll(src, snk);
src.close();
snk.close(); // this should block until recovery complete.
CounterSink cnt = (CounterSink) ReportManager.get().getReportable("count");
// 461 in file before truncated + 5 from silly driver
assertEquals(466, cnt.getCount());
// need to trigger ack checks..
// BenchmarkHarness.mock.ackman.;
// check to make sure wal file is gone
File nodedir = new File(tmp, BenchmarkHarness.node.getPhysicalNodeName());
assertFalse(new File(new File(nodedir, "import"), truncated.getName())
.exists());
assertFalse(new File(new File(nodedir, "writing"), truncated.getName())
.exists());
assertFalse(new File(new File(nodedir, "logged"), truncated.getName())
.exists());
assertFalse(new File(new File(nodedir, "sending"), truncated.getName())
.exists());
assertFalse(new File(new File(nodedir, "sent"), truncated.getName())
.exists());
assertTrue(new File(new File(nodedir, "error"), truncated.getName())
.exists());
assertFalse(new File(new File(nodedir, "done"), truncated.getName())
.exists());
BenchmarkHarness.cleanupLocalWriteDir();
}
/**
* An old implementation could deadlock if append was called before open. This
* test tests this issue to an extent - we don't test concurrent append-> open
* as it requires injecting code into the implementation of append.
*/
@Test
public void testAppendBeforeOpen() throws InterruptedException {
final NaiveFileWALDeco<EventSink> d = new NaiveFileWALDeco<EventSink>(
new Context(), new NullSink(),
new NaiveFileWALManager(new File("/tmp")), new SizeTrigger(0, null),
new AckListener.Empty(), 1000000);
final CountDownLatch cdl1 = new CountDownLatch(1);
new Thread() {
public void run() {
try {
d.append(new EventImpl("hello".getBytes()));
d.open();
cdl1.countDown();
} catch (IOException e) {
LOG.error("Saw IOException in testAppendBeforeOpen", e);
} catch (IllegalStateException e) {
// Expected illegal state exception due to not being open
cdl1.countDown();
}
}
}.start();
assertTrue("Latch not fired", cdl1.await(5000, TimeUnit.MILLISECONDS));
}
/**
* Test the case where data with out ack tags is being sent to the wal deco.
*/
@Test
public void testBadRegistererAppend() throws InterruptedException {
final NaiveFileWALDeco<EventSink> d = new NaiveFileWALDeco<EventSink>(
new Context(), new NullSink(),
new NaiveFileWALManager(new File("/tmp")), new SizeTrigger(0, null),
new AckListener.Empty(), 1000000);
final CountDownLatch cdl1 = new CountDownLatch(1);
new Thread() {
public void run() {
try {
d.append(new EventImpl("hello".getBytes()));
d.open();
cdl1.countDown();
} catch (IOException e) {
LOG.error("Saw IOException in testAppendBeforeOpen", e);
} catch (IllegalStateException e) {
// Expected illegal state exception due to not being open
cdl1.countDown();
}
}
}.start();
assertTrue("Latch not fired", cdl1.await(5000, TimeUnit.MILLISECONDS));
}
@Test(expected = IOException.class)
public void testExceptionThreadHandoff() throws IOException {
try {
BenchmarkHarness.setupLocalWriteDir();
Event e = new EventImpl(new byte[0]);
EventSink snk = new EventSink.Base() {
@Override
public void append(Event e) throws IOException {
throw new IOException("mock ioe");
}
};
FlumeNode node = FlumeNode.getInstance();
EventSinkDecorator<EventSink> deco = new NaiveFileWALDeco<EventSink>(
new Context(), snk, node.getWalManager(), new TimeTrigger(
new ProcessTagger(), 1000), node.getAckChecker()
.getAgentAckQueuer(), 1000);
deco.open();
deco.append(e);
deco.close();
} catch (IOException e) {
throw e;
} finally {
BenchmarkHarness.cleanupLocalWriteDir();
}
}
@Test(expected = IllegalArgumentException.class)
public void testBadE2eBuilderArgs() {
SinkDecoBuilder b = NaiveFileWALDeco.builderEndToEndDir();
b.build(new Context(), "foo", "bar");
}
}