This is an automated email from the ASF dual-hosted git repository.
ppkarwasz pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/logging-flume.git
The following commit(s) were added to refs/heads/trunk by this push:
new ae6c7db2 Fix the file channel and the test suite on Windows (#478)
ae6c7db2 is described below
commit ae6c7db22f7cab4d4dc496ae4cdb1d13130a8343
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Fri Jul 31 21:58:37 2026 +0200
Fix the file channel and the test suite on Windows (#478)
* Skip Taildir tests on non-POSIX file systems
TaildirSource tracks files by inode, so its tests cannot pass when the
default file system lacks a `unix` attribute view (e.g. on Windows).
Document that requirement in the user guide.
Assisted-By: Claude Fable 5 <[email protected]>
* Fix TestSyslogTcpSource.testSSLMessages on Windows
The test wrote to the TLS socket and immediately closed it. The client
never reads the TLS 1.3 session tickets sent by the server, so close()
aborts the connection with a TCP RST, which on Windows discards the
syslog record before the server reads it. Keep the socket open, waiting
with Awaitility, until the server commits the event to the channel.
Assisted-By: Claude Fable 5 <[email protected]>
* Add Windows to the CI matrix
Assisted-By: Claude Fable 5 <[email protected]>
* Unmap the checkpoint buffer when the backing store closes
On Java 17 nothing unmaps the checkpoint MappedByteBuffer, and on
Windows a mapped file cannot be deleted, so checkpoint recovery and
restarts fail. Unmap explicitly via sun.misc.Unsafe.invokeCleaner,
make close() idempotent, guard the accessors against use after unmap
(which would crash the JVM) and release the mapping when a constructor
throws BadCheckpointException or when upgrading a V2 checkpoint.
Assisted-By: Claude Fable 5 <[email protected]>
* Close the file channel resources on every failure path
MapDB 0.9.x cannot unmap its buffers on Java 17, so disable
memory-mapped files for the transient queue set. Close the queue and
backing store discarded by the bad-checkpoint recovery in Log.replay,
close the log of a channel whose start() failed, and release the
inflight files when the queue constructor throws. Windows keeps open
and mapped files locked, so any of these leaks made the channel unable
to restart over the same directories.
Assisted-By: Claude Fable 5 <[email protected]>
* Close file channel queues and stores in tests
Several tests leaked FlumeEventQueue instances or reused one temporary
directory across test methods, which fails on Windows where the leaked
MapDB files cannot be deleted. Also stop channels whose start() failed,
now that FileChannel releases the log in that case.
Assisted-By: Claude Fable 5 <[email protected]>
* Make the remaining file channel tests Windows-compatible
TestFileChannelRestart never closed the stream it corrupted the
checkpoint metadata with, so the recovery could not delete the file on
Windows. TestFileChannelEncryption asserted the POSIX rendering of a
FileNotFoundException message. TestFileChannelErrorMetrics injects I/O
errors by deleting files the running channel uses, which Windows does
not allow at all, so those tests are skipped there.
Assisted-By: Claude Fable 5 <[email protected]>
* Improve `checkNotClosed` comment
Co-authored-by: Copilot Autofix powered by AI
<[email protected]>
* Clean up the bad-checkpoint failure paths
Attach close failures to the original exception instead of logging
them separately, so they show up with their cause in the recovery
warnings. Fold the V3 constructor helper back into the constructor,
which lets metaDataFile become final.
Assisted-By: Claude Fable 5 <[email protected]>
* Release the MapDB before re-creating a queue in tests
The FlumeEventQueue constructor deletes the queueset directory, which
fails on Windows while the previous queue instance still holds the
MapDB open. Production code always calls replayComplete() before a
queue is abandoned, so do the same in the tests.
Assisted-By: Claude Fable 5 <[email protected]>
* Let the GC release the checkpoint mapping
Forcibly unmapping the checkpoint MappedByteBuffer via
sun.misc.Unsafe.invokeCleaner is unsafe: any access to an unmapped
buffer crashes the JVM. Drop the buffer references when the backing
store closes and let the garbage collector release the mapping instead.
The corrupt-checkpoint recovery path deletes checkpoint files moments
after the discarded store is closed, so retry the deletion with GC
cycles in between (a no-op when the first attempt succeeds). Tests that
delete a recently mapped checkpoint wait the same way via
TestUtils.awaitDelete. The V2 to V3 upgrade now reads the queue state,
closes the V2 store and backs up the file before rewriting it in place,
which needs no unmapping: writing to and re-mapping a mapped file is
allowed even on Windows, only deletion is blocked.
Also ratchet the spotbugs and pmd ceilings to the current counts.
Assisted-By: Claude Fable 5 <[email protected]>
* Shorten comments and use semantic line breaks
Assisted-By: Claude Fable 5 <[email protected]>
* Fix TestLogFile and TestLog on Windows
TestLogFile deleted the data file while the writer from setup() still
had it open, which Windows forbids. TestLog filled the disk to exactly
the minimum required space, so any temp files freed by other processes
on a shared CI runner pushed the usable space back above the limit
before the put checked it.
Assisted-By: Claude Fable 5 <[email protected]>
* Open java.lang to the EnvironmentVariables rule
On Windows the rule also patches
ProcessEnvironment.theCaseInsensitiveEnvironment, which lives in
java.lang, while on Linux opening java.util was enough.
Assisted-By: Claude Fable 5 <[email protected]>
* Retry directory deletion in TestFileChannelIntegrityTool
On Windows the checkpoint file of a stopped channel cannot be deleted
until the garbage collector releases its mapping. The failed teardown
leaked files into the shared directories and broke the following tests.
Assisted-By: Claude Fable 5 <[email protected]>
* Open java.lang to the EnvironmentVariables rule in config filter tests
Same fix as in flume-ng-node: on Windows the rule also patches
ProcessEnvironment.theCaseInsensitiveEnvironment, which lives in
java.lang.
Assisted-By: Claude Fable 5 <[email protected]>
* Skip TestExternalProcessConfigFilter on Windows
The external commands under test are shell scripts, which Windows
cannot execute.
Assisted-By: Claude Fable 5 <[email protected]>
---------
Co-authored-by: Copilot Autofix powered by AI
<[email protected]>
---
.github/workflows/build.yml | 2 +-
flume-ng-channels/flume-file-channel/pom.xml | 4 +-
.../file/EventQueueBackingStoreFactory.java | 20 ++-
.../channel/file/EventQueueBackingStoreFile.java | 101 +++++++++-----
.../channel/file/EventQueueBackingStoreFileV2.java | 25 ++--
.../channel/file/EventQueueBackingStoreFileV3.java | 146 +++++++++++----------
.../org/apache/flume/channel/file/FileChannel.java | 8 +-
.../apache/flume/channel/file/FlumeEventQueue.java | 90 ++++++++-----
.../java/org/apache/flume/channel/file/Log.java | 23 +++-
.../apache/flume/channel/file/Serialization.java | 36 ++++-
.../apache/flume/channel/file/TestCheckpoint.java | 9 ++
.../channel/file/TestCheckpointRebuilder.java | 13 +-
.../file/TestEventQueueBackingStoreFactory.java | 24 ++--
.../flume/channel/file/TestFileChannelBase.java | 3 +-
.../channel/file/TestFileChannelErrorMetrics.java | 17 +++
.../flume/channel/file/TestFileChannelRestart.java | 20 ++-
.../flume/channel/file/TestFlumeEventQueue.java | 36 ++++-
.../org/apache/flume/channel/file/TestLog.java | 8 +-
.../org/apache/flume/channel/file/TestLogFile.java | 4 +
.../org/apache/flume/channel/file/TestUtils.java | 39 ++++++
.../file/encryption/TestFileChannelEncryption.java | 8 +-
.../pom.xml | 5 +-
.../TestExternalProcessConfigFilter.java | 5 +
flume-ng-node/pom.xml | 5 +-
flume-ng-sources/flume-syslog-source/pom.xml | 6 +
.../flume/source/syslog/TestSyslogTcpSource.java | 22 +++-
.../source/taildir/TestTaildirEventReader.java | 11 ++
.../flume/source/taildir/TestTaildirSource.java | 11 ++
flume-parent/pom.xml | 8 ++
.../flume/tools/TestFileChannelIntegrityTool.java | 26 +++-
.../antora/modules/ROOT/pages/FlumeUserGuide.adoc | 5 +-
31 files changed, 549 insertions(+), 191 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index afbb2120..49f910d0 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -43,7 +43,7 @@ jobs:
# Don't cancel the remaining OS builds when one fails
fail-fast: false
matrix:
- os: [ ubuntu-latest ]
+ os: [ ubuntu-latest, windows-latest ]
java-distribution: [ temurin ]
#
# There is no protobuf 2.x version for `aarch64`.
diff --git a/flume-ng-channels/flume-file-channel/pom.xml
b/flume-ng-channels/flume-file-channel/pom.xml
index 9e7e219c..70c26cd0 100644
--- a/flume-ng-channels/flume-file-channel/pom.xml
+++ b/flume-ng-channels/flume-file-channel/pom.xml
@@ -30,8 +30,8 @@
<properties>
<!-- TODO fix spotbugs violations -->
- <spotbugs.maxAllowedViolations>284</spotbugs.maxAllowedViolations>
- <pmd.maxAllowedViolations>544</pmd.maxAllowedViolations>
+ <spotbugs.maxAllowedViolations>162</spotbugs.maxAllowedViolations>
+ <pmd.maxAllowedViolations>19</pmd.maxAllowedViolations>
<module.name>org.apache.flume.channel.file</module.name>
</properties>
diff --git
a/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/EventQueueBackingStoreFactory.java
b/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/EventQueueBackingStoreFactory.java
index db5bc899..39a7fb1c 100644
---
a/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/EventQueueBackingStoreFactory.java
+++
b/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/EventQueueBackingStoreFactory.java
@@ -20,6 +20,8 @@ import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
import org.apache.flume.channel.file.instrumentation.FileChannelCounter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -116,10 +118,26 @@ class EventQueueBackingStoreFactory {
logger.info("Attempting upgrade of " + checkpointFile + " for " +
name);
EventQueueBackingStoreFileV2 backingStoreV2 =
new EventQueueBackingStoreFileV2(checkpointFile, capacity,
name, counter);
+ int queueHead;
+ int queueSize;
+ long writeOrderID;
+ Map<Integer, AtomicInteger> referenceCounts;
+ try {
+ queueHead = backingStoreV2.getHead();
+ queueSize = backingStoreV2.getSize();
+ writeOrderID = backingStoreV2.getLogWriteOrderID();
+ referenceCounts = backingStoreV2.logFileIDReferenceCounts;
+ } finally {
+ // Close the V2 store before the file is copied and rewritten.
+ // The stale mapping lingers until garbage collected, which is
harmless:
+ // even on Windows a mapped file can be read, written and mapped
again.
+ backingStoreV2.close();
+ }
String backupName = checkpointFile.getName() + "-backup-" +
System.currentTimeMillis();
Files.copy(checkpointFile, new File(checkpointFile.getParentFile(),
backupName));
File metaDataFile = Serialization.getMetaDataFile(checkpointFile);
- EventQueueBackingStoreFileV3.upgrade(backingStoreV2, checkpointFile,
metaDataFile);
+ EventQueueBackingStoreFileV3.upgrade(
+ checkpointFile, metaDataFile, queueHead, queueSize,
writeOrderID, referenceCounts);
return new EventQueueBackingStoreFileV3(
checkpointFile, capacity, name, counter, backupCheckpointDir,
shouldBackup, compressBackup);
}
diff --git
a/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/EventQueueBackingStoreFile.java
b/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/EventQueueBackingStoreFile.java
index 11768082..43e835c4 100644
---
a/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/EventQueueBackingStoreFile.java
+++
b/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/EventQueueBackingStoreFile.java
@@ -56,7 +56,7 @@ abstract class EventQueueBackingStoreFile extends
EventQueueBackingStore {
protected LongBuffer elementsBuffer;
protected final Map<Integer, Long> overwriteMap = new HashMap<Integer,
Long>();
protected final Map<Integer, AtomicInteger> logFileIDReferenceCounts =
Maps.newHashMap();
- protected final MappedByteBuffer mappedBuffer;
+ private MappedByteBuffer mappedBuffer;
protected final RandomAccessFile checkpointFileHandle;
private final FileChannelCounter fileChannelCounter;
protected final File checkpointFile;
@@ -87,36 +87,53 @@ abstract class EventQueueBackingStoreFile extends
EventQueueBackingStore {
this.shouldBackup = backupCheckpoint;
this.compressBackup = compressBackup;
this.backupDir = checkpointBackupDir;
- checkpointFileHandle = new RandomAccessFile(checkpointFile, "rw");
- long totalBytes = (capacity + HEADER_SIZE) *
Serialization.SIZE_OF_LONG;
- if (checkpointFileHandle.length() == 0) {
- allocate(checkpointFile, totalBytes);
- checkpointFileHandle.seek(INDEX_VERSION *
Serialization.SIZE_OF_LONG);
- checkpointFileHandle.writeLong(getVersion());
- checkpointFileHandle.getChannel().force(true);
- logger.info("Preallocated " + checkpointFile + " to " +
checkpointFileHandle.length() + " for capacity "
- + capacity);
- }
- if (checkpointFile.length() != totalBytes) {
- String msg = "Configured capacity is " + capacity + " but the "
- + " checkpoint file capacity is "
- + ((checkpointFile.length() / Serialization.SIZE_OF_LONG)
- HEADER_SIZE)
- + ". See FileChannel documentation on how to change a
channels" + " capacity.";
- throw new BadCheckpointException(msg);
- }
- mappedBuffer =
checkpointFileHandle.getChannel().map(MapMode.READ_WRITE, 0,
checkpointFile.length());
- elementsBuffer = mappedBuffer.asLongBuffer();
+ // On failure, close the file handle and drop all references to the
mapping before rethrowing:
+ // the mapping is only released when the buffer is garbage collected,
+ // and a reachable buffer keeps the checkpoint file undeletable on
Windows.
+ RandomAccessFile checkpointFileHandle = new
RandomAccessFile(checkpointFile, "rw");
+ MappedByteBuffer mappedBuffer;
+ try {
+ long totalBytes = (capacity + HEADER_SIZE) *
Serialization.SIZE_OF_LONG;
+ if (checkpointFileHandle.length() == 0) {
+ allocate(checkpointFile, totalBytes);
+ checkpointFileHandle.seek(INDEX_VERSION *
Serialization.SIZE_OF_LONG);
+ checkpointFileHandle.writeLong(getVersion());
+ checkpointFileHandle.getChannel().force(true);
+ logger.info("Preallocated " + checkpointFile + " to " +
checkpointFileHandle.length() + " for capacity "
+ + capacity);
+ }
+ if (checkpointFile.length() != totalBytes) {
+ String msg = "Configured capacity is " + capacity + " but the "
+ + " checkpoint file capacity is "
+ + ((checkpointFile.length() /
Serialization.SIZE_OF_LONG) - HEADER_SIZE)
+ + ". See FileChannel documentation on how to change a
channels" + " capacity.";
+ throw new BadCheckpointException(msg);
+ }
+ mappedBuffer =
checkpointFileHandle.getChannel().map(MapMode.READ_WRITE, 0,
checkpointFile.length());
+ elementsBuffer = mappedBuffer.asLongBuffer();
- long version = elementsBuffer.get(INDEX_VERSION);
- if (version != (long) getVersion()) {
- throw new BadCheckpointException("Invalid version: " + version + "
" + name + ", expected " + getVersion());
- }
- long checkpointComplete = elementsBuffer.get(INDEX_CHECKPOINT_MARKER);
- if (checkpointComplete != (long) CHECKPOINT_COMPLETE) {
- throw new BadCheckpointException("Checkpoint was not completed
correctly,"
- + " probably because the agent stopped while the channel
was"
- + " checkpointing.");
+ long version = elementsBuffer.get(INDEX_VERSION);
+ if (version != (long) getVersion()) {
+ throw new BadCheckpointException(
+ "Invalid version: " + version + " " + name + ",
expected " + getVersion());
+ }
+ long checkpointComplete =
elementsBuffer.get(INDEX_CHECKPOINT_MARKER);
+ if (checkpointComplete != (long) CHECKPOINT_COMPLETE) {
+ throw new BadCheckpointException("Checkpoint was not completed
correctly,"
+ + " probably because the agent stopped while the
channel was"
+ + " checkpointing.");
+ }
+ } catch (IOException | RuntimeException e) {
+ elementsBuffer = null;
+ try {
+ checkpointFileHandle.close();
+ } catch (IOException closeEx) {
+ e.addSuppressed(closeEx);
+ }
+ throw e;
}
+ this.checkpointFileHandle = checkpointFileHandle;
+ this.mappedBuffer = mappedBuffer;
if (shouldBackup) {
checkpointBackUpExecutor = Executors.newSingleThreadExecutor(new
ThreadFactoryBuilder()
.setNameFormat(getName() + " - CheckpointBackUpThread")
@@ -127,6 +144,7 @@ abstract class EventQueueBackingStoreFile extends
EventQueueBackingStore {
}
protected long getCheckpointLogWriteOrderID() {
+ checkNotClosed();
return elementsBuffer.get(INDEX_WRITE_ORDER_ID);
}
@@ -223,8 +241,22 @@ abstract class EventQueueBackingStoreFile extends
EventQueueBackingStore {
}
}
+ /**
+ * Throws {@link IllegalStateException} if this store is closed.
+ *
+ * <p>A null {@code mappedBuffer} marks the store as closed.
+ * Methods that dereference {@code elementsBuffer} or {@code mappedBuffer}
should call this method first,
+ * to fail fast instead of throwing a NullPointerException.
+ */
+ private void checkNotClosed() {
+ if (mappedBuffer == null) {
+ throw new IllegalStateException("Backing store " + checkpointFile
+ " is closed");
+ }
+ }
+
@Override
void beginCheckpoint() throws IOException {
+ checkNotClosed();
logger.info("Start checkpoint for " + checkpointFile + ", elements to
sync = " + overwriteMap.size());
if (shouldBackup) {
@@ -248,7 +280,7 @@ abstract class EventQueueBackingStoreFile extends
EventQueueBackingStore {
@Override
void checkpoint() throws IOException {
-
+ checkNotClosed();
setLogWriteOrderID(WriteOrderOracle.next());
logger.info("Updating checkpoint metadata: logWriteOrderID: "
+ getLogWriteOrderID() + ", queueSize: " + getSize() + ",
queueHead: "
@@ -310,7 +342,14 @@ abstract class EventQueueBackingStoreFile extends
EventQueueBackingStore {
@Override
void close() {
+ if (mappedBuffer == null) {
+ return;
+ }
mappedBuffer.force();
+ // Drop the buffer references, so the mapping can be garbage collected
while the store is still reachable:
+ // the file cannot be deleted on Windows until the mapping is gone.
+ mappedBuffer = null;
+ elementsBuffer = null;
try {
checkpointFileHandle.close();
} catch (IOException e) {
@@ -329,6 +368,7 @@ abstract class EventQueueBackingStoreFile extends
EventQueueBackingStore {
@Override
long get(int index) {
+ checkNotClosed();
int realIndex = getPhysicalIndex(index);
long result = EMPTY;
if (overwriteMap.containsKey(realIndex)) {
@@ -346,6 +386,7 @@ abstract class EventQueueBackingStoreFile extends
EventQueueBackingStore {
@Override
void put(int index, long value) {
+ checkNotClosed();
int realIndex = getPhysicalIndex(index);
overwriteMap.put(realIndex, value);
}
diff --git
a/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/EventQueueBackingStoreFileV2.java
b/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/EventQueueBackingStoreFileV2.java
index 7dc53e74..cd06d24a 100644
---
a/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/EventQueueBackingStoreFileV2.java
+++
b/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/EventQueueBackingStoreFileV2.java
@@ -34,19 +34,24 @@ final class EventQueueBackingStoreFileV2 extends
EventQueueBackingStoreFile {
EventQueueBackingStoreFileV2(File checkpointFile, int capacity, String
name, FileChannelCounter counter)
throws IOException, BadCheckpointException {
super(capacity, name, counter, checkpointFile);
- Preconditions.checkArgument(capacity > 0, "capacity must be greater
than 0 " + capacity);
+ try {
+ Preconditions.checkArgument(capacity > 0, "capacity must be
greater than 0 " + capacity);
- setLogWriteOrderID(elementsBuffer.get(INDEX_WRITE_ORDER_ID));
- setSize((int) elementsBuffer.get(INDEX_SIZE));
- setHead((int) elementsBuffer.get(INDEX_HEAD));
+ setLogWriteOrderID(elementsBuffer.get(INDEX_WRITE_ORDER_ID));
+ setSize((int) elementsBuffer.get(INDEX_SIZE));
+ setHead((int) elementsBuffer.get(INDEX_HEAD));
- int indexMaxLog = INDEX_ACTIVE_LOG + MAX_ACTIVE_LOGS;
- for (int i = INDEX_ACTIVE_LOG; i < indexMaxLog; i++) {
- long nextFileCode = elementsBuffer.get(i);
- if (nextFileCode != EMPTY) {
- Pair<Integer, Integer> idAndCount =
deocodeActiveLogCounter(nextFileCode);
- logFileIDReferenceCounts.put(idAndCount.getLeft(), new
AtomicInteger(idAndCount.getRight()));
+ int indexMaxLog = INDEX_ACTIVE_LOG + MAX_ACTIVE_LOGS;
+ for (int i = INDEX_ACTIVE_LOG; i < indexMaxLog; i++) {
+ long nextFileCode = elementsBuffer.get(i);
+ if (nextFileCode != EMPTY) {
+ Pair<Integer, Integer> idAndCount =
deocodeActiveLogCounter(nextFileCode);
+ logFileIDReferenceCounts.put(idAndCount.getLeft(), new
AtomicInteger(idAndCount.getRight()));
+ }
}
+ } catch (RuntimeException e) {
+ close();
+ throw e;
}
}
diff --git
a/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/EventQueueBackingStoreFileV3.java
b/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/EventQueueBackingStoreFileV3.java
index 61ccbcd4..49e96ed6 100644
---
a/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/EventQueueBackingStoreFileV3.java
+++
b/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/EventQueueBackingStoreFileV3.java
@@ -49,75 +49,83 @@ final class EventQueueBackingStoreFileV3 extends
EventQueueBackingStoreFile {
boolean compressBackup)
throws IOException, BadCheckpointException {
super(capacity, name, counter, checkpointFile, checkpointBackupDir,
backupCheckpoint, compressBackup);
- Preconditions.checkArgument(capacity > 0, "capacity must be greater
than 0 " + capacity);
- metaDataFile = Serialization.getMetaDataFile(checkpointFile);
- logger.info("Starting up with " + checkpointFile + " and " +
metaDataFile);
- if (metaDataFile.exists()) {
- FileInputStream inputStream = new FileInputStream(metaDataFile);
- try {
- logger.info("Reading checkpoint metadata from " +
metaDataFile);
- ProtosFactory.Checkpoint checkpoint =
ProtosFactory.Checkpoint.parseDelimitedFrom(inputStream);
- if (checkpoint == null) {
+ try {
+ Preconditions.checkArgument(capacity > 0, "capacity must be
greater than 0 " + capacity);
+ metaDataFile = Serialization.getMetaDataFile(checkpointFile);
+ logger.info("Starting up with " + checkpointFile + " and " +
metaDataFile);
+ if (metaDataFile.exists()) {
+ FileInputStream inputStream = new
FileInputStream(metaDataFile);
+ try {
+ logger.info("Reading checkpoint metadata from " +
metaDataFile);
+ ProtosFactory.Checkpoint checkpoint =
ProtosFactory.Checkpoint.parseDelimitedFrom(inputStream);
+ if (checkpoint == null) {
+ throw new BadCheckpointException(
+ "The checkpoint metadata file does " + "not
exist or has zero length");
+ }
+ int version = checkpoint.getVersion();
+ if (version != getVersion()) {
+ throw new BadCheckpointException(
+ "Invalid version: " + version + " " + name +
", expected " + getVersion());
+ }
+ long logWriteOrderID = checkpoint.getWriteOrderID();
+ if (logWriteOrderID != getCheckpointLogWriteOrderID()) {
+ String msg = "Checkpoint and Meta files have differing
" + "logWriteOrderIDs "
+ + getCheckpointLogWriteOrderID() + ", and "
+ + logWriteOrderID;
+ logger.warn(msg);
+ throw new BadCheckpointException(msg);
+ }
+ WriteOrderOracle.setSeed(logWriteOrderID);
+ setLogWriteOrderID(logWriteOrderID);
+ setSize(checkpoint.getQueueSize());
+ setHead(checkpoint.getQueueHead());
+ for (ProtosFactory.ActiveLog activeLog :
checkpoint.getActiveLogsList()) {
+ Integer logFileID = activeLog.getLogFileID();
+ Integer count = activeLog.getCount();
+ logFileIDReferenceCounts.put(logFileID, new
AtomicInteger(count));
+ }
+ } catch (InvalidProtocolBufferException ex) {
throw new BadCheckpointException(
- "The checkpoint metadata file does " + "not exist
or has zero length");
+ "Checkpoint metadata file is invalid. "
+ + "The agent might have been stopped while
it was being "
+ + "written",
+ ex);
+ } finally {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ logger.warn("Unable to close " + metaDataFile, e);
+ }
}
- int version = checkpoint.getVersion();
- if (version != getVersion()) {
+ } else {
+ if (backupExists(checkpointBackupDir) && shouldBackup) {
+ // If a backup exists, then throw an exception to recover
checkpoint
throw new BadCheckpointException(
- "Invalid version: " + version + " " + name + ",
expected " + getVersion());
- }
- long logWriteOrderID = checkpoint.getWriteOrderID();
- if (logWriteOrderID != getCheckpointLogWriteOrderID()) {
- String msg = "Checkpoint and Meta files have differing " +
"logWriteOrderIDs "
- + getCheckpointLogWriteOrderID() + ", and "
- + logWriteOrderID;
- logger.warn(msg);
- throw new BadCheckpointException(msg);
+ "The checkpoint metadata file does " + "not exist,
but a backup exists");
}
- WriteOrderOracle.setSeed(logWriteOrderID);
- setLogWriteOrderID(logWriteOrderID);
- setSize(checkpoint.getQueueSize());
- setHead(checkpoint.getQueueHead());
- for (ProtosFactory.ActiveLog activeLog :
checkpoint.getActiveLogsList()) {
- Integer logFileID = activeLog.getLogFileID();
- Integer count = activeLog.getCount();
- logFileIDReferenceCounts.put(logFileID, new
AtomicInteger(count));
- }
- } catch (InvalidProtocolBufferException ex) {
- throw new BadCheckpointException(
- "Checkpoint metadata file is invalid. "
- + "The agent might have been stopped while it
was being "
- + "written",
- ex);
- } finally {
+ ProtosFactory.Checkpoint.Builder checkpointBuilder =
ProtosFactory.Checkpoint.newBuilder();
+ checkpointBuilder.setVersion(getVersion());
+ checkpointBuilder.setQueueHead(getHead());
+ checkpointBuilder.setQueueSize(getSize());
+ checkpointBuilder.setWriteOrderID(getLogWriteOrderID());
+ FileOutputStream outputStream = new
FileOutputStream(metaDataFile);
try {
- inputStream.close();
- } catch (IOException e) {
- logger.warn("Unable to close " + metaDataFile, e);
- }
- }
- } else {
- if (backupExists(checkpointBackupDir) && shouldBackup) {
- // If a backup exists, then throw an exception to recover
checkpoint
- throw new BadCheckpointException(
- "The checkpoint metadata file does " + "not exist, but
a backup exists");
- }
- ProtosFactory.Checkpoint.Builder checkpointBuilder =
ProtosFactory.Checkpoint.newBuilder();
- checkpointBuilder.setVersion(getVersion());
- checkpointBuilder.setQueueHead(getHead());
- checkpointBuilder.setQueueSize(getSize());
- checkpointBuilder.setWriteOrderID(getLogWriteOrderID());
- FileOutputStream outputStream = new FileOutputStream(metaDataFile);
- try {
- checkpointBuilder.build().writeDelimitedTo(outputStream);
- outputStream.getChannel().force(true);
- } finally {
- try {
- outputStream.close();
- } catch (IOException e) {
- logger.warn("Unable to close " + metaDataFile, e);
+ checkpointBuilder.build().writeDelimitedTo(outputStream);
+ outputStream.getChannel().force(true);
+ } finally {
+ try {
+ outputStream.close();
+ } catch (IOException e) {
+ logger.warn("Unable to close " + metaDataFile, e);
+ }
}
}
+ } catch (IOException | RuntimeException e) {
+ // Close the store opened by the superclass:
+ // dropping the buffer references lets the garbage collector
release the mapping
+ // before the recovery path deletes the checkpoint file.
+ close();
+ throw e;
}
}
@@ -159,14 +167,14 @@ final class EventQueueBackingStoreFileV3 extends
EventQueueBackingStoreFile {
}
}
- static void upgrade(EventQueueBackingStoreFileV2 backingStoreV2, File
checkpointFile, File metaDataFile)
+ static void upgrade(
+ File checkpointFile,
+ File metaDataFile,
+ int head,
+ int size,
+ long writeOrderID,
+ Map<Integer, AtomicInteger> referenceCounts)
throws IOException {
-
- int head = backingStoreV2.getHead();
- int size = backingStoreV2.getSize();
- long writeOrderID = backingStoreV2.getLogWriteOrderID();
- Map<Integer, AtomicInteger> referenceCounts =
backingStoreV2.logFileIDReferenceCounts;
-
ProtosFactory.Checkpoint.Builder checkpointBuilder =
ProtosFactory.Checkpoint.newBuilder();
checkpointBuilder.setVersion(Serialization.VERSION_3);
checkpointBuilder.setQueueHead(head);
diff --git
a/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/FileChannel.java
b/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/FileChannel.java
index bd9b9c3a..2c7d52d2 100644
---
a/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/FileChannel.java
+++
b/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/FileChannel.java
@@ -317,7 +317,7 @@ public class FileChannel extends BasicChannelSemantics
implements TransactionCap
public synchronized void stop() {
logger.info("Stopping {}...", this);
startupError = null;
- int size = getDepth();
+ int size = open ? getDepth() : 0;
close();
if (!open) {
channelCounter.setChannelSize(size);
@@ -370,8 +370,10 @@ public class FileChannel extends BasicChannelSemantics
implements TransactionCap
}
void close() {
- if (open) {
- setOpen(false);
+ setOpen(false);
+ // Close the log even when the channel never opened:
+ // a failed start() leaves a partially built log whose open files must
still be released.
+ if (log != null) {
try {
log.close();
} catch (Exception e) {
diff --git
a/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/FlumeEventQueue.java
b/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/FlumeEventQueue.java
index accabf23..ef67f87c 100644
---
a/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/FlumeEventQueue.java
+++
b/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/FlumeEventQueue.java
@@ -84,31 +84,63 @@ final class FlumeEventQueue {
logger.error("Could not read checkpoint.", e);
throw e;
}
- if (queueSetDBDir.isDirectory()) {
- FileUtils.deleteDirectory(queueSetDBDir);
- } else if (queueSetDBDir.isFile() && !queueSetDBDir.delete()) {
- throw new IOException("QueueSetDir " + queueSetDBDir + " is a file
and" + " could not be deleted");
- }
- if (!queueSetDBDir.mkdirs()) {
- throw new IllegalStateException("Could not create QueueSet Dir " +
queueSetDBDir);
- }
- File dbFile = new File(queueSetDBDir, "db");
- db = DBMaker.newFileDB(dbFile)
- .closeOnJvmShutdown()
- .transactionDisable()
- .syncOnCommitDisable()
- .deleteFilesAfterClose()
- .cacheDisable()
- .mmapFileEnableIfSupported()
- .make();
- queueSet =
- db.createHashSet("QueueSet " + " - " +
backingStore.getName()).make();
- long start = System.currentTimeMillis();
- for (int i = 0; i < backingStore.getSize(); i++) {
- queueSet.add(get(i));
+ try {
+ if (queueSetDBDir.isDirectory()) {
+ FileUtils.deleteDirectory(queueSetDBDir);
+ } else if (queueSetDBDir.isFile() && !queueSetDBDir.delete()) {
+ throw new IOException("QueueSetDir " + queueSetDBDir + " is a
file and" + " could not be deleted");
+ }
+ if (!queueSetDBDir.mkdirs()) {
+ throw new IllegalStateException("Could not create QueueSet Dir
" + queueSetDBDir);
+ }
+ File dbFile = new File(queueSetDBDir, "db");
+ // Don't enable memory-mapped files:
+ // MapDB 0.9.x cannot unmap its buffers on Java 17,
+ // so `deleteFilesAfterClose()` would silently fail on Windows
+ // and the next construction over the same directory could not
delete the files.
+ db = DBMaker.newFileDB(dbFile)
+ .closeOnJvmShutdown()
+ .transactionDisable()
+ .syncOnCommitDisable()
+ .deleteFilesAfterClose()
+ .cacheDisable()
+ .make();
+ queueSet = db.createHashSet("QueueSet " + " - " +
backingStore.getName())
+ .make();
+ long start = System.currentTimeMillis();
+ for (int i = 0; i < backingStore.getSize(); i++) {
+ queueSet.add(get(i));
+ }
+ logger.info("QueueSet population inserting " +
backingStore.getSize() + " took "
+ + (System.currentTimeMillis() - start));
+ } catch (Exception e) {
+ closeQuietly();
+ throw e;
+ }
+ }
+
+ /** Releases the resources this constructor opened, without closing the
backing store. */
+ private void closeQuietly() {
+ try {
+ if (db != null) {
+ db.close();
+ }
+ } catch (Exception ex) {
+ logger.warn("Error closing db", ex);
+ } finally {
+ db = null;
+ queueSet = null;
+ }
+ try {
+ inflightPuts.close();
+ } catch (IOException ex) {
+ logger.warn("Error closing inflight puts", ex);
+ }
+ try {
+ inflightTakes.close();
+ } catch (IOException ex) {
+ logger.warn("Error closing inflight takes", ex);
}
- logger.info("QueueSet population inserting " + backingStore.getSize()
+ " took "
- + (System.currentTimeMillis() - start));
}
SetMultimap<Long, Long> deserializeInflightPuts() throws IOException,
BadCheckpointException {
@@ -377,20 +409,12 @@ final class FlumeEventQueue {
}
synchronized void close() throws IOException {
- try {
- if (db != null) {
- db.close();
- }
- } catch (Exception ex) {
- logger.warn("Error closing db", ex);
- }
try {
backingStore.close();
- inflightPuts.close();
- inflightTakes.close();
} catch (IOException e) {
logger.warn("Error closing backing store", e);
}
+ closeQuietly();
}
/**
diff --git
a/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/Log.java
b/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/Log.java
index 68d37913..c7da2159 100644
---
a/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/Log.java
+++
b/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/Log.java
@@ -485,6 +485,23 @@ public class Log {
*/
doReplay(queue, dataFiles, encryptionKeyProvider,
shouldFastReplay);
} catch (BadCheckpointException ex) {
+ // Close the discarded queue and backing store and drop all
references to them:
+ // the checkpoint mapping is only released when the buffer is
garbage collected,
+ // and the deletion below cannot succeed on Windows until then.
+ if (queue != null) {
+ try {
+ queue.close();
+ } catch (Exception closeEx) {
+ ex.addSuppressed(closeEx);
+ }
+ queue = null;
+ } else if (backingStore != null) {
+ try {
+ backingStore.close();
+ } catch (Exception closeEx) {
+ ex.addSuppressed(closeEx);
+ }
+ }
backupRestored = false;
if (useDualCheckpoints) {
logger.warn(
@@ -839,7 +856,7 @@ public class Log {
try {
open = false;
try {
- if (checkpointOnClose) {
+ if (checkpointOnClose && queue != null) {
writeCheckpoint(true); // do this before acquiring
exclusive lock
}
} catch (Exception err) {
@@ -865,7 +882,9 @@ public class Log {
}
}
}
- queue.close();
+ if (queue != null) {
+ queue.close();
+ }
try {
unlock(checkpointDir);
} catch (IOException ex) {
diff --git
a/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/Serialization.java
b/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/Serialization.java
index 309f1ac6..b58d2c44 100644
---
a/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/Serialization.java
+++
b/flume-ng-channels/flume-file-channel/src/main/java/org/apache/flume/channel/file/Serialization.java
@@ -26,10 +26,12 @@ import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Collections;
import java.util.Set;
+import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import org.apache.commons.io.FileUtils;
import org.apache.flume.annotations.InterfaceAudience;
import org.apache.flume.annotations.InterfaceStability;
+import org.apache.flume.conf.internal.SuppressFBWarnings;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.xerial.snappy.SnappyInputStream;
@@ -53,6 +55,10 @@ public class Serialization {
// 64 K buffer to copy and compress files.
private static final int FILE_BUFFER_SIZE = 64 * 1024;
+ // Retries for deleting files that a garbage-collectable mapping still
pins.
+ private static final int DELETE_ATTEMPTS = 10;
+ private static final long DELETE_RETRY_DELAY_MILLIS = 100;
+
private static final Logger logger = LogManager.getLogger();
static File getMetaDataTempFile(File metaDataFile) {
@@ -102,7 +108,7 @@ public class Serialization {
logger.info("Skipping " + file.getName() + " because it is in
excludes " + "set");
continue;
}
- if (!FileUtils.deleteQuietly(file)) {
+ if (!deleteWithRetries(file)) {
logger.info(builder.toString());
logger.error("Error while attempting to delete: " +
file.getAbsolutePath());
return false;
@@ -114,6 +120,34 @@ public class Serialization {
return true;
}
+ /**
+ * Deletes a file, retrying with garbage collection cycles in between.
+ *
+ * <p>On Windows the checkpoint file cannot be deleted while a discarded
backing store still maps it.
+ * Nothing unmaps the buffer explicitly,
+ * so ask for a collection and retry a bounded number of times.
+ * The first attempt is free, so healthy platforms and unmapped files pay
nothing.
+ */
+ @SuppressFBWarnings(value = "DM_GC")
+ private static boolean deleteWithRetries(File file) {
+ if (FileUtils.deleteQuietly(file)) {
+ return true;
+ }
+ for (int attempt = 0; attempt < DELETE_ATTEMPTS; attempt++) {
+ System.gc();
+ try {
+ TimeUnit.MILLISECONDS.sleep(DELETE_RETRY_DELAY_MILLIS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return false;
+ }
+ if (FileUtils.deleteQuietly(file)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Copy a file using a 64K size buffer. This method will copy the file and
* then fsync to disk
diff --git
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestCheckpoint.java
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestCheckpoint.java
index 54b51222..4fffa2dd 100644
---
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestCheckpoint.java
+++
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestCheckpoint.java
@@ -19,6 +19,7 @@ package org.apache.flume.channel.file;
import java.io.File;
import java.io.IOException;
import junit.framework.Assert;
+import org.apache.commons.io.FileUtils;
import org.apache.flume.channel.file.instrumentation.FileChannelCounter;
import org.junit.After;
import org.junit.Before;
@@ -43,6 +44,9 @@ public class TestCheckpoint {
@After
public void cleanup() {
file.delete();
+ inflightPuts.delete();
+ inflightTakes.delete();
+ FileUtils.deleteQuietly(queueSet);
}
@Test
@@ -52,12 +56,17 @@ public class TestCheckpoint {
FlumeEventPointer ptrIn = new FlumeEventPointer(10, 20);
FlumeEventQueue queueIn = new FlumeEventQueue(backingStore,
inflightTakes, inflightPuts, queueSet);
queueIn.addHead(ptrIn);
+ // The three queues share one backing store and one queue set
directory,
+ // so each queue must release the queue set database before the next
queue is created.
+ queueIn.replayComplete();
FlumeEventQueue queueOut = new FlumeEventQueue(backingStore,
inflightTakes, inflightPuts, queueSet);
Assert.assertEquals(0, queueOut.getLogWriteOrderID());
+ queueOut.replayComplete();
queueIn.checkpoint(false);
FlumeEventQueue queueOut2 = new FlumeEventQueue(backingStore,
inflightTakes, inflightPuts, queueSet);
FlumeEventPointer ptrOut = queueOut2.removeHead(0L);
Assert.assertEquals(ptrIn, ptrOut);
Assert.assertTrue(queueOut2.getLogWriteOrderID() > 0);
+ queueOut2.close();
}
}
diff --git
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestCheckpointRebuilder.java
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestCheckpointRebuilder.java
index 7ebebc7e..2e40edcf 100644
---
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestCheckpointRebuilder.java
+++
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestCheckpointRebuilder.java
@@ -53,20 +53,27 @@ public class TestCheckpointRebuilder extends
TestFileChannelBase {
Assert.assertTrue(channel.isOpen());
Set<String> in = fillChannel(channel, "checkpointBulder");
channel.stop();
+ // Drop the channel reference so the GC can release the checkpoint
mapping.
+ channel = null;
File checkpointFile = new File(checkpointDir, "checkpoint");
File metaDataFile = Serialization.getMetaDataFile(checkpointFile);
File inflightTakesFile = new File(checkpointDir, "inflighttakes");
File inflightPutsFile = new File(checkpointDir, "inflightputs");
File queueSetDir = new File(checkpointDir, "queueset");
- Assert.assertTrue(checkpointFile.delete());
+ TestUtils.awaitDelete(checkpointFile);
Assert.assertTrue(metaDataFile.delete());
Assert.assertTrue(inflightTakesFile.delete());
Assert.assertTrue(inflightPutsFile.delete());
EventQueueBackingStore backingStore =
EventQueueBackingStoreFactory.get(checkpointFile, 50, "test",
new FileChannelCounter("test"));
FlumeEventQueue queue = new FlumeEventQueue(backingStore,
inflightTakesFile, inflightPutsFile, queueSetDir);
- CheckpointRebuilder checkpointRebuilder = new
CheckpointRebuilder(getAllLogs(dataDirs), queue, true);
- Assert.assertTrue(checkpointRebuilder.rebuild());
+ try {
+ CheckpointRebuilder checkpointRebuilder = new
CheckpointRebuilder(getAllLogs(dataDirs), queue, true);
+ Assert.assertTrue(checkpointRebuilder.rebuild());
+ } finally {
+ // Release the checkpoint files before the channel below replays
them.
+ queue.close();
+ }
channel = createFileChannel(overrides);
channel.start();
Assert.assertTrue(channel.isOpen());
diff --git
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestEventQueueBackingStoreFactory.java
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestEventQueueBackingStoreFactory.java
index fb26f382..1c8c8f12 100644
---
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestEventQueueBackingStoreFactory.java
+++
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestEventQueueBackingStoreFactory.java
@@ -272,16 +272,20 @@ public class TestEventQueueBackingStoreFactory {
private void verify(EventQueueBackingStore backingStore, long
expectedVersion, List<Long> expectedPointers)
throws Exception {
FlumeEventQueue queue = new FlumeEventQueue(backingStore,
inflightTakes, inflightPuts, queueSetDir);
- List<Long> actualPointers = Lists.newArrayList();
- FlumeEventPointer ptr;
- while ((ptr = queue.removeHead(0L)) != null) {
- actualPointers.add(ptr.toLong());
+ try {
+ List<Long> actualPointers = Lists.newArrayList();
+ FlumeEventPointer ptr;
+ while ((ptr = queue.removeHead(0L)) != null) {
+ actualPointers.add(ptr.toLong());
+ }
+ Assert.assertEquals(expectedPointers, actualPointers);
+ Assert.assertEquals(10, backingStore.getCapacity());
+ DataInputStream in = new DataInputStream(new
FileInputStream(checkpoint));
+ long actualVersion = in.readLong();
+ Assert.assertEquals(expectedVersion, actualVersion);
+ in.close();
+ } finally {
+ queue.close();
}
- Assert.assertEquals(expectedPointers, actualPointers);
- Assert.assertEquals(10, backingStore.getCapacity());
- DataInputStream in = new DataInputStream(new
FileInputStream(checkpoint));
- long actualVersion = in.readLong();
- Assert.assertEquals(expectedVersion, actualVersion);
- in.close();
}
}
diff --git
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestFileChannelBase.java
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestFileChannelBase.java
index 46257d14..bc821af2 100644
---
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestFileChannelBase.java
+++
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestFileChannelBase.java
@@ -70,7 +70,8 @@ public class TestFileChannelBase {
@After
public void teardown() {
- if (channel != null && channel.isOpen()) {
+ // Stop the channel even when it failed to start: it still holds open
files.
+ if (channel != null) {
channel.stop();
}
FileUtils.deleteQuietly(baseDir);
diff --git
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestFileChannelErrorMetrics.java
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestFileChannelErrorMetrics.java
index 4b435ec0..22bfebee 100644
---
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestFileChannelErrorMetrics.java
+++
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestFileChannelErrorMetrics.java
@@ -33,6 +33,7 @@ import org.apache.flume.Transaction;
import org.apache.flume.channel.file.instrumentation.FileChannelCounter;
import org.apache.flume.event.EventBuilder;
import org.apache.flume.exception.ChannelException;
+import org.junit.Assume;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
@@ -46,12 +47,26 @@ public class TestFileChannelErrorMetrics extends
TestFileChannelBase {
super(1);
}
+ /**
+ * Skips tests that delete in-use files on Windows.
+ *
+ * <p>These tests inject I/O errors by deleting the channel's files while
the channel is using them.
+ * Windows does not allow deleting files that are in use,
+ * so the deletion fails in the test instead of causing the intended
errors in the channel.
+ */
+ private static void requireOpenFileDeletion() {
+ Assume.assumeFalse(
+ "Windows cannot delete files that are in use",
+ System.getProperty("os.name").startsWith("Windows"));
+ }
+
/**
* This tests multiple successful and failed put and take operations
* and checks the values of the channel's counters.
*/
@Test
public void testEventTakePutErrorCount() throws Exception {
+ requireOpenFileDeletion();
final long usableSpaceRefreshInterval = 1;
FileChannel channel = Mockito.spy(createFileChannel());
Mockito.when(channel.createLogBuilder()).then(new
Answer<Log.Builder>() {
@@ -161,6 +176,7 @@ public class TestFileChannelErrorMetrics extends
TestFileChannelBase {
@Test
public void testCheckpointWriteErrorCount() throws Exception {
+ requireOpenFileDeletion();
int checkpointInterval = 1500;
final FileChannel channel = createFileChannel(Collections.singletonMap(
FileChannelConfiguration.CHECKPOINT_INTERVAL,
String.valueOf(checkpointInterval)));
@@ -262,6 +278,7 @@ public class TestFileChannelErrorMetrics extends
TestFileChannelBase {
@Test
public void testCheckpointBackupWriteErrorShouldIncreaseCounter2() throws
Exception {
+ requireOpenFileDeletion();
int checkpointInterval = 1500;
Map config = new HashMap();
config.put(FileChannelConfiguration.CHECKPOINT_INTERVAL,
String.valueOf(checkpointInterval));
diff --git
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestFileChannelRestart.java
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestFileChannelRestart.java
index 69bb9a4b..f18b04b3 100644
---
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestFileChannelRestart.java
+++
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestFileChannelRestart.java
@@ -126,8 +126,10 @@ public class TestFileChannelRestart extends
TestFileChannelBase {
}
channel.stop();
if (deleteCheckpoint) {
+ // Drop the channel reference so the GC can release the checkpoint
mapping.
+ channel = null;
File checkpoint = new File(checkpointDir, "checkpoint");
- Assert.assertTrue(checkpoint.delete());
+ TestUtils.awaitDelete(checkpoint);
File checkpointMetaData =
Serialization.getMetaDataFile(checkpoint);
Assert.assertTrue(checkpointMetaData.delete());
}
@@ -161,8 +163,10 @@ public class TestFileChannelRestart extends
TestFileChannelBase {
Thread.sleep(2000);
}
channel.stop();
+ // Drop the channel reference so the GC can release the checkpoint
mapping.
+ channel = null;
File checkpoint = new File(checkpointDir, "checkpoint");
- Assert.assertTrue(checkpoint.delete());
+ TestUtils.awaitDelete(checkpoint);
File checkpointMetaData = Serialization.getMetaDataFile(checkpoint);
Assert.assertTrue(checkpointMetaData.exists());
channel = createFileChannel(overrides);
@@ -235,10 +239,12 @@ public class TestFileChannelRestart extends
TestFileChannelBase {
Thread.sleep(2000);
}
channel.stop();
+ // Drop the channel reference so the GC can release the checkpoint
mapping.
+ channel = null;
File checkpoint = new File(checkpointDir, "checkpoint");
File checkpointMetaData = Serialization.getMetaDataFile(checkpoint);
Assert.assertTrue(checkpointMetaData.delete());
- Assert.assertTrue(checkpoint.delete());
+ TestUtils.awaitDelete(checkpoint);
channel = createFileChannel(overrides);
channel.start();
Assert.assertTrue(channel.isOpen());
@@ -317,6 +323,8 @@ public class TestFileChannelRestart extends
TestFileChannelBase {
FileOutputStream os = new
FileOutputStream(Serialization.getMetaDataFile(checkpoint));
meta.toBuilder().setVersion(2).build().writeDelimitedTo(os);
os.flush();
+ // Close the stream, or the recovery below cannot delete the file on
Windows.
+ os.close();
channel = createFileChannel(overrides);
channel.start();
Assert.assertTrue(channel.isOpen());
@@ -356,6 +364,8 @@ public class TestFileChannelRestart extends
TestFileChannelBase {
FileOutputStream os = new
FileOutputStream(Serialization.getMetaDataFile(checkpoint));
meta.toBuilder().setWriteOrderID(12).build().writeDelimitedTo(os);
os.flush();
+ // Close the stream, or the recovery below cannot delete the file on
Windows.
+ os.close();
channel = createFileChannel(overrides);
channel.start();
Assert.assertTrue(channel.isOpen());
@@ -869,8 +879,10 @@ public class TestFileChannelRestart extends
TestFileChannelBase {
Assert.assertEquals(compressedBackupCheckpoint.exists(),
originalCheckpointCompressed);
Assert.assertEquals(uncompressedBackupCheckpoint.exists(),
!originalCheckpointCompressed);
channel.stop();
+ // Drop the channel reference so the GC can release the checkpoint
mapping.
+ channel = null;
File checkpoint = new File(checkpointDir, "checkpoint");
- Assert.assertTrue(checkpoint.delete());
+ TestUtils.awaitDelete(checkpoint);
File checkpointMetaData = Serialization.getMetaDataFile(checkpoint);
Assert.assertTrue(checkpointMetaData.delete());
overrides.put(
diff --git
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestFlumeEventQueue.java
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestFlumeEventQueue.java
index c8c3c2a9..f5f148ce 100644
---
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestFlumeEventQueue.java
+++
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestFlumeEventQueue.java
@@ -54,6 +54,17 @@ public class TestFlumeEventQueue {
File queueSetDir;
EventQueueBackingStoreSupplier() {
+ reset();
+ }
+
+ /**
+ * Creates a fresh directory for each test.
+ *
+ * <p>The supplier instances are shared by all test methods of a
parameter.
+ * A file leaked by one test, e.g. still open on Windows,
+ * must not break the cleanup of the following tests.
+ */
+ void reset() {
baseDir = Files.createTempDir();
checkpoint = new File(baseDir, "checkpoint");
inflightTakes = new File(baseDir, "inflightputs");
@@ -117,11 +128,16 @@ public class TestFlumeEventQueue {
@Before
public void setup() throws Exception {
+ backingStoreSupplier.reset();
backingStore = backingStoreSupplier.get();
}
@After
public void cleanup() throws IOException {
+ if (queue != null) {
+ queue.close();
+ queue = null;
+ }
if (backingStore != null) {
backingStore.close();
}
@@ -131,8 +147,10 @@ public class TestFlumeEventQueue {
@Test
public void testCapacity() throws Exception {
backingStore.close();
+ // Drop the reference so the GC can release the mapping and the file
can be deleted.
+ backingStore = null;
File checkpoint = backingStoreSupplier.getCheckpoint();
- Assert.assertTrue(checkpoint.delete());
+ TestUtils.awaitDelete(checkpoint);
backingStore = new EventQueueBackingStoreFileV2(checkpoint, 1, "test",
new FileChannelCounter("test"));
queue = new FlumeEventQueue(
backingStore,
@@ -146,8 +164,10 @@ public class TestFlumeEventQueue {
@Test(expected = IllegalArgumentException.class)
public void testInvalidCapacityZero() throws Exception {
backingStore.close();
+ // Drop the reference so the GC can release the mapping and the file
can be deleted.
+ backingStore = null;
File checkpoint = backingStoreSupplier.getCheckpoint();
- Assert.assertTrue(checkpoint.delete());
+ TestUtils.awaitDelete(checkpoint);
backingStore = new EventQueueBackingStoreFileV2(checkpoint, 0, "test",
new FileChannelCounter("test"));
queue = new FlumeEventQueue(
backingStore,
@@ -159,8 +179,10 @@ public class TestFlumeEventQueue {
@Test(expected = IllegalArgumentException.class)
public void testInvalidCapacityNegative() throws Exception {
backingStore.close();
+ // Drop the reference so the GC can release the mapping and the file
can be deleted.
+ backingStore = null;
File checkpoint = backingStoreSupplier.getCheckpoint();
- Assert.assertTrue(checkpoint.delete());
+ TestUtils.awaitDelete(checkpoint);
backingStore = new EventQueueBackingStoreFileV2(checkpoint, -1,
"test", new FileChannelCounter("test"));
queue = new FlumeEventQueue(
backingStore,
@@ -400,6 +422,8 @@ public class TestFlumeEventQueue {
queue.addWithoutCommit(new FlumeEventPointer(2, 2), txnID2);
queue.checkpoint(true);
TimeUnit.SECONDS.sleep(3L);
+ // Close the MapDB, so that the new queue can delete the queueset
directory on Windows.
+ queue.replayComplete();
queue = new FlumeEventQueue(
backingStore,
backingStoreSupplier.getInflightTakes(),
@@ -428,6 +452,8 @@ public class TestFlumeEventQueue {
queue.removeHead(txnID2);
queue.checkpoint(true);
TimeUnit.SECONDS.sleep(3L);
+ // Close the MapDB, so that the new queue can delete the queueset
directory on Windows.
+ queue.replayComplete();
queue = new FlumeEventQueue(
backingStore,
backingStoreSupplier.getInflightTakes(),
@@ -458,6 +484,8 @@ public class TestFlumeEventQueue {
inflight = new
RandomAccessFile(backingStoreSupplier.getInflightPuts(), "rw");
inflight.seek(0);
inflight.writeInt(new Random().nextInt());
+ // Close the MapDB, so that the new queue can delete the queueset
directory on Windows.
+ queue.replayComplete();
queue = new FlumeEventQueue(
backingStore,
backingStoreSupplier.getInflightTakes(),
@@ -491,6 +519,8 @@ public class TestFlumeEventQueue {
inflight = new
RandomAccessFile(backingStoreSupplier.getInflightTakes(), "rw");
inflight.seek(0);
inflight.writeInt(new Random().nextInt());
+ // Close the MapDB, so that the new queue can delete the queueset
directory on Windows.
+ queue.replayComplete();
queue = new FlumeEventQueue(
backingStore,
backingStoreSupplier.getInflightTakes(),
diff --git
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestLog.java
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestLog.java
index 879949be..9e0ed60d 100644
---
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestLog.java
+++
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestLog.java
@@ -224,8 +224,12 @@ public class TestLog {
log.replay();
File filler = new File(checkpointDir, "filler");
byte[] buffer = new byte[64 * 1024];
+ // Fill well below the limit:
+ // other processes on a shared CI runner free temp space all the time,
+ // and a thin margin lets the usable space bounce back above the limit
before the put checks it.
+ long margin = 2L * 1024L * 1024L;
FileOutputStream out = new FileOutputStream(filler);
- while (checkpointDir.getUsableSpace() > minimumRequiredSpace) {
+ while (checkpointDir.getUsableSpace() > minimumRequiredSpace - margin)
{
out.write(buffer);
}
out.close();
@@ -419,7 +423,7 @@ public class TestLog {
log.commitPut(transactionID);
log.close();
if (useFastReplay) {
- FileUtils.deleteQuietly(checkpointDir);
+ TestUtils.awaitDeleteDirectory(checkpointDir);
Assert.assertTrue(checkpointDir.mkdir());
}
List<File> logFiles = Lists.newArrayList();
diff --git
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestLogFile.java
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestLogFile.java
index d2e003be..79bca4a7 100644
---
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestLogFile.java
+++
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestLogFile.java
@@ -85,6 +85,8 @@ public class TestLogFile {
@Test
public void testWriterFailsWithDirectory() throws IOException {
+ // Close the writer opened by setup(), so that the file can be deleted
on Windows.
+ logFileWriter.close();
FileUtils.deleteQuietly(dataFile);
Assert.assertFalse(dataFile.exists());
Assert.assertTrue(dataFile.mkdirs());
@@ -245,6 +247,8 @@ public class TestLogFile {
@Test
public void testWriteDelimitedTo() throws IOException {
+ // Close the writer opened by setup(), so that the file can be deleted
on Windows.
+ logFileWriter.close();
if (dataFile.isFile()) {
Assert.assertTrue(dataFile.delete());
}
diff --git
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestUtils.java
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestUtils.java
index 51d62848..2fc4aa93 100644
---
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestUtils.java
+++
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/TestUtils.java
@@ -41,6 +41,7 @@ import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.zip.GZIPInputStream;
+import org.apache.commons.io.FileUtils;
import org.apache.flume.Channel;
import org.apache.flume.Context;
import org.apache.flume.Event;
@@ -52,6 +53,44 @@ import org.junit.Assert;
public class TestUtils {
+ private static final int DELETE_ATTEMPTS = 50;
+ private static final long DELETE_RETRY_DELAY_MILLIS = 100;
+
+ /**
+ * Deletes a file that a discarded backing store may still map.
+ *
+ * <p>On Windows a mapped file cannot be deleted,
+ * and the mapping is only released when the buffer is garbage collected,
+ * so this method retries with garbage collection cycles in between.
+ * The caller must first drop all references to the store and to anything
that references it,
+ * such as a stopped channel,
+ * otherwise the mapping stays reachable and this method fails the test.
+ */
+ public static void awaitDelete(File file) throws InterruptedException {
+ for (int attempt = 0; attempt < DELETE_ATTEMPTS; attempt++) {
+ if (!file.exists() || file.delete()) {
+ return;
+ }
+ System.gc();
+ Thread.sleep(DELETE_RETRY_DELAY_MILLIS);
+ }
+ Assert.fail("Could not delete " + file + ", a mapping is probably
still reachable");
+ }
+
+ /**
+ * Recursively deletes a directory, retrying like {@link
#awaitDelete(File)}.
+ */
+ public static void awaitDeleteDirectory(File dir) throws
InterruptedException {
+ for (int attempt = 0; attempt < DELETE_ATTEMPTS; attempt++) {
+ if (FileUtils.deleteQuietly(dir) && !dir.exists()) {
+ return;
+ }
+ System.gc();
+ Thread.sleep(DELETE_RETRY_DELAY_MILLIS);
+ }
+ Assert.fail("Could not delete " + dir + ", a mapping is probably still
reachable");
+ }
+
public static FlumeEvent newPersistableEvent() {
Map<String, String> headers = Maps.newHashMap();
String timestamp = String.valueOf(System.currentTimeMillis());
diff --git
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/encryption/TestFileChannelEncryption.java
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/encryption/TestFileChannelEncryption.java
index 5c8499d6..d88e7a8f 100644
---
a/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/encryption/TestFileChannelEncryption.java
+++
b/flume-ng-channels/flume-file-channel/src/test/java/org/apache/flume/channel/file/encryption/TestFileChannelEncryption.java
@@ -314,9 +314,11 @@ public class TestFileChannelEncryption extends
TestFileChannelBase {
channel = createFileChannel(overrides);
Assert.fail();
} catch (RuntimeException ex) {
+ // The path separator and the reason text are platform-dependent.
Assert.assertTrue(
"Exception message is incorrect: " + ex.getMessage(),
- ex.getMessage().startsWith("java.io.FileNotFoundException:
/path/does/not/exist "));
+ ex.getMessage()
+ .startsWith("java.io.FileNotFoundException: " +
new File("/path/does/not/exist") + " "));
}
}
@@ -334,9 +336,11 @@ public class TestFileChannelEncryption extends
TestFileChannelBase {
channel = createFileChannel(overrides);
Assert.fail();
} catch (RuntimeException ex) {
+ // The path separator and the reason text are platform-dependent.
Assert.assertTrue(
"Exception message is incorrect: " + ex.getMessage(),
- ex.getMessage().startsWith("java.io.FileNotFoundException:
/path/does/not/exist "));
+ ex.getMessage()
+ .startsWith("java.io.FileNotFoundException: " +
new File("/path/does/not/exist") + " "));
}
}
diff --git
a/flume-ng-configfilters/flume-ng-environment-variable-config-filter/pom.xml
b/flume-ng-configfilters/flume-ng-environment-variable-config-filter/pom.xml
index 9126933b..78c30b0c 100644
--- a/flume-ng-configfilters/flume-ng-environment-variable-config-filter/pom.xml
+++ b/flume-ng-configfilters/flume-ng-environment-variable-config-filter/pom.xml
@@ -52,7 +52,10 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
- <argLine>--add-opens java.base/java.util=ALL-UNNAMED</argLine>
+ <!-- `java.lang` is opened for the `EnvironmentVariables` rule,
+ which needs `ProcessEnvironment.theCaseInsensitiveEnvironment`
on Windows. -->
+ <argLine>--add-opens java.base/java.util=ALL-UNNAMED
+ --add-opens java.base/java.lang=ALL-UNNAMED</argLine>
</configuration>
</plugin>
</plugins>
diff --git
a/flume-ng-configfilters/flume-ng-external-process-config-filter/src/test/java/org/apache/flume/configfilter/TestExternalProcessConfigFilter.java
b/flume-ng-configfilters/flume-ng-external-process-config-filter/src/test/java/org/apache/flume/configfilter/TestExternalProcessConfigFilter.java
index ed59341a..2f3e6253 100644
---
a/flume-ng-configfilters/flume-ng-external-process-config-filter/src/test/java/org/apache/flume/configfilter/TestExternalProcessConfigFilter.java
+++
b/flume-ng-configfilters/flume-ng-external-process-config-filter/src/test/java/org/apache/flume/configfilter/TestExternalProcessConfigFilter.java
@@ -21,6 +21,7 @@ import static org.junit.Assert.assertNull;
import java.io.File;
import java.util.HashMap;
+import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
@@ -34,6 +35,10 @@ public class TestExternalProcessConfigFilter {
@Before
public void setUp() {
+ // The external commands are shell scripts, which Windows cannot
execute.
+ Assume.assumeFalse(
+ "Windows cannot execute the test shell scripts",
+ System.getProperty("os.name").startsWith("Windows"));
configFilter = new ExternalProcessConfigFilter();
}
diff --git a/flume-ng-node/pom.xml b/flume-ng-node/pom.xml
index 0542d117..3f96fb84 100644
--- a/flume-ng-node/pom.xml
+++ b/flume-ng-node/pom.xml
@@ -186,8 +186,11 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
+ <!-- `java.lang` is opened for the `EnvironmentVariables` rule,
+ which needs `ProcessEnvironment.theCaseInsensitiveEnvironment`
on Windows. -->
<argLine>-Djava.net.preferIPv4Stack=true
- --add-opens java.base/java.util=ALL-UNNAMED</argLine>
+ --add-opens java.base/java.util=ALL-UNNAMED
+ --add-opens java.base/java.lang=ALL-UNNAMED</argLine>
</configuration>
</plugin>
diff --git a/flume-ng-sources/flume-syslog-source/pom.xml
b/flume-ng-sources/flume-syslog-source/pom.xml
index f4bf49c1..d169d164 100644
--- a/flume-ng-sources/flume-syslog-source/pom.xml
+++ b/flume-ng-sources/flume-syslog-source/pom.xml
@@ -74,6 +74,12 @@
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.awaitility</groupId>
+ <artifactId>awaitility</artifactId>
+ <scope>test</scope>
+ </dependency>
+
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
diff --git
a/flume-ng-sources/flume-syslog-source/src/test/java/org/apache/flume/source/syslog/TestSyslogTcpSource.java
b/flume-ng-sources/flume-syslog-source/src/test/java/org/apache/flume/source/syslog/TestSyslogTcpSource.java
index cbc6f404..d6cf09a4 100644
---
a/flume-ng-sources/flume-syslog-source/src/test/java/org/apache/flume/source/syslog/TestSyslogTcpSource.java
+++
b/flume-ng-sources/flume-syslog-source/src/test/java/org/apache/flume/source/syslog/TestSyslogTcpSource.java
@@ -16,7 +16,9 @@
*/
package org.apache.flume.source.syslog;
+import static org.awaitility.Awaitility.await;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
@@ -269,17 +271,23 @@ public class TestSyslogTcpSource {
},
null);
SocketFactory socketFactory = sslContext.getSocketFactory();
- Socket socket = socketFactory.createSocket();
- socket.connect(address);
- OutputStream outputStream = socket.getOutputStream();
- outputStream.write(bodyWithTandH.getBytes());
- socket.close();
- // Thread.sleep(100);
+ try (Socket socket = socketFactory.createSocket()) {
+ socket.connect(address);
+ OutputStream outputStream = socket.getOutputStream();
+ outputStream.write(bodyWithTandH.getBytes());
+ outputStream.flush();
+ // Close the socket only after the server has committed the event
to the channel.
+ // The client never reads the TLS 1.3 session tickets sent by the
server,
+ // so an earlier close() aborts the connection with a TCP RST,
+ // which on Windows can discard the data before the server reads
it.
+ await().until(() ->
source.getSourceCounter().getEventAcceptedCount() >= 1);
+ }
Transaction transaction = channel.getTransaction();
transaction.begin();
Event event = channel.take();
- assertEquals(new String(event.getBody()), data1);
+ assertNotNull("The source accepted an event, but the channel did not
deliver one.", event);
+ assertEquals(data1, new String(event.getBody()));
transaction.commit();
transaction.close();
}
diff --git
a/flume-ng-sources/flume-taildir-source/src/test/java/org/apache/flume/source/taildir/TestTaildirEventReader.java
b/flume-ng-sources/flume-taildir-source/src/test/java/org/apache/flume/source/taildir/TestTaildirEventReader.java
index bb198640..54d57380 100644
---
a/flume-ng-sources/flume-taildir-source/src/test/java/org/apache/flume/source/taildir/TestTaildirEventReader.java
+++
b/flume-ng-sources/flume-taildir-source/src/test/java/org/apache/flume/source/taildir/TestTaildirEventReader.java
@@ -31,18 +31,29 @@ import com.google.common.collect.Table;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
+import java.nio.file.FileSystems;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.flume.Event;
import org.junit.After;
+import org.junit.Assume;
import org.junit.Before;
+import org.junit.BeforeClass;
import org.junit.Test;
public class TestTaildirEventReader {
private File tmpDir;
private String posFilePath;
+ @BeforeClass
+ public static void requireUnixFileAttributeView() {
+ // ReliableTaildirEventReader tracks files by inode via the `unix:ino`
file attribute
+ Assume.assumeTrue(
+ "The default file system does not provide a `unix` file
attribute view",
+
FileSystems.getDefault().supportedFileAttributeViews().contains("unix"));
+ }
+
public static String bodyAsString(Event event) {
return new String(event.getBody());
}
diff --git
a/flume-ng-sources/flume-taildir-source/src/test/java/org/apache/flume/source/taildir/TestTaildirSource.java
b/flume-ng-sources/flume-taildir-source/src/test/java/org/apache/flume/source/taildir/TestTaildirSource.java
index 2991bfb7..4c8ec6a6 100644
---
a/flume-ng-sources/flume-taildir-source/src/test/java/org/apache/flume/source/taildir/TestTaildirSource.java
+++
b/flume-ng-sources/flume-taildir-source/src/test/java/org/apache/flume/source/taildir/TestTaildirSource.java
@@ -39,6 +39,7 @@ import com.google.common.collect.Lists;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
+import java.nio.file.FileSystems;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -58,7 +59,9 @@ import org.apache.flume.lifecycle.LifecycleController;
import org.apache.flume.lifecycle.LifecycleState;
import org.apache.flume.util.Whitebox;
import org.junit.After;
+import org.junit.Assume;
import org.junit.Before;
+import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.Mockito;
@@ -68,6 +71,14 @@ public class TestTaildirSource {
private File tmpDir;
private String posFilePath;
+ @BeforeClass
+ public static void requireUnixFileAttributeView() {
+ // TaildirSource tracks files by inode via the `unix:ino` file
attribute
+ Assume.assumeTrue(
+ "The default file system does not provide a `unix` file
attribute view",
+
FileSystems.getDefault().supportedFileAttributeViews().contains("unix"));
+ }
+
@Before
public void setUp() {
source = new TaildirSource();
diff --git a/flume-parent/pom.xml b/flume-parent/pom.xml
index 56a3f80b..00c1969a 100644
--- a/flume-parent/pom.xml
+++ b/flume-parent/pom.xml
@@ -95,6 +95,7 @@
<test.include.pattern>**/Test*.java</test.include.pattern>
<redirectTestOutput>true</redirectTestOutput>
+ <awaitility.version>4.3.0</awaitility.version>
<curator.version>5.9.0</curator.version>
<fest-reflect.version>1.4.1</fest-reflect.version>
<hadoop.version>3.5.0</hadoop.version>
@@ -118,6 +119,13 @@
<scope>import</scope>
</dependency>
+ <dependency>
+ <groupId>org.awaitility</groupId>
+ <artifactId>awaitility</artifactId>
+ <version>${awaitility.version}</version>
+ <scope>test</scope>
+ </dependency>
+
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
diff --git
a/flume-tools/src/test/java/org/apache/flume/tools/TestFileChannelIntegrityTool.java
b/flume-tools/src/test/java/org/apache/flume/tools/TestFileChannelIntegrityTool.java
index b829a55d..91da39c0 100644
---
a/flume-tools/src/test/java/org/apache/flume/tools/TestFileChannelIntegrityTool.java
+++
b/flume-tools/src/test/java/org/apache/flume/tools/TestFileChannelIntegrityTool.java
@@ -85,14 +85,32 @@ public class TestFileChannelIntegrityTool {
@After
public void tearDown() throws Exception {
- FileUtils.deleteDirectory(checkpointDir);
- FileUtils.deleteDirectory(dataDir);
+ deleteDirectoryWithRetries(checkpointDir);
+ deleteDirectoryWithRetries(dataDir);
}
@AfterClass
public static void tearDownClass() throws Exception {
- FileUtils.deleteDirectory(origCheckpointDir);
- FileUtils.deleteDirectory(origDataDir);
+ deleteDirectoryWithRetries(origCheckpointDir);
+ deleteDirectoryWithRetries(origDataDir);
+ }
+
+ /**
+ * Deletes a directory, retrying with garbage collection cycles in between.
+ *
+ * <p>On Windows the checkpoint file of a stopped channel cannot be deleted
+ * until the garbage collector releases its mapping.
+ * A failed deletion would leak files into the next test's directories.
+ */
+ private static void deleteDirectoryWithRetries(File dir) throws
InterruptedException {
+ for (int attempt = 0; attempt < 50; attempt++) {
+ if (FileUtils.deleteQuietly(dir) && !dir.exists()) {
+ return;
+ }
+ System.gc();
+ Thread.sleep(100);
+ }
+ Assert.fail("Could not delete " + dir + ", a mapping is probably still
reachable");
}
@Test
diff --git a/src/site/antora/modules/ROOT/pages/FlumeUserGuide.adoc
b/src/site/antora/modules/ROOT/pages/FlumeUserGuide.adoc
index 72d2428c..397c8027 100644
--- a/src/site/antora/modules/ROOT/pages/FlumeUserGuide.adoc
+++ b/src/site/antora/modules/ROOT/pages/FlumeUserGuide.adoc
@@ -1680,7 +1680,10 @@ This deserializer reads a Binary Large Object (BLOB) per
event, typically one BL
[NOTE]
====
-*This source is provided as a preview feature. It does not work on Windows.*
+*This source is provided as a preview feature.*
+
+This source tracks files by their inode number and therefore requires a file
system that provides the `unix` file attribute view.
+In particular, it does not work on Windows.
====
Watch the specified files, and tail them in nearly real-time once detected new
lines appended to the each files.