diff --git a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/util/FormatUtils.java b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/util/FormatUtils.java index 5fda79cdc3f4..6acbe601ee36 100644 --- a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/util/FormatUtils.java +++ b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/util/FormatUtils.java @@ -20,6 +20,7 @@ import org.apache.nifi.time.DurationFormat; import java.text.NumberFormat; +import java.time.Duration; import java.time.Instant; import java.time.LocalDateTime; import java.time.OffsetDateTime; @@ -28,8 +29,12 @@ import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalQueries; +import java.time.temporal.UnsupportedTemporalTypeException; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; @@ -298,4 +303,54 @@ public static Instant parseToInstant(final DateTimeFormatter formatter, final St final OffsetDateTime offsetDateTime = OffsetDateTime.of(localDateTime, zoneOffset); return offsetDateTime.toInstant(); } + + /** + * Format a value of ChronoUnit into a word representation. + * Does not handle estimated ChronoUnit values. + * For anything greater than {@link ChronoUnit#DAYS}, use {@link #formatDurationToWords(Duration)}. + * + * @param value the number of the given {@link ChronoUnit} to measure + * @param unit {@link ChronoUnit} that the value represents + * @return String representation of the given value and unit + * @throws UnsupportedTemporalTypeException if the unit is not supported ({@link ChronoUnit#isDurationEstimated()} == true) + */ + public static String formatDurationToWords(final long value, final ChronoUnit unit) throws UnsupportedTemporalTypeException { + return formatDurationToWords(Duration.ZERO.plus(value, unit)); + } + + /** + * Format a Duration using words (days, hours, minutes, seconds, ns) where all lower units are + * included once a non-zero unit is found. Unit plurality is preserved. + * Maximum resolution is in terms of days. + * + * @param source duration to convert to words + * @return String representation of the given duration + */ + public static String formatDurationToWords(final Duration source) { + final long days = source.toDaysPart(); + final long hours = source.toHoursPart(); + final long minutes = source.toMinutesPart(); + final int seconds = source.toSecondsPart(); + final int nanos = source.toNanosPart(); + + final List parts = new ArrayList<>(); + + if (days > 0) { + parts.add(days + (days == 1 ? " day" : " days")); + } + if (hours > 0 || !parts.isEmpty()) { + parts.add(hours + (hours == 1 ? " hour" : " hours")); + } + if (minutes > 0 || !parts.isEmpty()) { + parts.add(minutes + (minutes == 1 ? " minute" : " minutes")); + } + if (seconds > 0 || !parts.isEmpty()) { + parts.add(seconds + (seconds == 1 ? " second" : " seconds")); + } + if (nanos > 0 || !parts.isEmpty()) { + parts.add(nanos + " ns"); + } + + return String.join(" ", parts); + } } diff --git a/nifi-commons/nifi-utils/src/test/java/org/apache/nifi/util/TestFormatUtils.java b/nifi-commons/nifi-utils/src/test/java/org/apache/nifi/util/TestFormatUtils.java index b2c1dba37d2c..cd8076fad8ac 100644 --- a/nifi-commons/nifi-utils/src/test/java/org/apache/nifi/util/TestFormatUtils.java +++ b/nifi-commons/nifi-utils/src/test/java/org/apache/nifi/util/TestFormatUtils.java @@ -21,6 +21,7 @@ import org.junit.jupiter.params.provider.MethodSource; import java.text.DecimalFormatSymbols; +import java.time.Duration; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; @@ -194,4 +195,26 @@ private static Stream getFormatTime() { + TimeUnit.MILLISECONDS.convert(60, TimeUnit.SECONDS) + TimeUnit.MILLISECONDS.convert(1001, TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS, "1000:01:01.001")); } + + @ParameterizedTest + @MethodSource("getDurationValues") + public void testFormatDurationToWords(Duration duration, String expected) { + assertEquals(expected, FormatUtils.formatDurationToWords(duration)); + } + + private static Stream getDurationValues() { + return Stream.of( + Arguments.of(Duration.parse("PT0.000000001S"), "1 ns"), + Arguments.of(Duration.parse("PT0.000000002S"), "2 ns"), + Arguments.of(Duration.parse("PT1S"), "1 second 0 ns"), + Arguments.of(Duration.parse("PT2S"), "2 seconds 0 ns"), + Arguments.of(Duration.parse("PT1M"), "1 minute 0 seconds 0 ns"), + Arguments.of(Duration.parse("PT2M"), "2 minutes 0 seconds 0 ns"), + Arguments.of(Duration.parse("PT1H"), "1 hour 0 minutes 0 seconds 0 ns"), + Arguments.of(Duration.parse("PT2H"), "2 hours 0 minutes 0 seconds 0 ns"), + Arguments.of(Duration.parse("P1D"), "1 day 0 hours 0 minutes 0 seconds 0 ns"), + Arguments.of(Duration.parse("P35D"), "35 days 0 hours 0 minutes 0 seconds 0 ns"), + Arguments.of(Duration.parse("P366D"), "366 days 0 hours 0 minutes 0 seconds 0 ns") + ); + } } diff --git a/nifi-framework-bundle/nifi-framework-extensions/nifi-provenance-repository-bundle/nifi-persistent-provenance-repository/src/main/java/org/apache/nifi/provenance/store/EventStorePartition.java b/nifi-framework-bundle/nifi-framework-extensions/nifi-provenance-repository-bundle/nifi-persistent-provenance-repository/src/main/java/org/apache/nifi/provenance/store/EventStorePartition.java index 7e4967ebec7f..5e02f9933ac0 100644 --- a/nifi-framework-bundle/nifi-framework-extensions/nifi-provenance-repository-bundle/nifi-persistent-provenance-repository/src/main/java/org/apache/nifi/provenance/store/EventStorePartition.java +++ b/nifi-framework-bundle/nifi-framework-extensions/nifi-provenance-repository-bundle/nifi-persistent-provenance-repository/src/main/java/org/apache/nifi/provenance/store/EventStorePartition.java @@ -23,9 +23,9 @@ import java.io.Closeable; import java.io.IOException; +import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Optional; -import java.util.concurrent.TimeUnit; public interface EventStorePartition extends Closeable { /** @@ -102,7 +102,7 @@ public interface EventStorePartition extends Closeable { * @param olderThan the amount of time for which any event older than this should be removed * @param timeUnit the unit of time that applies to the first argument */ - void purgeOldEvents(long olderThan, TimeUnit timeUnit); + void purgeOldEvents(long olderThan, ChronoUnit timeUnit); /** * Purges some number of events from the partition. The oldest events will be purged. diff --git a/nifi-framework-bundle/nifi-framework-extensions/nifi-provenance-repository-bundle/nifi-persistent-provenance-repository/src/main/java/org/apache/nifi/provenance/store/PartitionedEventStore.java b/nifi-framework-bundle/nifi-framework-extensions/nifi-provenance-repository-bundle/nifi-persistent-provenance-repository/src/main/java/org/apache/nifi/provenance/store/PartitionedEventStore.java index 7d329f5b042c..295dc2c1e900 100644 --- a/nifi-framework-bundle/nifi-framework-extensions/nifi-provenance-repository-bundle/nifi-persistent-provenance-repository/src/main/java/org/apache/nifi/provenance/store/PartitionedEventStore.java +++ b/nifi-framework-bundle/nifi-framework-extensions/nifi-provenance-repository-bundle/nifi-persistent-provenance-repository/src/main/java/org/apache/nifi/provenance/store/PartitionedEventStore.java @@ -32,6 +32,7 @@ import java.io.File; import java.io.IOException; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -243,7 +244,7 @@ void performMaintenance() { final long maxFileLife = repoConfig.getMaxRecordLife(TimeUnit.MILLISECONDS); for (final EventStorePartition partition : getPartitions()) { try { - partition.purgeOldEvents(maxFileLife, TimeUnit.MILLISECONDS); + partition.purgeOldEvents(maxFileLife, ChronoUnit.MILLIS); } catch (final Exception e) { logger.error("Failed to purge expired events from {}", partition, e); eventReporter.reportEvent(Severity.WARNING, EVENT_CATEGORY, diff --git a/nifi-framework-bundle/nifi-framework-extensions/nifi-provenance-repository-bundle/nifi-persistent-provenance-repository/src/main/java/org/apache/nifi/provenance/store/WriteAheadStorePartition.java b/nifi-framework-bundle/nifi-framework-extensions/nifi-provenance-repository-bundle/nifi-persistent-provenance-repository/src/main/java/org/apache/nifi/provenance/store/WriteAheadStorePartition.java index 1eec9de83f2a..688ba9da577b 100644 --- a/nifi-framework-bundle/nifi-framework-extensions/nifi-provenance-repository-bundle/nifi-persistent-provenance-repository/src/main/java/org/apache/nifi/provenance/store/WriteAheadStorePartition.java +++ b/nifi-framework-bundle/nifi-framework-extensions/nifi-provenance-repository-bundle/nifi-persistent-provenance-repository/src/main/java/org/apache/nifi/provenance/store/WriteAheadStorePartition.java @@ -41,6 +41,8 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Files; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -490,18 +492,22 @@ private Optional getPathForEventId(final long id) { } @Override - public void purgeOldEvents(final long olderThan, final TimeUnit unit) { - final long timeCutoff = System.currentTimeMillis() - unit.toMillis(olderThan); - + public void purgeOldEvents(final long olderThan, final ChronoUnit timeUnit) { + // Use ZDT to allow the system to handle a ChronoUnit that is otherwise "estimated" + final long timeCutoff = ZonedDateTime.now() + .minus(olderThan, timeUnit) + .toInstant().toEpochMilli(); final List removed = getEventFilesFromDisk().filter(file -> file.lastModified() < timeCutoff) .sorted(DirectoryUtils.SMALLEST_ID_FIRST) .filter(this::delete) .collect(Collectors.toList()); + String thresholdWords = FormatUtils.formatDurationToWords(olderThan, timeUnit); + if (removed.isEmpty()) { - logger.debug("No Provenance Event files that exceed time-based threshold of {} {}", olderThan, unit); + logger.debug("No Provenance Event files that exceed time-based threshold of {}", thresholdWords); } else { - logger.info("Purged {} Provenance Event files from Provenance Repository because the events were older than {} {}: {}", removed.size(), olderThan, unit, removed); + logger.info("Purged {} Provenance Event files from Provenance Repository because the events were older than {} : {}", removed.size(), thresholdWords, removed); } }