Skip to content

Commit 42a86bd

Browse files
[IO-279] Add Tailer ignoreTouch option (#757)
* [io-279] added ignoreOnTouch option to Tailer so that user can now choose to ignore touching of watched file * [io-279] fixing checkstyle errors * Add Javadoc since tag for new public method --------- Co-authored-by: Gary Gregory <[email protected]>
1 parent 0db0fa6 commit 42a86bd

File tree

2 files changed

+111
-9
lines changed

2 files changed

+111
-9
lines changed

src/main/java/org/apache/commons/io/input/Tailer.java

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@
138138
* VFS</a>.
139139
*/
140140
public class Tailer implements Runnable, AutoCloseable {
141+
private static final boolean DEFAULT_IGNORE_TOUCH = false;
141142

142143
// @formatter:off
143144
/**
@@ -186,6 +187,7 @@ private static Thread newDaemonThread(final Runnable runnable) {
186187
private boolean tailFromEnd;
187188
private boolean reOpen;
188189
private boolean startThread = true;
190+
private boolean ignoreTouch = DEFAULT_IGNORE_TOUCH;
189191
private ExecutorService executorService = Executors.newSingleThreadExecutor(Builder::newDaemonThread);
190192

191193
/**
@@ -216,7 +218,7 @@ public Builder() {
216218
*/
217219
@Override
218220
public Tailer get() {
219-
final Tailer tailer = new Tailer(tailable, getCharset(), tailerListener, delayDuration, tailFromEnd, reOpen, getBufferSize());
221+
final Tailer tailer = new Tailer(tailable, getCharset(), tailerListener, delayDuration, tailFromEnd, reOpen, getBufferSize(), ignoreTouch);
220222
if (startThread) {
221223
executorService.submit(tailer);
222224
}
@@ -310,6 +312,23 @@ public Builder setTailFromEnd(final boolean end) {
310312
this.tailFromEnd = end;
311313
return this;
312314
}
315+
316+
/**
317+
* Sets ignoreTouch behaviour
318+
*
319+
* @param ignoreTouch This can be useful when your watched file gets touched (meaning it gets more recent timestamps
320+
* without changing the file) for some reason or when you are working on file systems where timestamp
321+
* is updated before content.
322+
* The default behaviour (ignoreTouch=false) would then reissue the whole current file, while
323+
* ignoreTouch=true does nothing in that case.
324+
*
325+
* @return {@code this} instance.
326+
* @since 2.20.0
327+
*/
328+
public Builder setIgnoreTouch(final boolean ignoreTouch) {
329+
this.ignoreTouch = ignoreTouch;
330+
return this;
331+
}
313332
}
314333

315334
/**
@@ -695,6 +714,8 @@ public static Tailer create(final File file, final TailerListener listener, fina
695714
*/
696715
private volatile boolean run = true;
697716

717+
private boolean ignoreTouch = DEFAULT_IGNORE_TOUCH;
718+
698719
/**
699720
* Creates a Tailer for the given file, with a specified buffer size.
700721
*
@@ -710,7 +731,7 @@ public static Tailer create(final File file, final TailerListener listener, fina
710731
@Deprecated
711732
public Tailer(final File file, final Charset charset, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen,
712733
final int bufSize) {
713-
this(new TailablePath(file.toPath()), charset, listener, Duration.ofMillis(delayMillis), end, reOpen, bufSize);
734+
this(new TailablePath(file.toPath()), charset, listener, Duration.ofMillis(delayMillis), end, reOpen, bufSize, DEFAULT_IGNORE_TOUCH);
714735
}
715736

716737
/**
@@ -807,10 +828,11 @@ public Tailer(final File file, final TailerListener listener, final long delayMi
807828
* @param delayDuration the delay between checks of the file for new content in milliseconds.
808829
* @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
809830
* @param reOpen if true, close and reopen the file between reading chunks
831+
* @param ignoreTouch if true, file timestamp changes without content change get ignored
810832
* @param bufferSize Buffer size
811833
*/
812834
private Tailer(final Tailable tailable, final Charset charset, final TailerListener listener, final Duration delayDuration, final boolean end,
813-
final boolean reOpen, final int bufferSize) {
835+
final boolean reOpen, final int bufferSize, final boolean ignoreTouch) {
814836
this.tailable = Objects.requireNonNull(tailable, "tailable");
815837
this.listener = Objects.requireNonNull(listener, "listener");
816838
this.delayDuration = delayDuration;
@@ -821,6 +843,7 @@ private Tailer(final Tailable tailable, final Charset charset, final TailerListe
821843
listener.init(this);
822844
this.reOpen = reOpen;
823845
this.charset = charset;
846+
this.ignoreTouch = ignoreTouch;
824847
}
825848

826849
/**
@@ -996,14 +1019,22 @@ public void run() {
9961019
last = tailable.lastModifiedFileTime();
9971020
} else if (newer) {
9981021
/*
999-
* This can happen if the file is truncated or overwritten with the exact same length of information. In cases like
1000-
* this, the file position needs to be reset
1022+
* This can happen if the file
1023+
* - is overwritten with the exact same length of information
1024+
* - gets "touched"
1025+
* - Files.getLastModifiedTime returns a new timestamp but newer data is not yet there (
1026+
* was reported to happen on busy systems or samba network shares, see IO-279)
1027+
* The default behaviour is to replay the whole file. If this is unsdesired in your usecase,
1028+
* use the ignoreTouch builder flag
10011029
*/
1002-
position = 0;
1003-
reader.seek(position); // cannot be null here
1030+
if (!ignoreTouch) {
1031+
position = 0;
1032+
reader.seek(position); // cannot be null here
10041033

1005-
// Now we can read new lines
1006-
position = readLines(reader);
1034+
// Now we can read new lines
1035+
position = readLines(reader);
1036+
}
1037+
// we eitherway continue with the new timestamp
10071038
last = tailable.lastModifiedFileTime();
10081039
}
10091040
if (reOpen && reader != null) {

src/test/java/org/apache/commons/io/input/TailerTest.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.nio.file.Files;
3838
import java.nio.file.StandardOpenOption;
3939
import java.nio.file.attribute.FileTime;
40+
import java.time.Duration;
4041
import java.util.ArrayList;
4142
import java.util.Arrays;
4243
import java.util.Collections;
@@ -576,6 +577,76 @@ void testStopWithNoFileUsingExecutor() throws Exception {
576577
assertEquals(0, listener.reachedEndOfFile, "end of file never reached");
577578
}
578579

580+
@Test
581+
void testTailerIgnoreTouch() throws Exception {
582+
// Create & start the Tailer
583+
final long delayMillis = 50;
584+
final File file = new File(temporaryFolder, "tailer1-testIgnoreTouch.txt");
585+
createFile(file, 0);
586+
final TestTailerListener listener = new TestTailerListener();
587+
try (Tailer tailer = Tailer.builder()
588+
.setFile(file)
589+
.setTailerListener(listener)
590+
.setDelayDuration(Duration.ofMillis(delayMillis))
591+
.setStartThread(false)
592+
.setIgnoreTouch(true)
593+
.get()) {
594+
final Thread thread = new Thread(tailer);
595+
thread.start();
596+
597+
// Write some lines to the file
598+
write(file, "Line one");
599+
final long testDelayMillis = delayMillis * 10;
600+
TestUtils.sleep(testDelayMillis);
601+
List<String> lines = listener.getLines();
602+
assertEquals(1, lines.size(), "1 line count");
603+
assertEquals("Line one", lines.get(0), "1 line 1");
604+
listener.clear();
605+
606+
// touch the file
607+
file.setLastModified(System.currentTimeMillis());
608+
TestUtils.sleep(testDelayMillis);
609+
lines = listener.getLines();
610+
assertEquals(0, lines.size(), "nothing should have changed by touching");
611+
}
612+
}
613+
614+
@Test
615+
void testTailerReissueOnTouch() throws Exception {
616+
// Create & start the Tailer
617+
final long delayMillis = 50;
618+
final File file = new File(temporaryFolder, "tailer1-testReissueOnTouch.txt");
619+
createFile(file, 0);
620+
final TestTailerListener listener = new TestTailerListener();
621+
try (Tailer tailer = Tailer.builder()
622+
.setFile(file)
623+
.setTailerListener(listener)
624+
.setDelayDuration(Duration.ofMillis(delayMillis))
625+
.setStartThread(false)
626+
.setIgnoreTouch(false)
627+
.get()) {
628+
final Thread thread = new Thread(tailer);
629+
thread.start();
630+
631+
// Write some lines to the file
632+
write(file, "Line one");
633+
final long testDelayMillis = delayMillis * 10;
634+
TestUtils.sleep(testDelayMillis);
635+
List<String> lines = listener.getLines();
636+
assertEquals(1, lines.size(), "1 line count");
637+
assertEquals("Line one", lines.get(0), "1 line 1");
638+
listener.clear();
639+
640+
// touch the file
641+
file.setLastModified(System.currentTimeMillis());
642+
TestUtils.sleep(testDelayMillis);
643+
lines = listener.getLines();
644+
assertEquals(1, lines.size(), "1 line count");
645+
assertEquals("Line one", lines.get(0), "1 line 1");
646+
listener.clear();
647+
}
648+
}
649+
579650
@Test
580651
void testTailer() throws Exception {
581652

0 commit comments

Comments
 (0)