/**
* 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 org.apache.aurora.scheduler.log.mesos;
import java.lang.reflect.Constructor;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.TimeoutException;
import com.google.common.base.Function;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedSet;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.TypeLiteral;
import com.twitter.common.application.Lifecycle;
import com.twitter.common.base.Command;
import com.twitter.common.quantity.Amount;
import com.twitter.common.quantity.Time;
import com.twitter.common.testing.easymock.EasyMockTest;
import org.apache.aurora.scheduler.log.Log.Stream.StreamAccessException;
import org.apache.aurora.scheduler.log.mesos.LogInterface.ReaderInterface;
import org.apache.aurora.scheduler.log.mesos.LogInterface.WriterInterface;
import org.apache.mesos.Log;
import org.easymock.EasyMock;
import org.easymock.IExpectationSetters;
import org.junit.Before;
import org.junit.Test;
import static org.apache.aurora.scheduler.log.mesos.MesosLog.LogStream.LogPosition;
import static org.apache.mesos.Log.Position;
import static org.easymock.EasyMock.expect;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
public class MesosLogTest extends EasyMockTest {
private static final Amount<Long, Time> READ_TIMEOUT = Amount.of(5L, Time.SECONDS);
private static final Amount<Long, Time> WRITE_TIMEOUT = Amount.of(3L, Time.SECONDS);
private static final String DUMMY_CONTENT = "test data";
private Command shutdownHooks;
private LogInterface backingLog;
private ReaderInterface logReader;
private WriterInterface logWriter;
private org.apache.aurora.scheduler.log.Log.Stream logStream;
@Before
public void setUp() {
shutdownHooks = createMock(Command.class);
backingLog = createMock(LogInterface.class);
logReader = createMock(ReaderInterface.class);
logWriter = createMock(WriterInterface.class);
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bind(LogInterface.class).toInstance(backingLog);
bind(ReaderInterface.class).toInstance(logReader);
bind(new TypeLiteral<Amount<Long, Time>>() { }).annotatedWith(MesosLog.ReadTimeout.class)
.toInstance(READ_TIMEOUT);
bind(WriterInterface.class).toInstance(logWriter);
bind(new TypeLiteral<Amount<Long, Time>>() { }).annotatedWith(MesosLog.WriteTimeout.class)
.toInstance(WRITE_TIMEOUT);
bind(byte[].class).annotatedWith(MesosLog.NoopEntry.class)
.toInstance(DUMMY_CONTENT.getBytes(StandardCharsets.UTF_8));
bind(Lifecycle.class).toInstance(new Lifecycle(shutdownHooks, null));
}
});
MesosLog log = injector.getInstance(MesosLog.class);
logStream = log.open();
}
@Test
public void testLogStreamTimeout() throws Exception {
try {
testMutationFailure(new TimeoutException("Task timed out"));
fail();
} catch (StreamAccessException e) {
// Expected.
}
expectStreamUnusable();
}
@Test
public void testLogStreamWriteFailure() throws Exception {
try {
testMutationFailure(new Log.WriterFailedException("Failed to write to log"));
fail();
} catch (StreamAccessException e) {
// Expected.
}
expectStreamUnusable();
}
private void testMutationFailure(Exception e) throws Exception {
String data = "hello";
expectWrite(data).andThrow(e);
shutdownHooks.execute();
control.replay();
logStream.append(data.getBytes(StandardCharsets.UTF_8));
}
private void expectStreamUnusable() throws Exception {
try {
logStream.append("nothing".getBytes(StandardCharsets.UTF_8));
fail();
} catch (IllegalStateException e) {
// Expected.
}
}
private static Position makePosition(long value) throws Exception {
// The only way to create a Position instance is through a private constructor (MESOS-1519).
Constructor<Position> positionConstructor = Position.class.getDeclaredConstructor(long.class);
positionConstructor.setAccessible(true);
return positionConstructor.newInstance(value);
}
private static Log.Entry makeEntry(Position position, String data) throws Exception {
// The only way to create an Entry instance is through a private constructor (MESOS-1519).
Constructor<Log.Entry> entryConstructor =
Log.Entry.class.getDeclaredConstructor(Position.class, byte[].class);
entryConstructor.setAccessible(true);
return entryConstructor.newInstance(position, data.getBytes(StandardCharsets.UTF_8));
}
private IExpectationSetters<Position> expectWrite(String content) throws Exception {
return expect(
logWriter.append(EasyMock.aryEq(content.getBytes(StandardCharsets.UTF_8)),
// Cast is needed to prevent NullPointerException on unboxing.
EasyMock.eq((long) WRITE_TIMEOUT.getValue()),
EasyMock.eq(WRITE_TIMEOUT.getUnit().getTimeUnit())));
}
private Position expectWrite(String content, long resultingPosition) throws Exception {
Position position = makePosition(resultingPosition);
expectWrite(content).andReturn(position);
return position;
}
private void expectDiscoverEntryRange(Position beginning, Position end) {
expect(logReader.beginning()).andReturn(beginning);
expect(logReader.ending()).andReturn(end);
}
private void expectSetPosition(Position position) {
expect(backingLog.position(EasyMock.aryEq(position.identity()))).andReturn(position);
}
private IExpectationSetters<List<Log.Entry>> expectRead(Position position) throws Exception {
expectSetPosition(position);
return expect(logReader.read(
position,
position,
READ_TIMEOUT.getValue(),
READ_TIMEOUT.getUnit().getTimeUnit()));
}
private void expectRead(Position position, String dataReturned) throws Exception {
expectRead(position).andReturn(ImmutableList.of(makeEntry(position, dataReturned)));
}
private List<String> readAll() {
List<byte[]> entryBytes = FluentIterable.from(ImmutableList.copyOf(logStream.readAll()))
.transform(new Function<org.apache.aurora.scheduler.log.Log.Entry, byte[]>() {
@Override
public byte[] apply(org.apache.aurora.scheduler.log.Log.Entry entry) {
return entry.contents();
}
})
.toList();
return FluentIterable.from(entryBytes)
.transform(new Function<byte[], String>() {
@Override
public String apply(byte[] data) {
return new String(data, StandardCharsets.UTF_8);
}
})
.toList();
}
@Test
public void testLogRead() throws Exception {
Position beginning = makePosition(1);
Position middle = makePosition(2);
Position end = expectWrite(DUMMY_CONTENT, 3);
expectDiscoverEntryRange(beginning, end);
String beginningData = "beginningData";
String middleData = "middleData";
expectRead(beginning, beginningData);
expectRead(middle, middleData);
expectRead(end, DUMMY_CONTENT);
String newData = "newly appended data";
expectWrite(newData, 4);
control.replay();
assertEquals(ImmutableList.of(beginningData, middleData, DUMMY_CONTENT), readAll());
logStream.append(newData.getBytes());
}
@Test(expected = StreamAccessException.class)
public void testInitialAppendFails() throws Exception {
expectWrite(DUMMY_CONTENT).andThrow(new Log.WriterFailedException("injected"));
shutdownHooks.execute();
control.replay();
readAll();
}
@Test(expected = StreamAccessException.class)
public void testReadTimeout() throws Exception {
Position beginning = makePosition(1);
Position end = expectWrite(DUMMY_CONTENT, 3);
expectDiscoverEntryRange(beginning, end);
expectRead(beginning).andThrow(new TimeoutException("injected"));
control.replay();
readAll();
}
@Test(expected = StreamAccessException.class)
public void testLogError() throws Exception {
Position beginning = makePosition(1);
Position end = expectWrite(DUMMY_CONTENT, 3);
expectDiscoverEntryRange(beginning, end);
expectRead(beginning).andThrow(new Log.OperationFailedException("injected"));
control.replay();
readAll();
}
@Test
public void testTruncate() throws Exception {
Position truncate = makePosition(5);
expect(logWriter.truncate(
truncate,
WRITE_TIMEOUT.getValue(),
WRITE_TIMEOUT.getUnit().getTimeUnit()))
.andReturn(truncate);
control.replay();
logStream.truncateBefore(new LogPosition(truncate));
}
@Test(expected = NoSuchElementException.class)
public void testIteratorUsage() throws Exception {
Position beginning = makePosition(1);
Position middle = makePosition(2);
Position end = expectWrite(DUMMY_CONTENT, 3);
expectDiscoverEntryRange(beginning, end);
// SKipped entries.
expectRead(beginning).andReturn(ImmutableList.<Log.Entry>of());
expectRead(middle).andReturn(ImmutableList.<Log.Entry>of());
expectRead(end).andReturn(ImmutableList.<Log.Entry>of());
control.replay();
// So close! The implementation requires that hasNext() is called first.
logStream.readAll().next();
}
@Test
public void testSortOrder() throws Exception {
control.replay();
LogPosition a = new LogPosition(makePosition(5));
LogPosition b = new LogPosition(makePosition(10));
LogPosition c = new LogPosition(makePosition(3));
assertEquals(
ImmutableList.of(c, a, b),
ImmutableList.copyOf(ImmutableSortedSet.of(a, b, c))
);
}
}