Introduce FileRotator.
Utility that rotates files over time, similar to logrotate. There is a single "active" file, which is periodically rotated into historical files, and eventually deleted entirely. Files are stored under a specific directory with a well-known prefix. Bug: 5386531 Change-Id: I29f821a881247e50ce0f6f73b20bbd020db39e43
This commit is contained in:
330
core/java/com/android/internal/util/FileRotator.java
Normal file
330
core/java/com/android/internal/util/FileRotator.java
Normal file
@ -0,0 +1,330 @@
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* 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 com.android.internal.util;
|
||||
|
||||
import android.os.FileUtils;
|
||||
|
||||
import com.android.internal.util.FileRotator.Reader;
|
||||
import com.android.internal.util.FileRotator.Writer;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import libcore.io.IoUtils;
|
||||
|
||||
/**
|
||||
* Utility that rotates files over time, similar to {@code logrotate}. There is
|
||||
* a single "active" file, which is periodically rotated into historical files,
|
||||
* and eventually deleted entirely. Files are stored under a specific directory
|
||||
* with a well-known prefix.
|
||||
* <p>
|
||||
* Instead of manipulating files directly, users implement interfaces that
|
||||
* perform operations on {@link InputStream} and {@link OutputStream}. This
|
||||
* enables atomic rewriting of file contents in
|
||||
* {@link #combineActive(Reader, Writer, long)}.
|
||||
* <p>
|
||||
* Users must periodically call {@link #maybeRotate(long)} to perform actual
|
||||
* rotation. Not inherently thread safe.
|
||||
*/
|
||||
public class FileRotator {
|
||||
private final File mBasePath;
|
||||
private final String mPrefix;
|
||||
private final long mRotateAgeMillis;
|
||||
private final long mDeleteAgeMillis;
|
||||
|
||||
private static final String SUFFIX_BACKUP = ".backup";
|
||||
private static final String SUFFIX_NO_BACKUP = ".no_backup";
|
||||
|
||||
// TODO: provide method to append to active file
|
||||
|
||||
/**
|
||||
* External class that reads data from a given {@link InputStream}. May be
|
||||
* called multiple times when reading rotated data.
|
||||
*/
|
||||
public interface Reader {
|
||||
public void read(InputStream in) throws IOException;
|
||||
}
|
||||
|
||||
/**
|
||||
* External class that writes data to a given {@link OutputStream}.
|
||||
*/
|
||||
public interface Writer {
|
||||
public void write(OutputStream out) throws IOException;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a file rotator.
|
||||
*
|
||||
* @param basePath Directory under which all files will be placed.
|
||||
* @param prefix Filename prefix used to identify this rotator.
|
||||
* @param rotateAgeMillis Age in milliseconds beyond which an active file
|
||||
* may be rotated into a historical file.
|
||||
* @param deleteAgeMillis Age in milliseconds beyond which a rotated file
|
||||
* may be deleted.
|
||||
*/
|
||||
public FileRotator(File basePath, String prefix, long rotateAgeMillis, long deleteAgeMillis) {
|
||||
mBasePath = Preconditions.checkNotNull(basePath);
|
||||
mPrefix = Preconditions.checkNotNull(prefix);
|
||||
mRotateAgeMillis = rotateAgeMillis;
|
||||
mDeleteAgeMillis = deleteAgeMillis;
|
||||
|
||||
// ensure that base path exists
|
||||
mBasePath.mkdirs();
|
||||
|
||||
// recover any backup files
|
||||
for (String name : mBasePath.list()) {
|
||||
if (!name.startsWith(mPrefix)) continue;
|
||||
|
||||
if (name.endsWith(SUFFIX_BACKUP)) {
|
||||
final File backupFile = new File(mBasePath, name);
|
||||
final File file = new File(
|
||||
mBasePath, name.substring(0, name.length() - SUFFIX_BACKUP.length()));
|
||||
|
||||
// write failed with backup; recover last file
|
||||
backupFile.renameTo(file);
|
||||
|
||||
} else if (name.endsWith(SUFFIX_NO_BACKUP)) {
|
||||
final File noBackupFile = new File(mBasePath, name);
|
||||
final File file = new File(
|
||||
mBasePath, name.substring(0, name.length() - SUFFIX_NO_BACKUP.length()));
|
||||
|
||||
// write failed without backup; delete both
|
||||
noBackupFile.delete();
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically combine data with existing data in currently active file.
|
||||
* Maintains a backup during write, which is restored if the write fails.
|
||||
*/
|
||||
public void combineActive(Reader reader, Writer writer, long currentTimeMillis)
|
||||
throws IOException {
|
||||
final String activeName = getActiveName(currentTimeMillis);
|
||||
|
||||
final File file = new File(mBasePath, activeName);
|
||||
final File backupFile;
|
||||
|
||||
if (file.exists()) {
|
||||
// read existing data
|
||||
readFile(file, reader);
|
||||
|
||||
// backup existing data during write
|
||||
backupFile = new File(mBasePath, activeName + SUFFIX_BACKUP);
|
||||
file.renameTo(backupFile);
|
||||
|
||||
try {
|
||||
writeFile(file, writer);
|
||||
|
||||
// write success, delete backup
|
||||
backupFile.delete();
|
||||
} catch (IOException e) {
|
||||
// write failed, delete file and restore backup
|
||||
file.delete();
|
||||
backupFile.renameTo(file);
|
||||
throw e;
|
||||
}
|
||||
|
||||
} else {
|
||||
// create empty backup during write
|
||||
backupFile = new File(mBasePath, activeName + SUFFIX_NO_BACKUP);
|
||||
backupFile.createNewFile();
|
||||
|
||||
try {
|
||||
writeFile(file, writer);
|
||||
|
||||
// write success, delete empty backup
|
||||
backupFile.delete();
|
||||
} catch (IOException e) {
|
||||
// write failed, delete file and empty backup
|
||||
file.delete();
|
||||
backupFile.delete();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read any rotated data that overlap the requested time range.
|
||||
*/
|
||||
public void readMatching(Reader reader, long matchStartMillis, long matchEndMillis)
|
||||
throws IOException {
|
||||
final FileInfo info = new FileInfo(mPrefix);
|
||||
for (String name : mBasePath.list()) {
|
||||
if (!info.parse(name)) continue;
|
||||
|
||||
// read file when it overlaps
|
||||
if (info.startMillis <= matchEndMillis && matchStartMillis <= info.endMillis) {
|
||||
final File file = new File(mBasePath, name);
|
||||
readFile(file, reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the currently active file, which may not exist yet.
|
||||
*/
|
||||
private String getActiveName(long currentTimeMillis) {
|
||||
String oldestActiveName = null;
|
||||
long oldestActiveStart = Long.MAX_VALUE;
|
||||
|
||||
final FileInfo info = new FileInfo(mPrefix);
|
||||
for (String name : mBasePath.list()) {
|
||||
if (!info.parse(name)) continue;
|
||||
|
||||
// pick the oldest active file which covers current time
|
||||
if (info.isActive() && info.startMillis < currentTimeMillis
|
||||
&& info.startMillis < oldestActiveStart) {
|
||||
oldestActiveName = name;
|
||||
oldestActiveStart = info.startMillis;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldestActiveName != null) {
|
||||
return oldestActiveName;
|
||||
} else {
|
||||
// no active file found above; create one starting now
|
||||
info.startMillis = currentTimeMillis;
|
||||
info.endMillis = Long.MAX_VALUE;
|
||||
return info.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Examine all files managed by this rotator, renaming or deleting if their
|
||||
* age matches the configured thresholds.
|
||||
*/
|
||||
public void maybeRotate(long currentTimeMillis) {
|
||||
final long rotateBefore = currentTimeMillis - mRotateAgeMillis;
|
||||
final long deleteBefore = currentTimeMillis - mDeleteAgeMillis;
|
||||
|
||||
final FileInfo info = new FileInfo(mPrefix);
|
||||
for (String name : mBasePath.list()) {
|
||||
if (!info.parse(name)) continue;
|
||||
|
||||
if (info.isActive()) {
|
||||
// found active file; rotate if old enough
|
||||
if (info.startMillis < rotateBefore) {
|
||||
info.endMillis = currentTimeMillis;
|
||||
|
||||
final File file = new File(mBasePath, name);
|
||||
final File destFile = new File(mBasePath, info.build());
|
||||
file.renameTo(destFile);
|
||||
}
|
||||
} else if (info.endMillis < deleteBefore) {
|
||||
// found rotated file; delete if old enough
|
||||
final File file = new File(mBasePath, name);
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void readFile(File file, Reader reader) throws IOException {
|
||||
final FileInputStream fis = new FileInputStream(file);
|
||||
final BufferedInputStream bis = new BufferedInputStream(fis);
|
||||
try {
|
||||
reader.read(bis);
|
||||
} finally {
|
||||
IoUtils.closeQuietly(bis);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeFile(File file, Writer writer) throws IOException {
|
||||
final FileOutputStream fos = new FileOutputStream(file);
|
||||
final BufferedOutputStream bos = new BufferedOutputStream(fos);
|
||||
try {
|
||||
writer.write(bos);
|
||||
bos.flush();
|
||||
} finally {
|
||||
FileUtils.sync(fos);
|
||||
IoUtils.closeQuietly(bos);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Details for a rotated file, either parsed from an existing filename, or
|
||||
* ready to be built into a new filename.
|
||||
*/
|
||||
private static class FileInfo {
|
||||
public final String prefix;
|
||||
|
||||
public long startMillis;
|
||||
public long endMillis;
|
||||
|
||||
public FileInfo(String prefix) {
|
||||
this.prefix = Preconditions.checkNotNull(prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt parsing the given filename.
|
||||
*
|
||||
* @return Whether parsing was successful.
|
||||
*/
|
||||
public boolean parse(String name) {
|
||||
startMillis = endMillis = -1;
|
||||
|
||||
final int dotIndex = name.lastIndexOf('.');
|
||||
final int dashIndex = name.lastIndexOf('-');
|
||||
|
||||
// skip when missing time section
|
||||
if (dotIndex == -1 || dashIndex == -1) return false;
|
||||
|
||||
// skip when prefix doesn't match
|
||||
if (!prefix.equals(name.substring(0, dotIndex))) return false;
|
||||
|
||||
try {
|
||||
startMillis = Long.parseLong(name.substring(dotIndex + 1, dashIndex));
|
||||
|
||||
if (name.length() - dashIndex == 1) {
|
||||
endMillis = Long.MAX_VALUE;
|
||||
} else {
|
||||
endMillis = Long.parseLong(name.substring(dashIndex + 1));
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build current state into filename.
|
||||
*/
|
||||
public String build() {
|
||||
final StringBuilder name = new StringBuilder();
|
||||
name.append(prefix).append('.').append(startMillis).append('-');
|
||||
if (endMillis != Long.MAX_VALUE) {
|
||||
name.append(endMillis);
|
||||
}
|
||||
return name.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if current file is active (no end timestamp).
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return endMillis == Long.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,428 @@
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* 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 com.android.internal.util;
|
||||
|
||||
import static android.text.format.DateUtils.DAY_IN_MILLIS;
|
||||
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
|
||||
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
|
||||
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
|
||||
import static android.text.format.DateUtils.WEEK_IN_MILLIS;
|
||||
import static android.text.format.DateUtils.YEAR_IN_MILLIS;
|
||||
|
||||
import android.test.AndroidTestCase;
|
||||
import android.test.suitebuilder.annotation.Suppress;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.util.FileRotator.Reader;
|
||||
import com.android.internal.util.FileRotator.Writer;
|
||||
import com.google.android.collect.Lists;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.ProtocolException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
|
||||
import libcore.io.IoUtils;
|
||||
|
||||
/**
|
||||
* Tests for {@link FileRotator}.
|
||||
*/
|
||||
public class FileRotatorTest extends AndroidTestCase {
|
||||
private static final String TAG = "FileRotatorTest";
|
||||
|
||||
private File mBasePath;
|
||||
|
||||
private static final String PREFIX = "rotator";
|
||||
private static final String ANOTHER_PREFIX = "another_rotator";
|
||||
|
||||
private static final long TEST_TIME = 1300000000000L;
|
||||
|
||||
// TODO: test throwing rolls back correctly
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
mBasePath = getContext().getFilesDir();
|
||||
IoUtils.deleteContents(mBasePath);
|
||||
}
|
||||
|
||||
public void testEmpty() throws Exception {
|
||||
final FileRotator rotate1 = new FileRotator(
|
||||
mBasePath, PREFIX, DAY_IN_MILLIS, WEEK_IN_MILLIS);
|
||||
final FileRotator rotate2 = new FileRotator(
|
||||
mBasePath, ANOTHER_PREFIX, DAY_IN_MILLIS, WEEK_IN_MILLIS);
|
||||
|
||||
final RecordingReader reader = new RecordingReader();
|
||||
long currentTime = TEST_TIME;
|
||||
|
||||
// write single new value
|
||||
rotate1.combineActive(reader, writer("foo"), currentTime);
|
||||
reader.assertRead();
|
||||
|
||||
// assert that one rotator doesn't leak into another
|
||||
assertReadAll(rotate1, "foo");
|
||||
assertReadAll(rotate2);
|
||||
}
|
||||
|
||||
public void testCombine() throws Exception {
|
||||
final FileRotator rotate = new FileRotator(
|
||||
mBasePath, PREFIX, DAY_IN_MILLIS, WEEK_IN_MILLIS);
|
||||
|
||||
final RecordingReader reader = new RecordingReader();
|
||||
long currentTime = TEST_TIME;
|
||||
|
||||
// first combine should have empty read, but still write data.
|
||||
rotate.combineActive(reader, writer("foo"), currentTime);
|
||||
reader.assertRead();
|
||||
assertReadAll(rotate, "foo");
|
||||
|
||||
// second combine should replace contents; should read existing data,
|
||||
// and write final data to disk.
|
||||
currentTime += SECOND_IN_MILLIS;
|
||||
reader.reset();
|
||||
rotate.combineActive(reader, writer("bar"), currentTime);
|
||||
reader.assertRead("foo");
|
||||
assertReadAll(rotate, "bar");
|
||||
}
|
||||
|
||||
public void testRotate() throws Exception {
|
||||
final FileRotator rotate = new FileRotator(
|
||||
mBasePath, PREFIX, DAY_IN_MILLIS, WEEK_IN_MILLIS);
|
||||
|
||||
final RecordingReader reader = new RecordingReader();
|
||||
long currentTime = TEST_TIME;
|
||||
|
||||
// combine first record into file
|
||||
rotate.combineActive(reader, writer("foo"), currentTime);
|
||||
reader.assertRead();
|
||||
assertReadAll(rotate, "foo");
|
||||
|
||||
// push time a few minutes forward; shouldn't rotate file
|
||||
reader.reset();
|
||||
currentTime += MINUTE_IN_MILLIS;
|
||||
rotate.combineActive(reader, writer("bar"), currentTime);
|
||||
reader.assertRead("foo");
|
||||
assertReadAll(rotate, "bar");
|
||||
|
||||
// push time forward enough to rotate file; should still have same data
|
||||
currentTime += DAY_IN_MILLIS + SECOND_IN_MILLIS;
|
||||
rotate.maybeRotate(currentTime);
|
||||
assertReadAll(rotate, "bar");
|
||||
|
||||
// combine a second time, should leave rotated value untouched, and
|
||||
// active file should be empty.
|
||||
reader.reset();
|
||||
rotate.combineActive(reader, writer("baz"), currentTime);
|
||||
reader.assertRead();
|
||||
assertReadAll(rotate, "bar", "baz");
|
||||
}
|
||||
|
||||
public void testDelete() throws Exception {
|
||||
final FileRotator rotate = new FileRotator(
|
||||
mBasePath, PREFIX, MINUTE_IN_MILLIS, DAY_IN_MILLIS);
|
||||
|
||||
final RecordingReader reader = new RecordingReader();
|
||||
long currentTime = TEST_TIME;
|
||||
|
||||
// create first record and trigger rotating it
|
||||
rotate.combineActive(reader, writer("foo"), currentTime);
|
||||
reader.assertRead();
|
||||
currentTime += MINUTE_IN_MILLIS + SECOND_IN_MILLIS;
|
||||
rotate.maybeRotate(currentTime);
|
||||
|
||||
// create second record
|
||||
reader.reset();
|
||||
rotate.combineActive(reader, writer("bar"), currentTime);
|
||||
reader.assertRead();
|
||||
assertReadAll(rotate, "foo", "bar");
|
||||
|
||||
// push time far enough to expire first record
|
||||
currentTime = TEST_TIME + DAY_IN_MILLIS + (2 * MINUTE_IN_MILLIS);
|
||||
rotate.maybeRotate(currentTime);
|
||||
assertReadAll(rotate, "bar");
|
||||
|
||||
// push further to delete second record
|
||||
currentTime += WEEK_IN_MILLIS;
|
||||
rotate.maybeRotate(currentTime);
|
||||
assertReadAll(rotate);
|
||||
}
|
||||
|
||||
public void testThrowRestoresBackup() throws Exception {
|
||||
final FileRotator rotate = new FileRotator(
|
||||
mBasePath, PREFIX, MINUTE_IN_MILLIS, DAY_IN_MILLIS);
|
||||
|
||||
final RecordingReader reader = new RecordingReader();
|
||||
long currentTime = TEST_TIME;
|
||||
|
||||
// first, write some valid data
|
||||
rotate.combineActive(reader, writer("foo"), currentTime);
|
||||
reader.assertRead();
|
||||
assertReadAll(rotate, "foo");
|
||||
|
||||
try {
|
||||
// now, try writing which will throw
|
||||
reader.reset();
|
||||
rotate.combineActive(reader, new Writer() {
|
||||
public void write(OutputStream out) throws IOException {
|
||||
new DataOutputStream(out).writeUTF("bar");
|
||||
throw new ProtocolException("yikes");
|
||||
}
|
||||
}, currentTime);
|
||||
|
||||
fail("woah, somehow able to write exception");
|
||||
} catch (ProtocolException e) {
|
||||
// expected from above
|
||||
}
|
||||
|
||||
// assert that we read original data, and that it's still intact after
|
||||
// the failed write above.
|
||||
reader.assertRead("foo");
|
||||
assertReadAll(rotate, "foo");
|
||||
}
|
||||
|
||||
public void testOtherFilesAndMalformed() throws Exception {
|
||||
final FileRotator rotate = new FileRotator(
|
||||
mBasePath, PREFIX, SECOND_IN_MILLIS, SECOND_IN_MILLIS);
|
||||
|
||||
// should ignore another prefix
|
||||
touch("another_rotator.1024");
|
||||
touch("another_rotator.1024-2048");
|
||||
assertReadAll(rotate);
|
||||
|
||||
// verify that broken filenames don't crash
|
||||
touch("rotator");
|
||||
touch("rotator...");
|
||||
touch("rotator.-");
|
||||
touch("rotator.---");
|
||||
touch("rotator.a-b");
|
||||
touch("rotator_but_not_actually");
|
||||
assertReadAll(rotate);
|
||||
|
||||
// and make sure that we can read something from a legit file
|
||||
write("rotator.100-200", "meow");
|
||||
assertReadAll(rotate, "meow");
|
||||
}
|
||||
|
||||
private static final String RED = "red";
|
||||
private static final String GREEN = "green";
|
||||
private static final String BLUE = "blue";
|
||||
private static final String YELLOW = "yellow";
|
||||
|
||||
public void testQueryMatch() throws Exception {
|
||||
final FileRotator rotate = new FileRotator(
|
||||
mBasePath, PREFIX, HOUR_IN_MILLIS, YEAR_IN_MILLIS);
|
||||
|
||||
final RecordingReader reader = new RecordingReader();
|
||||
long currentTime = TEST_TIME;
|
||||
|
||||
// rotate a bunch of historical data
|
||||
rotate.maybeRotate(currentTime);
|
||||
rotate.combineActive(reader, writer(RED), currentTime);
|
||||
|
||||
currentTime += DAY_IN_MILLIS;
|
||||
rotate.maybeRotate(currentTime);
|
||||
rotate.combineActive(reader, writer(GREEN), currentTime);
|
||||
|
||||
currentTime += DAY_IN_MILLIS;
|
||||
rotate.maybeRotate(currentTime);
|
||||
rotate.combineActive(reader, writer(BLUE), currentTime);
|
||||
|
||||
currentTime += DAY_IN_MILLIS;
|
||||
rotate.maybeRotate(currentTime);
|
||||
rotate.combineActive(reader, writer(YELLOW), currentTime);
|
||||
|
||||
final String[] FULL_SET = { RED, GREEN, BLUE, YELLOW };
|
||||
|
||||
assertReadAll(rotate, FULL_SET);
|
||||
assertReadMatching(rotate, Long.MIN_VALUE, Long.MAX_VALUE, FULL_SET);
|
||||
assertReadMatching(rotate, Long.MIN_VALUE, currentTime, FULL_SET);
|
||||
assertReadMatching(rotate, TEST_TIME + SECOND_IN_MILLIS, currentTime, FULL_SET);
|
||||
|
||||
// should omit last value, since it only touches at currentTime
|
||||
assertReadMatching(rotate, TEST_TIME + SECOND_IN_MILLIS, currentTime - SECOND_IN_MILLIS,
|
||||
RED, GREEN, BLUE);
|
||||
|
||||
// check boundary condition
|
||||
assertReadMatching(rotate, TEST_TIME + DAY_IN_MILLIS, Long.MAX_VALUE, FULL_SET);
|
||||
assertReadMatching(rotate, TEST_TIME + DAY_IN_MILLIS + SECOND_IN_MILLIS, Long.MAX_VALUE,
|
||||
GREEN, BLUE, YELLOW);
|
||||
|
||||
// test range smaller than file
|
||||
final long blueStart = TEST_TIME + (DAY_IN_MILLIS * 2);
|
||||
final long blueEnd = TEST_TIME + (DAY_IN_MILLIS * 3);
|
||||
assertReadMatching(rotate, blueStart + SECOND_IN_MILLIS, blueEnd - SECOND_IN_MILLIS, BLUE);
|
||||
|
||||
// outside range should return nothing
|
||||
assertReadMatching(rotate, Long.MIN_VALUE, TEST_TIME - DAY_IN_MILLIS);
|
||||
}
|
||||
|
||||
public void testClockRollingBackwards() throws Exception {
|
||||
final FileRotator rotate = new FileRotator(
|
||||
mBasePath, PREFIX, DAY_IN_MILLIS, YEAR_IN_MILLIS);
|
||||
|
||||
final RecordingReader reader = new RecordingReader();
|
||||
long currentTime = TEST_TIME;
|
||||
|
||||
// create record at current time
|
||||
// --> foo
|
||||
rotate.combineActive(reader, writer("foo"), currentTime);
|
||||
reader.assertRead();
|
||||
assertReadAll(rotate, "foo");
|
||||
|
||||
// record a day in past; should create a new active file
|
||||
// --> bar
|
||||
currentTime -= DAY_IN_MILLIS;
|
||||
reader.reset();
|
||||
rotate.combineActive(reader, writer("bar"), currentTime);
|
||||
reader.assertRead();
|
||||
assertReadAll(rotate, "bar", "foo");
|
||||
|
||||
// verify that we rewrite current active file
|
||||
// bar --> baz
|
||||
currentTime += SECOND_IN_MILLIS;
|
||||
reader.reset();
|
||||
rotate.combineActive(reader, writer("baz"), currentTime);
|
||||
reader.assertRead("bar");
|
||||
assertReadAll(rotate, "baz", "foo");
|
||||
|
||||
// return to present and verify we write oldest active file
|
||||
// baz --> meow
|
||||
currentTime = TEST_TIME + SECOND_IN_MILLIS;
|
||||
reader.reset();
|
||||
rotate.combineActive(reader, writer("meow"), currentTime);
|
||||
reader.assertRead("baz");
|
||||
assertReadAll(rotate, "meow", "foo");
|
||||
|
||||
// current time should trigger rotate of older active file
|
||||
rotate.maybeRotate(currentTime);
|
||||
|
||||
// write active file, verify this time we touch original
|
||||
// foo --> yay
|
||||
reader.reset();
|
||||
rotate.combineActive(reader, writer("yay"), currentTime);
|
||||
reader.assertRead("foo");
|
||||
assertReadAll(rotate, "meow", "yay");
|
||||
}
|
||||
|
||||
@Suppress
|
||||
public void testFuzz() throws Exception {
|
||||
final FileRotator rotate = new FileRotator(
|
||||
mBasePath, PREFIX, HOUR_IN_MILLIS, DAY_IN_MILLIS);
|
||||
|
||||
final RecordingReader reader = new RecordingReader();
|
||||
long currentTime = TEST_TIME;
|
||||
|
||||
// walk forward through time, ensuring that files are cleaned properly
|
||||
final Random random = new Random();
|
||||
for (int i = 0; i < 1024; i++) {
|
||||
currentTime += Math.abs(random.nextLong()) % DAY_IN_MILLIS;
|
||||
|
||||
reader.reset();
|
||||
rotate.combineActive(reader, writer("meow"), currentTime);
|
||||
|
||||
if (random.nextBoolean()) {
|
||||
rotate.maybeRotate(currentTime);
|
||||
}
|
||||
}
|
||||
|
||||
rotate.maybeRotate(currentTime);
|
||||
|
||||
Log.d(TAG, "currentTime=" + currentTime);
|
||||
Log.d(TAG, Arrays.toString(mBasePath.list()));
|
||||
}
|
||||
|
||||
public void testRecoverAtomic() throws Exception {
|
||||
write("rotator.1024-2048", "foo");
|
||||
write("rotator.1024-2048.backup", "bar");
|
||||
write("rotator.2048-4096", "baz");
|
||||
write("rotator.2048-4096.no_backup", "");
|
||||
|
||||
final FileRotator rotate = new FileRotator(
|
||||
mBasePath, PREFIX, SECOND_IN_MILLIS, SECOND_IN_MILLIS);
|
||||
|
||||
// verify backup value was recovered; no_backup indicates that
|
||||
// corresponding file had no backup and should be discarded.
|
||||
assertReadAll(rotate, "bar");
|
||||
}
|
||||
|
||||
private void touch(String... names) throws IOException {
|
||||
for (String name : names) {
|
||||
final OutputStream out = new FileOutputStream(new File(mBasePath, name));
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void write(String name, String value) throws IOException {
|
||||
final DataOutputStream out = new DataOutputStream(
|
||||
new FileOutputStream(new File(mBasePath, name)));
|
||||
out.writeUTF(value);
|
||||
out.close();
|
||||
}
|
||||
|
||||
private static Writer writer(final String value) {
|
||||
return new Writer() {
|
||||
public void write(OutputStream out) throws IOException {
|
||||
new DataOutputStream(out).writeUTF(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void assertReadAll(FileRotator rotate, String... expected) throws IOException {
|
||||
assertReadMatching(rotate, Long.MIN_VALUE, Long.MAX_VALUE, expected);
|
||||
}
|
||||
|
||||
private static void assertReadMatching(
|
||||
FileRotator rotate, long matchStartMillis, long matchEndMillis, String... expected)
|
||||
throws IOException {
|
||||
final RecordingReader reader = new RecordingReader();
|
||||
rotate.readMatching(reader, matchStartMillis, matchEndMillis);
|
||||
reader.assertRead(expected);
|
||||
}
|
||||
|
||||
private static class RecordingReader implements Reader {
|
||||
private ArrayList<String> mActual = Lists.newArrayList();
|
||||
|
||||
public void read(InputStream in) throws IOException {
|
||||
mActual.add(new DataInputStream(in).readUTF());
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
mActual.clear();
|
||||
}
|
||||
|
||||
public void assertRead(String... expected) {
|
||||
assertEquals(expected.length, mActual.size());
|
||||
|
||||
final ArrayList<String> actualCopy = new ArrayList<String>(mActual);
|
||||
for (String value : expected) {
|
||||
if (!actualCopy.remove(value)) {
|
||||
final String expectedString = Arrays.toString(expected);
|
||||
final String actualString = Arrays.toString(mActual.toArray());
|
||||
fail("expected: " + expectedString + " but was: " + actualString);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user