This is an automated email from the ASF dual-hosted git repository.

exceptionfactory pushed a commit to branch support/nifi-1.x
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/support/nifi-1.x by this push:
     new 63fe620d93 NIFI-13896 Improved TailFile performance (#9424)
63fe620d93 is described below

commit 63fe620d93ca856bd92e2ab74e50d3500c30813c
Author: Lehel Boér <[email protected]>
AuthorDate: Sat Oct 26 11:47:16 2024 -0500

    NIFI-13896 Improved TailFile performance (#9424)
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../apache/nifi/processors/standard/TailFile.java  | 198 ++++++++++-----------
 1 file changed, 99 insertions(+), 99 deletions(-)

diff --git 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/TailFile.java
 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/TailFile.java
index f1163ca522..c9993313e9 100644
--- 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/TailFile.java
+++ 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/TailFile.java
@@ -412,11 +412,9 @@ public class TailFile extends AbstractProcessor {
         final Scope scope = getStateScope(context);
         final StateMap stateMap = context.getStateManager().getState(scope);
 
-        final String startPosition = 
context.getProperty(START_POSITION).getValue();
-
         if (stateMap.getVersion() == -1L || stateMap.toMap().isEmpty()) {
             //state has been cleared or never stored so recover as 'empty 
state'
-            initStates(filesToTail, Collections.emptyMap(), true, 
startPosition);
+            initStates(filesToTail, Collections.emptyMap(), true);
             recoverState(context, filesToTail, Collections.emptyMap());
             return;
         }
@@ -424,14 +422,14 @@ public class TailFile extends AbstractProcessor {
         Map<String, String> statesMap = stateMap.toMap();
 
         if (statesMap.containsKey(TailFileState.StateKeys.FILENAME)
-                && !statesMap.keySet().stream().anyMatch(key -> 
key.startsWith(MAP_PREFIX))) {
+                && statesMap.keySet().stream().noneMatch(key -> 
key.startsWith(MAP_PREFIX))) {
             // If statesMap contains "filename" key without "file.0." prefix,
             // and there's no key with "file." prefix, then
             // it indicates that the statesMap is created with earlier version 
of NiFi.
             // In this case, we need to migrate the state by adding prefix 
indexed with 0.
             final Map<String, String> migratedStatesMap = new 
HashMap<>(statesMap.size());
-            for (String key : statesMap.keySet()) {
-                migratedStatesMap.put(MAP_PREFIX + "0." + key, 
statesMap.get(key));
+            for (Entry<String, String> entry : statesMap.entrySet()) {
+                migratedStatesMap.put(MAP_PREFIX + "0." + entry.getKey(), 
entry.getValue());
             }
 
             // LENGTH is added from NiFi 1.1.0. Set the value with using the 
last position so that we can use existing state
@@ -442,11 +440,44 @@ public class TailFile extends AbstractProcessor {
             getLogger().info("statesMap has been migrated. {}", 
migratedStatesMap);
         }
 
-        initStates(filesToTail, statesMap, false, startPosition);
+        initStates(filesToTail, statesMap, false);
         recoverState(context, filesToTail, statesMap);
+        removeLegacyStateEntries(context, statesMap, scope);
     }
 
-    private void initStates(final List<String> filesToTail, final Map<String, 
String> statesMap, final boolean isCleared, final String startPosition) {
+    /**
+     * Removes legacy state entries from versions prior to NiFi 1.0, where 
state keys were not indexed
+     * with file-specific prefixes. In NiFi 1.0 and older versions, tailing 
multiple files wasn't supported,
+     * so there was no need to associate state information with individual 
files.
+     * Newer versions use "file.[index]." prefixes to handle multiple tailed 
files,
+     * which makes the legacy state keys ('checksum', 'filename', 'position', 
and 'timestamp') obsolete.
+     * This method filters out these legacy keys and persists the updated 
state map without them.
+     *
+     * @param context the ProcessContext for accessing the state manager
+     * @param statesMap the current state map containing both legacy and new 
entries
+     * @param scope the scope (cluster or local) for state persistence
+     */
+    private void removeLegacyStateEntries(final ProcessContext context, final 
Map<String, String> statesMap, Scope scope) {
+        Map<String, String> updatedStatesMap = new HashMap<>();
+        for (Entry<String, String> entry : statesMap.entrySet()) {
+            final String key = entry.getKey();
+            if (TailFileState.StateKeys.CHECKSUM.equals(key)
+                    || TailFileState.StateKeys.FILENAME.equals(key)
+                    || TailFileState.StateKeys.POSITION.equals(key)
+                    || TailFileState.StateKeys.TIMESTAMP.equals(key)) {
+                getLogger().info("Removed state {}={} stored by older version 
of NiFi.", key, entry.getValue());
+                continue;
+            }
+            updatedStatesMap.put(key, entry.getValue());
+        }
+        try {
+            context.getStateManager().setState(updatedStatesMap, scope);
+        } catch (IOException e) {
+            getLogger().warn("Failed to store state due to {}; some data may 
be duplicated on restart of NiFi", e);
+        }
+    }
+
+    private void initStates(final List<String> filesToTail, final Map<String, 
String> statesMap, final boolean isCleared) {
         int fileIndex = 0;
 
         if (isCleared) {
@@ -456,30 +487,34 @@ public class TailFile extends AbstractProcessor {
             // case 'states' object is empty but the statesMap is not. So we 
have to
             // put back the files we already know about in 'states' object 
before
             // doing the recovery
-            if( states.isEmpty() && !statesMap.isEmpty()) {
-                for (String key : statesMap.keySet()) {
-                    if (key.endsWith(TailFileState.StateKeys.FILENAME) && 
filesToTail.contains(statesMap.get(key))) {
+            if(states.isEmpty() && !statesMap.isEmpty()) {
+                for (Entry<String, String> entry : statesMap.entrySet()) {
+                    final String key = entry.getKey();
+                    final String value = entry.getValue();
+                    if (key.endsWith(TailFileState.StateKeys.FILENAME) && 
filesToTail.contains(value)) {
                         int index = Integer.parseInt(key.split("\\.")[1]);
-                        states.put(statesMap.get(key), new 
TailFileObject(index, statesMap, preAllocatedBufferSize));
+                        states.put(value, new TailFileObject(index, statesMap, 
preAllocatedBufferSize));
                     }
                 }
             }
 
             // first, we remove the files that are no longer present
-            final List<String> toBeRemoved = new ArrayList<String>();
-            for (String file : states.keySet()) {
-                if(!filesToTail.contains(file)) {
-                    toBeRemoved.add(file);
-                    cleanReader(states.get(file));
+            final List<String> toBeRemoved = new ArrayList<>();
+            for (Entry<String, TailFileObject> entry : states.entrySet()) {
+                final String filePath = entry.getKey();
+                final TailFileObject tailFileObject = entry.getValue();
+                if(!filesToTail.contains(filePath)) {
+                    toBeRemoved.add(filePath);
+                    cleanReader(tailFileObject);
                 }
             }
-            states.keySet().removeAll(toBeRemoved);
+            toBeRemoved.forEach(states.keySet()::remove);
 
             // then we need to get the highest ID used so far to be sure
             // we don't mix different files in case we add new files to tail
-            for (String file : states.keySet()) {
-                if (fileIndex <= states.get(file).getFilenameIndex()) {
-                    fileIndex = states.get(file).getFilenameIndex() + 1;
+            for (TailFileObject tfo : states.values()) {
+                if (fileIndex <= tfo.getFilenameIndex()) {
+                    fileIndex = tfo.getFilenameIndex() + 1;
                 }
             }
 
@@ -497,7 +532,7 @@ public class TailFile extends AbstractProcessor {
 
     private void recoverState(final ProcessContext context, final List<String> 
filesToTail, final Map<String, String> map) throws IOException {
         for (String file : filesToTail) {
-            recoverState(context, map, file);
+            recoverState(map, file);
         }
     }
 
@@ -546,7 +581,6 @@ public class TailFile extends AbstractProcessor {
      * checksum, so that we are ready to proceed with the
      * {@link #onTrigger(ProcessContext, ProcessSession)} call.
      *
-     * @param context the ProcessContext
      * @param stateValues the values that were recovered from state that was
      * previously stored. This Map should be populated with the keys defined in
      * {@link TailFileState.StateKeys}.
@@ -554,23 +588,15 @@ public class TailFile extends AbstractProcessor {
      * @throws IOException if unable to seek to the appropriate location in the
      * tailed file.
      */
-    private void recoverState(final ProcessContext context, final Map<String, 
String> stateValues, final String filePath) throws IOException {
-
-        final String prefix = MAP_PREFIX + 
states.get(filePath).getFilenameIndex() + '.';
-
-        if (!stateValues.containsKey(prefix + 
TailFileState.StateKeys.FILENAME)) {
-            resetState(filePath);
-            return;
-        }
-        if (!stateValues.containsKey(prefix + 
TailFileState.StateKeys.POSITION)) {
-            resetState(filePath);
-            return;
-        }
-        if (!stateValues.containsKey(prefix + 
TailFileState.StateKeys.TIMESTAMP)) {
-            resetState(filePath);
-            return;
-        }
-        if (!stateValues.containsKey(prefix + TailFileState.StateKeys.LENGTH)) 
{
+    private void recoverState(final Map<String, String> stateValues, final 
String filePath) throws IOException {
+        final TailFileObject tailFileObject = states.get(filePath);
+        final String prefix = MAP_PREFIX + tailFileObject.getFilenameIndex() + 
'.';
+
+        // Combine all key checks into a single condition
+        if (!stateValues.containsKey(prefix + TailFileState.StateKeys.FILENAME)
+                || !stateValues.containsKey(prefix + 
TailFileState.StateKeys.POSITION)
+                || !stateValues.containsKey(prefix + 
TailFileState.StateKeys.TIMESTAMP)
+                || !stateValues.containsKey(prefix + 
TailFileState.StateKeys.LENGTH)) {
             resetState(filePath);
             return;
         }
@@ -586,7 +612,7 @@ public class TailFile extends AbstractProcessor {
         File tailFile = null;
 
         if (checksumPresent && filePath.equals(storedStateFilename)) {
-            
states.get(filePath).setExpectedRecoveryChecksum(Long.parseLong(checksumValue));
+            
tailFileObject.setExpectedRecoveryChecksum(Long.parseLong(checksumValue));
 
             // We have an expected checksum and the currently configured 
filename is the same as the state file.
             // We need to check if the existing file is the same as the one 
referred to in the state file based on
@@ -594,11 +620,11 @@ public class TailFile extends AbstractProcessor {
             final Checksum checksum = new CRC32();
             final File existingTailFile = new File(storedStateFilename);
             if (existingTailFile.length() >= position) {
-                try (final InputStream tailFileIs = new 
FileInputStream(existingTailFile);
-                        final CheckedInputStream in = new 
CheckedInputStream(tailFileIs, checksum)) {
+                try (final InputStream tailFileIs = 
Files.newInputStream(existingTailFile.toPath());
+                     final CheckedInputStream in = new 
CheckedInputStream(tailFileIs, checksum)) {
 
                     try {
-                        StreamUtils.copy(in, new NullOutputStream(), 
states.get(filePath).getState().getPosition());
+                        StreamUtils.copy(in, new NullOutputStream(), 
tailFileObject.getState().getPosition());
                     } catch (final EOFException eof) {
                         // If we hit EOFException, then the file is smaller 
than we expected. Assume rollover.
                         getLogger().debug("When recovering state, file being 
tailed has less data than was stored in the state. "
@@ -606,7 +632,7 @@ public class TailFile extends AbstractProcessor {
                     }
 
                     final long checksumResult = in.getChecksum().getValue();
-                    if (checksumResult == 
states.get(filePath).getExpectedRecoveryChecksum()) {
+                    if (checksumResult == 
tailFileObject.getExpectedRecoveryChecksum()) {
                         // Checksums match. This means that we want to resume 
reading from where we left off.
                         // So we will populate the reader object so that it 
will be used in onTrigger. If the
                         // checksums do not match, then we will leave the 
reader object null, so that the next
@@ -629,17 +655,18 @@ public class TailFile extends AbstractProcessor {
                         + "this indicates that the file has rotated. Will 
begin tailing current file from beginning.", existingTailFile.length(), 
position);
             }
 
-            states.get(filePath).setState(new TailFileState(filePath, 
tailFile, reader, position, timestamp, length, checksum, 
ByteBuffer.allocate(preAllocatedBufferSize)));
+            tailFileObject.setState(new TailFileState(filePath, tailFile, 
reader, position, timestamp, length, checksum, 
ByteBuffer.allocate(preAllocatedBufferSize)));
         } else {
             resetState(filePath);
         }
 
-        getLogger().debug("Recovered state {}", 
states.get(filePath).getState());
+        getLogger().debug("Recovered state {}", tailFileObject.getState());
     }
 
     private void resetState(final String filePath) {
-        states.get(filePath).setExpectedRecoveryChecksum(null);
-        states.get(filePath).setState(new TailFileState(filePath, null, null, 
0L, 0L, 0L, null, ByteBuffer.allocate(preAllocatedBufferSize)));
+        final TailFileObject tailFileObject = states.get(filePath);
+        tailFileObject.setExpectedRecoveryChecksum(null);
+        tailFileObject.setState(new TailFileState(filePath, null, null, 0L, 
0L, 0L, null, ByteBuffer.allocate(preAllocatedBufferSize)));
     }
 
     @OnStopped
@@ -675,14 +702,15 @@ public class TailFile extends AbstractProcessor {
 
     @Override
     public void onTrigger(final ProcessContext context, final ProcessSession 
session) throws ProcessException {
-        if(isMultiChanging.get()) {
+        if (isMultiChanging.get()) {
             long timeSinceLastLookup = new Date().getTime() - lastLookup.get();
-            if(timeSinceLastLookup > 
context.getProperty(LOOKUP_FREQUENCY).asTimePeriod(TimeUnit.MILLISECONDS)) {
+            long lookupFrequency = 
context.getProperty(LOOKUP_FREQUENCY).asTimePeriod(TimeUnit.MILLISECONDS);
+            if (timeSinceLastLookup > lookupFrequency) {
                 try {
                     final List<String> filesToTail = lookup(context);
                     final Scope scope = getStateScope(context);
                     final StateMap stateMap = session.getState(scope);
-                    initStates(filesToTail, stateMap.toMap(), false, 
context.getProperty(START_POSITION).getValue());
+                    initStates(filesToTail, stateMap.toMap(), false);
                 } catch (IOException e) {
                     getLogger().error("Exception raised while attempting to 
recover state about where the tailing last left off", e);
                     context.yield();
@@ -712,7 +740,7 @@ public class TailFile extends AbstractProcessor {
             try {
                 processTailFile(context, session, tailFile);
             } catch (NulCharacterEncounteredException e) {
-                getLogger().warn("NUL character encountered in " + tailFile + 
" and '" + REREAD_ON_NUL.getDisplayName() + "' is set to 'true', yielding.");
+                getLogger().warn("NUL character encountered in {} and '{}' is 
set to 'true', yielding.", tailFile, REREAD_ON_NUL.getDisplayName());
                 context.yield();
                 return;
             }
@@ -731,27 +759,26 @@ public class TailFile extends AbstractProcessor {
             StateMap sessionStateMap = session.getState(scope);
             Map<String, String> sessionStates = new 
HashMap<>(sessionStateMap.toMap());
             List<String> keysToRemove = collectKeysToBeRemoved(sessionStates);
-            sessionStates.keySet().removeAll(keysToRemove);
+            keysToRemove.forEach(sessionStates.keySet()::remove);
             getLogger().debug("Removed {} references to nonexistent files from 
session's state map",
                     keysToRemove.size());
             session.setState(sessionStates, scope);
         } catch (IOException e) {
             getLogger().error("Exception raised while attempting to cleanup 
session's state map", e);
             context.yield();
-            return;
         }
     }
 
     private List<String> collectKeysToBeRemoved(Map<String, String> 
sessionStates) {
         List<String> keysToRemove = new ArrayList<>();
         List<String> filesToRemove = sessionStates.entrySet().stream()
-                .filter(entry -> entry.getKey().endsWith("filename")
-                        && !states.keySet().contains(entry.getValue()))
+                .filter(entry -> entry.getKey().endsWith(StateKeys.FILENAME)
+                        && !states.containsKey(entry.getValue()))
                 .map(Entry::getKey)
                 .collect(toList());
 
         for (String key : filesToRemove) {
-            final String prefix = StringUtils.substringBefore(key, "filename");
+            final String prefix = StringUtils.substringBefore(key, 
StateKeys.FILENAME);
             keysToRemove.add(prefix + StateKeys.FILENAME);
             keysToRemove.add(prefix + StateKeys.LENGTH);
             keysToRemove.add(prefix + StateKeys.POSITION);
@@ -790,7 +817,7 @@ public class TailFile extends AbstractProcessor {
                     final long position = file.length();
                     final long timestamp = file.lastModified() + 1;
 
-                    try (final InputStream fis = new FileInputStream(file);
+                    try (final InputStream fis = 
Files.newInputStream(file.toPath());
                             final CheckedInputStream in = new 
CheckedInputStream(fis, checksum)) {
                         StreamUtils.copy(in, new NullOutputStream(), position);
                     }
@@ -799,7 +826,7 @@ public class TailFile extends AbstractProcessor {
                     cleanup(context);
                     tfo.setState(new TailFileState(filename, file, 
fileChannel, position, timestamp, file.length(), checksum, 
tfo.getState().getBuffer()));
                 } catch (final IOException ioe) {
-                    getLogger().error("Attempted to position Reader at current 
position in file {} but failed to do so due to {}", file, ioe.toString(), ioe);
+                    getLogger().error("Attempted to position Reader at current 
position in file {} but failed to do so", file, ioe);
                     context.yield();
                     return;
                 }
@@ -1179,33 +1206,19 @@ public class TailFile extends AbstractProcessor {
                 final File file = path.toFile();
                 final long lastMod = file.lastModified();
 
-                if (file.lastModified() < minTimestamp) {
+                if (lastMod >= minTimestamp && !file.equals(tailFile)) {
+                    rolledOffFiles.add(file);
+                } else {
                     getLogger().debug("Found rolled off file {} but its last 
modified timestamp is before the cutoff (Last Mod = {}, Cutoff = {}) so will 
not consume it",
                             file, lastMod, minTimestamp);
-
-                    continue;
-                } else if (file.equals(tailFile)) {
-                    continue;
                 }
-
-                rolledOffFiles.add(file);
             }
         }
 
         // Sort files based on last modified timestamp. If same timestamp, use 
filename as a secondary sort, as often
         // files that are rolled over are given a naming scheme that is 
lexicographically sort in the same order as the
         // timestamp, such as yyyy-MM-dd-HH-mm-ss
-        rolledOffFiles.sort(new Comparator<File>() {
-            @Override
-            public int compare(final File o1, final File o2) {
-                final int lastModifiedComp = Long.compare(o1.lastModified(), 
o2.lastModified());
-                if (lastModifiedComp != 0) {
-                    return lastModifiedComp;
-                }
-
-                return o1.getName().compareTo(o2.getName());
-            }
-        });
+        
rolledOffFiles.sort(Comparator.comparingLong(File::lastModified).thenComparing(File::getName));
 
         return rolledOffFiles;
     }
@@ -1227,21 +1240,8 @@ public class TailFile extends AbstractProcessor {
         try {
             final Scope scope = getStateScope(context);
             final StateMap oldState = session == null ? 
context.getStateManager().getState(scope) : session.getState(scope);
-            Map<String, String> updatedState = new HashMap<>();
-
-            for(String key : oldState.toMap().keySet()) {
-                // These states are stored by older version of NiFi, and won't 
be used anymore.
-                // New states have 'file.<index>.' prefix.
-                if (TailFileState.StateKeys.CHECKSUM.equals(key)
-                        || TailFileState.StateKeys.FILENAME.equals(key)
-                        || TailFileState.StateKeys.POSITION.equals(key)
-                        || TailFileState.StateKeys.TIMESTAMP.equals(key)) {
-                    getLogger().info("Removed state {}={} stored by older 
version of NiFi.", key, oldState.get(key));
-                    continue;
-                }
-                updatedState.put(key, oldState.get(key));
-            }
 
+            Map<String, String> updatedState = new HashMap<>(oldState.toMap());
             updatedState.putAll(state);
 
             if (session == null) {
@@ -1250,7 +1250,7 @@ public class TailFile extends AbstractProcessor {
                 session.setState(updatedState, scope);
             }
         } catch (final IOException e) {
-            getLogger().warn("Failed to store state; some data may be 
duplicated on restart of NiFi", e);
+            getLogger().warn("Failed to store state due to {}; some data may 
be duplicated on restart of NiFi", e);
         }
     }
 
@@ -1274,7 +1274,7 @@ public class TailFile extends AbstractProcessor {
             try {
                 reader.close();
                 getLogger().debug("Closed FileChannel {}", reader);
-            } catch (final IOException ioe2) {
+            } catch (final IOException ignored) {
             }
 
             return null;
@@ -1402,7 +1402,7 @@ public class TailFile extends AbstractProcessor {
                 final long millisSinceModified = getCurrentTimeMs() - 
newestFile.lastModified();
                 if (millisSinceModified < postRolloverTailMillis) {
                     getLogger().debug("Rolled over file {} (size={}, 
lastModified={}) was modified {} millis ago, which isn't long enough to consume 
file fully without taking line endings into " +
-                        "account. Will do nothing will file for now.", 
newestFile, newestFile.length(), newestFile.lastModified(), 
millisSinceModified);
+                        "account. Will do nothing for now.", newestFile, 
newestFile.length(), newestFile.lastModified(), millisSinceModified);
                     return true;
                 }
 
@@ -1541,7 +1541,7 @@ public class TailFile extends AbstractProcessor {
     private TailFileState consumeFileFully(final File file, final 
ProcessContext context, final ProcessSession session, TailFileObject tfo) 
throws IOException {
         FlowFile flowFile = session.create();
 
-        try (final InputStream fis = new FileInputStream(file)) {
+        try (final InputStream fis = Files.newInputStream(file.toPath())) {
             flowFile = session.write(flowFile, out -> {
                 flushLinesBuffer(out, new CRC32());
                 StreamUtils.copy(fis, out);
@@ -1579,7 +1579,7 @@ public class TailFile extends AbstractProcessor {
 
         private TailFileState state;
         private Long expectedRecoveryChecksum;
-        private int filenameIndex;
+        private final int filenameIndex;
         private boolean tailFileChanged = true;
 
         public TailFileObject(final int index, final TailFileState fileState) {
@@ -1739,8 +1739,8 @@ public class TailFile extends AbstractProcessor {
         }
 
         @Override
-        public Throwable fillInStackTrace() {
+        public synchronized Throwable fillInStackTrace() {
             return this;
         }
     }
-}
+}
\ No newline at end of file

Reply via email to