From 6ea2ff9a69cd992574aaaa3f9eeb678ec180e92a Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 12 Nov 2020 11:41:59 +0100 Subject: [PATCH 1/3] Use human readable durations instead of milliseconds --- .../mvnd/client/DaemonClientConnection.java | 2 +- .../mvnd/client/DaemonParameters.java | 10 +- .../mvndaemon/mvnd/common/Environment.java | 18 +- .../org/mvndaemon/mvnd/common/TimeUtils.java | 244 ++++++++++++++++++ .../mvnd/daemon/DaemonExpiration.java | 29 +-- .../org/mvndaemon/mvnd/daemon/Server.java | 9 +- .../mvnd/junit/MvndTestExtension.java | 5 +- .../mvndaemon/mvnd/junit/TestParameters.java | 8 +- 8 files changed, 284 insertions(+), 41 deletions(-) create mode 100644 common/src/main/java/org/mvndaemon/mvnd/common/TimeUtils.java diff --git a/client/src/main/java/org/mvndaemon/mvnd/client/DaemonClientConnection.java b/client/src/main/java/org/mvndaemon/mvnd/client/DaemonClientConnection.java index b3e357dc5..30ceb9609 100644 --- a/client/src/main/java/org/mvndaemon/mvnd/client/DaemonClientConnection.java +++ b/client/src/main/java/org/mvndaemon/mvnd/client/DaemonClientConnection.java @@ -94,7 +94,7 @@ public void dispatch(Message message) throws DaemonException.ConnectException { } public List receive() throws ConnectException, StaleAddressException { - int maxKeepAliveMs = parameters.keepAliveMs() * parameters.maxLostKeepAlive(); + long maxKeepAliveMs = parameters.keepAlive().toMillis() * parameters.maxLostKeepAlive(); while (true) { try { final Message m = queue.poll(maxKeepAliveMs, TimeUnit.MILLISECONDS); diff --git a/client/src/main/java/org/mvndaemon/mvnd/client/DaemonParameters.java b/client/src/main/java/org/mvndaemon/mvnd/client/DaemonParameters.java index 2dae6196f..db2335380 100644 --- a/client/src/main/java/org/mvndaemon/mvnd/client/DaemonParameters.java +++ b/client/src/main/java/org/mvndaemon/mvnd/client/DaemonParameters.java @@ -21,6 +21,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -41,6 +42,7 @@ import org.mvndaemon.mvnd.common.BuildProperties; import org.mvndaemon.mvnd.common.Environment; import org.mvndaemon.mvnd.common.Os; +import org.mvndaemon.mvnd.common.TimeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -268,8 +270,8 @@ public DaemonParameters cd(Path newUserDir) { .put(Environment.USER_DIR, newUserDir)); } - public int keepAliveMs() { - return property(Environment.DAEMON_KEEP_ALIVE_MS).orFail().asInt(); + public Duration keepAlive() { + return property(Environment.DAEMON_KEEP_ALIVE).orFail().asDuration(); } public int maxLostKeepAlive() { @@ -579,5 +581,9 @@ public int asInt(IntUnaryOperator function) { return function.applyAsInt(asInt()); } + public Duration asDuration() { + return TimeUtils.toDuration(get()); + } + } } diff --git a/common/src/main/java/org/mvndaemon/mvnd/common/Environment.java b/common/src/main/java/org/mvndaemon/mvnd/common/Environment.java index c23d8efe0..b406b5ead 100644 --- a/common/src/main/java/org/mvndaemon/mvnd/common/Environment.java +++ b/common/src/main/java/org/mvndaemon/mvnd/common/Environment.java @@ -17,11 +17,11 @@ import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Duration; import java.util.Collection; import java.util.Locale; import java.util.Objects; import java.util.Properties; -import java.util.concurrent.TimeUnit; /** * Collects system properties and environment variables used by mvnd client or server. @@ -74,8 +74,8 @@ public String asCommandLineProperty(String value) { DAEMON_REGISTRY("daemon.registry", null, null, false), MVND_NO_DAEMON("mvnd.noDaemon", "MVND_NO_DAEMON", "false", true), DAEMON_DEBUG("daemon.debug", null, false, true), - DAEMON_IDLE_TIMEOUT_MS("daemon.idleTimeoutMs", null, TimeUnit.HOURS.toMillis(3), true), - DAEMON_KEEP_ALIVE_MS("daemon.keepAliveMs", null, TimeUnit.SECONDS.toMillis(1), true), + DAEMON_IDLE_TIMEOUT("daemon.idleTimeout", null, "3 hours", true), + DAEMON_KEEP_ALIVE("daemon.keepAlive", null, "1 sec", true), DAEMON_MAX_LOST_KEEP_ALIVE("daemon.maxLostKeepAlive", null, 3, false), /** * The minimum number of threads to use when constructing the default {@code -T} parameter for the daemon. @@ -146,17 +146,13 @@ public String asCommandLineProperty(String value) { /** * Interval to check if the daemon should expire */ - DAEMON_EXPIRATION_CHECK_DELAY_MS("daemon.expirationCheckDelayMs", null, TimeUnit.SECONDS.toMillis(10), true), + DAEMON_EXPIRATION_CHECK_DELAY("daemon.expirationCheckDelay", null, "10 seconds", true), /** * Period after which idle daemons will shut down */ - DAEMON_DUPLICATE_DAEMON_GRACE_PERIOD_MS("daemon.duplicateDaemonGracePeriodMs", null, TimeUnit.SECONDS.toMillis(10), true), + DAEMON_DUPLICATE_DAEMON_GRACE_PERIOD("daemon.duplicateDaemonGracePeriod", null, "10 seconds", true), ; - public static final int DEFAULT_IDLE_TIMEOUT = (int) TimeUnit.HOURS.toMillis(3); - - public static final int DEFAULT_KEEP_ALIVE = (int) TimeUnit.SECONDS.toMillis(1); - static Properties properties = System.getProperties(); public static void setProperties(Properties properties) { @@ -219,6 +215,10 @@ public Path asPath() { return Paths.get(result); } + public Duration asDuration() { + return TimeUtils.toDuration(asString()); + } + public String asCommandLineProperty(String value) { return "-D" + property + "=" + value; } diff --git a/common/src/main/java/org/mvndaemon/mvnd/common/TimeUtils.java b/common/src/main/java/org/mvndaemon/mvnd/common/TimeUtils.java new file mode 100644 index 000000000..da72775d9 --- /dev/null +++ b/common/src/main/java/org/mvndaemon/mvnd/common/TimeUtils.java @@ -0,0 +1,244 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mvndaemon.mvnd.common; + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.time.Duration; +import java.util.Locale; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Time utils. + * + * Origin file: + * https://github.com/apache/camel/blob/4ea9e6c357371682b855d2d79655b41120331b7a/core/camel-util/src/main/java/org/apache/camel/util/TimeUtils.java + */ +public final class TimeUtils { + + private static final Pattern NUMBERS_ONLY_STRING_PATTERN = Pattern.compile("^[-]?(\\d)+$", Pattern.CASE_INSENSITIVE); + private static final Pattern WEEK_REGEX_PATTERN = Pattern.compile("((\\d)*(\\d))\\s*w(eek(s)?)?(?=\\b|\\d|$)", + Pattern.CASE_INSENSITIVE); + private static final Pattern DAY_REGEX_PATTERN = Pattern.compile("((\\d)*(\\d))\\s*d(ay(s)?)?(?=\\b|\\d|$)", + Pattern.CASE_INSENSITIVE); + private static final Pattern HOUR_REGEX_PATTERN = Pattern.compile("((\\d)*(\\d))\\s*h(our(s)?)?(?=\\b|\\d|$)", + Pattern.CASE_INSENSITIVE); + private static final Pattern MINUTES_REGEX_PATTERN = Pattern.compile("((\\d)*(\\d))\\s*m(in(ute(s)?)?)?(?=\\b|\\d|$)", + Pattern.CASE_INSENSITIVE); + private static final Pattern SECONDS_REGEX_PATTERN = Pattern + .compile("((\\d)(\\d)*)(\\.(\\d+))?\\s*s(ec(ond)?(s)?)?(?=\\b|\\d|$)", Pattern.CASE_INSENSITIVE); + private static final Pattern MILLIS_REGEX_PATTERN = Pattern + .compile("((\\d)(\\d)*)(\\.(\\d+))?\\s*m(illi)?s(ec(ond)?(s)?)?(?=\\b|\\d|$)", Pattern.CASE_INSENSITIVE); + + private TimeUtils() { + } + + public static boolean isPositive(Duration dur) { + return dur.getSeconds() > 0 || dur.getNano() != 0; + } + + public static String printDuration(Duration uptime) { + return printDuration(uptime.toMillis()); + } + + /** + * Prints the duration in a human readable format as X days Y hours Z minutes etc. + * + * @param uptime the uptime in millis + * @return the time used for displaying on screen or in logs + */ + public static String printDuration(double uptime) { + // Code taken from Karaf + // https://svn.apache.org/repos/asf/karaf/trunk/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/InfoAction.java + + NumberFormat fmtI = new DecimalFormat("###,###", new DecimalFormatSymbols(Locale.ENGLISH)); + NumberFormat fmtD = new DecimalFormat("###,##0.000", new DecimalFormatSymbols(Locale.ENGLISH)); + + uptime /= 1000; + if (uptime < 60) { + return fmtD.format(uptime) + " seconds"; + } + uptime /= 60; + if (uptime < 60) { + long minutes = (long) uptime; + String s = fmtI.format(minutes) + (minutes > 1 ? " minutes" : " minute"); + return s; + } + uptime /= 60; + if (uptime < 24) { + long hours = (long) uptime; + long minutes = (long) ((uptime - hours) * 60); + String s = fmtI.format(hours) + (hours > 1 ? " hours" : " hour"); + if (minutes != 0) { + s += " " + fmtI.format(minutes) + (minutes > 1 ? " minutes" : " minute"); + } + return s; + } + uptime /= 24; + long days = (long) uptime; + long hours = (long) ((uptime - days) * 24); + String s = fmtI.format(days) + (days > 1 ? " days" : " day"); + if (hours != 0) { + s += " " + fmtI.format(hours) + (hours > 1 ? " hours" : " hour"); + } + return s; + } + + public static Duration toDuration(String source) throws IllegalArgumentException { + return Duration.ofMillis(toMilliSeconds(source)); + } + + public static long toMilliSeconds(String source) throws IllegalArgumentException { + // quick conversion if its only digits + boolean digit = true; + for (int i = 0; i < source.length(); i++) { + char ch = source.charAt(i); + // special for first as it can be negative number + if (i == 0 && ch == '-') { + continue; + } + // quick check if its 0..9 + if (ch < '0' || ch > '9') { + digit = false; + break; + } + } + if (digit) { + return Long.parseLong(source); + } + + long milliseconds = 0; + boolean foundFlag = false; + + checkCorrectnessOfPattern(source); + Matcher matcher; + + matcher = createMatcher(NUMBERS_ONLY_STRING_PATTERN, source); + if (matcher.find()) { + // Note: This will also be used for regular numeric strings. + // This String -> long converter will be used for all strings. + milliseconds = Long.parseLong(source); + } else { + matcher = createMatcher(WEEK_REGEX_PATTERN, source); + if (matcher.find()) { + milliseconds += 7 * TimeUnit.DAYS.toMillis(Long.parseLong(matcher.group(1))); + foundFlag = true; + } + + matcher = createMatcher(DAY_REGEX_PATTERN, source); + if (matcher.find()) { + milliseconds += TimeUnit.DAYS.toMillis(Long.parseLong(matcher.group(1))); + foundFlag = true; + } + + matcher = createMatcher(HOUR_REGEX_PATTERN, source); + if (matcher.find()) { + milliseconds += TimeUnit.HOURS.toMillis(Long.parseLong(matcher.group(1))); + foundFlag = true; + } + + matcher = createMatcher(MINUTES_REGEX_PATTERN, source); + if (matcher.find()) { + milliseconds += TimeUnit.MINUTES.toMillis(Long.parseLong(matcher.group(1))); + foundFlag = true; + } + + matcher = createMatcher(SECONDS_REGEX_PATTERN, source); + if (matcher.find()) { + milliseconds += TimeUnit.SECONDS.toMillis(Long.parseLong(matcher.group(1))); + if (matcher.group(5) != null && !matcher.group(5).isEmpty()) { + milliseconds += TimeUnit.MILLISECONDS.toMillis(Long.parseLong(matcher.group(5))); + } + foundFlag = true; + } + + matcher = createMatcher(MILLIS_REGEX_PATTERN, source); + if (matcher.find()) { + milliseconds += TimeUnit.MILLISECONDS.toMillis(Long.parseLong(matcher.group(1))); + foundFlag = true; + } + + // No pattern matched... initiating fallback check and conversion (if required). + // The source at this point may contain illegal values or special characters + if (!foundFlag) { + milliseconds = Long.parseLong(source); + } + } + + return milliseconds; + } + + private static void checkCorrectnessOfPattern(String source) { + //replace only numbers once + Matcher matcher = createMatcher(NUMBERS_ONLY_STRING_PATTERN, source); + String replaceSource = matcher.replaceFirst(""); + + //replace week string once + matcher = createMatcher(WEEK_REGEX_PATTERN, replaceSource); + if (matcher.find() && matcher.find()) { + throw new IllegalArgumentException("Weeks should not be specified more then once: " + source); + } + replaceSource = matcher.replaceFirst(""); + + //replace day string once + matcher = createMatcher(DAY_REGEX_PATTERN, replaceSource); + if (matcher.find() && matcher.find()) { + throw new IllegalArgumentException("Days should not be specified more then once: " + source); + } + replaceSource = matcher.replaceFirst(""); + + //replace hour string once + matcher = createMatcher(HOUR_REGEX_PATTERN, replaceSource); + if (matcher.find() && matcher.find()) { + throw new IllegalArgumentException("Hours should not be specified more then once: " + source); + } + replaceSource = matcher.replaceFirst(""); + + //replace minutes once + matcher = createMatcher(MINUTES_REGEX_PATTERN, replaceSource); + if (matcher.find() && matcher.find()) { + throw new IllegalArgumentException("Minutes should not be specified more then once: " + source); + } + replaceSource = matcher.replaceFirst(""); + + //replace seconds once + matcher = createMatcher(SECONDS_REGEX_PATTERN, replaceSource); + if (matcher.find() && matcher.find()) { + throw new IllegalArgumentException("Seconds should not be specified more then once: " + source); + } + replaceSource = matcher.replaceFirst(""); + + //replace millis once + matcher = createMatcher(MILLIS_REGEX_PATTERN, replaceSource); + if (matcher.find() && matcher.find()) { + throw new IllegalArgumentException("Milliseconds should not be specified more then once: " + source); + } + replaceSource = matcher.replaceFirst(""); + + if (replaceSource.length() > 0) { + throw new IllegalArgumentException("Illegal characters: " + source); + } + } + + private static Matcher createMatcher(Pattern pattern, String source) { + return pattern.matcher(source); + } + +} diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/daemon/DaemonExpiration.java b/daemon/src/main/java/org/mvndaemon/mvnd/daemon/DaemonExpiration.java index bbbda8a2b..d91d59a05 100644 --- a/daemon/src/main/java/org/mvndaemon/mvnd/daemon/DaemonExpiration.java +++ b/daemon/src/main/java/org/mvndaemon/mvnd/daemon/DaemonExpiration.java @@ -17,6 +17,8 @@ import java.nio.file.Files; import java.nio.file.Paths; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -28,6 +30,7 @@ import org.mvndaemon.mvnd.common.DaemonInfo; import org.mvndaemon.mvnd.common.DaemonState; import org.mvndaemon.mvnd.common.Environment; +import org.mvndaemon.mvnd.common.TimeUtils; import static org.mvndaemon.mvnd.common.DaemonExpirationStatus.DO_NOT_EXPIRE; import static org.mvndaemon.mvnd.common.DaemonExpirationStatus.GRACEFUL_EXPIRE; @@ -41,8 +44,6 @@ */ public class DaemonExpiration { - public static final int DUPLICATE_DAEMON_GRACE_PERIOD_MS = 10000; - public interface DaemonExpirationStrategy { DaemonExpirationResult checkExpiration(Server daemon); @@ -53,7 +54,7 @@ public static DaemonExpirationStrategy master() { return any( any(gcTrashing(), lowHeapSpace(), lowNonHeap()), all(compatible(), duplicateGracePeriod(), notMostRecentlyUsed()), - idleTimeout(Environment.DAEMON_IDLE_TIMEOUT_MS.asInt()), + idleTimeout(Environment.DAEMON_IDLE_TIMEOUT.asDuration()), all(duplicateGracePeriod(), notMostRecentlyUsed(), lowMemory(0.05)), registryUnavailable()); } @@ -82,26 +83,14 @@ static DaemonExpirationStrategy lowMemory(double minFreeMemoryPercentage) { } static DaemonExpirationStrategy duplicateGracePeriod() { - return idleTimeout(Environment.DAEMON_DUPLICATE_DAEMON_GRACE_PERIOD_MS.asInt()); + return idleTimeout(Environment.DAEMON_DUPLICATE_DAEMON_GRACE_PERIOD.asDuration()); } - private static final long HOUR = 60 * 60 * 1000; - private static final long MINUTE = 60 * 1000; - private static final long SECOND = 1000; - - static DaemonExpirationStrategy idleTimeout(long timeout) { + static DaemonExpirationStrategy idleTimeout(Duration timeout) { return daemon -> { - long idl = System.currentTimeMillis() - daemon.getLastIdle(); - if (daemon.getState() == DaemonState.Idle && idl > timeout) { - String str; - if (idl > HOUR) { - str = (idl / HOUR) + " hours"; - } else if (idl > MINUTE) { - str = (idl / MINUTE) + " minutes"; - } else { - str = (idl / SECOND) + " seconds"; - } - return new DaemonExpirationResult(QUIET_EXPIRE, "after being idle for " + str); + Duration idl = Duration.between(Instant.ofEpochMilli(daemon.getLastIdle()), Instant.now()); + if (daemon.getState() == DaemonState.Idle && idl.compareTo(timeout) > 0) { + return new DaemonExpirationResult(QUIET_EXPIRE, "after being idle for " + TimeUtils.printDuration(idl)); } else { return NOT_TRIGGERED; } diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/daemon/Server.java b/daemon/src/main/java/org/mvndaemon/mvnd/daemon/Server.java index c3b1d506d..1703e19e0 100644 --- a/daemon/src/main/java/org/mvndaemon/mvnd/daemon/Server.java +++ b/daemon/src/main/java/org/mvndaemon/mvnd/daemon/Server.java @@ -21,6 +21,7 @@ import java.net.InetSocketAddress; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -165,9 +166,9 @@ public void clearCache(String clazzName, String fieldName) { public void run() { try { - int expirationCheckDelayMs = Environment.DAEMON_EXPIRATION_CHECK_DELAY_MS.asInt(); + Duration expirationCheckDelay = Environment.DAEMON_EXPIRATION_CHECK_DELAY.asDuration(); executor.scheduleAtFixedRate(this::expirationCheck, - expirationCheckDelayMs, expirationCheckDelayMs, TimeUnit.MILLISECONDS); + expirationCheckDelay.toMillis(), expirationCheckDelay.toMillis(), TimeUnit.MILLISECONDS); LOGGER.info("Daemon started"); if (noDaemon) { try (SocketChannel socket = this.socket.accept()) { @@ -405,7 +406,7 @@ private void cancelNow() { private void handle(DaemonConnection connection, BuildRequest buildRequest) { updateState(Busy); try { - int keepAlive = Environment.DAEMON_KEEP_ALIVE_MS.asInt(); + Duration keepAlive = Environment.DAEMON_KEEP_ALIVE.asDuration(); LOGGER.info("Executing request"); @@ -421,7 +422,7 @@ private void handle(DaemonConnection connection, BuildRequest buildRequest) { while (true) { Message m; if (flushed) { - m = sendQueue.poll(keepAlive, TimeUnit.MILLISECONDS); + m = sendQueue.poll(keepAlive.toMillis(), TimeUnit.MILLISECONDS); if (m == null) { m = Message.KEEP_ALIVE_SINGLETON; } diff --git a/integration-tests/src/test/java/org/mvndaemon/mvnd/junit/MvndTestExtension.java b/integration-tests/src/test/java/org/mvndaemon/mvnd/junit/MvndTestExtension.java index 7d44db20d..ed9134864 100644 --- a/integration-tests/src/test/java/org/mvndaemon/mvnd/junit/MvndTestExtension.java +++ b/integration-tests/src/test/java/org/mvndaemon/mvnd/junit/MvndTestExtension.java @@ -35,6 +35,7 @@ import org.mvndaemon.mvnd.client.DefaultClient; import org.mvndaemon.mvnd.common.DaemonRegistry; import org.mvndaemon.mvnd.common.Environment; +import org.mvndaemon.mvnd.common.TimeUtils; import static org.mvndaemon.mvnd.junit.TestUtils.deleteDir; @@ -205,8 +206,8 @@ public static MvndResource create(String className, String rawProjectDir, boolea Paths.get(System.getProperty("java.home")).toAbsolutePath().normalize(), localMavenRepository, settingsPath, logback, - Environment.DEFAULT_IDLE_TIMEOUT, - Environment.DEFAULT_KEEP_ALIVE, + TimeUtils.toDuration(Environment.DAEMON_IDLE_TIMEOUT.getDef()), + TimeUtils.toDuration(Environment.DAEMON_KEEP_ALIVE.getDef()), Integer.parseInt(Environment.DAEMON_MAX_LOST_KEEP_ALIVE.getDef())); final TestRegistry registry = new TestRegistry(parameters.registry()); diff --git a/integration-tests/src/test/java/org/mvndaemon/mvnd/junit/TestParameters.java b/integration-tests/src/test/java/org/mvndaemon/mvnd/junit/TestParameters.java index a42247b5b..0f48c5357 100644 --- a/integration-tests/src/test/java/org/mvndaemon/mvnd/junit/TestParameters.java +++ b/integration-tests/src/test/java/org/mvndaemon/mvnd/junit/TestParameters.java @@ -16,8 +16,10 @@ package org.mvndaemon.mvnd.junit; import java.nio.file.Path; +import java.time.Duration; import org.mvndaemon.mvnd.client.DaemonParameters; import org.mvndaemon.mvnd.common.Environment; +import org.mvndaemon.mvnd.common.TimeUtils; public class TestParameters extends DaemonParameters { static final int TEST_MIN_THREADS = 2; @@ -26,7 +28,7 @@ public class TestParameters extends DaemonParameters { public TestParameters(Path testDir, Path mvndPropertiesPath, Path mavenHome, Path userHome, Path userDir, Path multiModuleProjectDirectory, Path javaHome, Path localMavenRepository, Path settings, Path logbackConfigurationPath, - int idleTimeout, int keepAlive, int maxLostKeepAlive) { + Duration idleTimeout, Duration keepAlive, int maxLostKeepAlive) { super(new PropertiesBuilder().put(Environment.MVND_PROPERTIES_PATH, mvndPropertiesPath) .put(Environment.MVND_HOME, mavenHome) .put(Environment.USER_HOME, userHome) @@ -36,8 +38,8 @@ public TestParameters(Path testDir, Path mvndPropertiesPath, Path mavenHome, Pat .put(Environment.MAVEN_REPO_LOCAL, localMavenRepository) .put(Environment.MAVEN_SETTINGS, settings) .put(Environment.LOGBACK_CONFIGURATION_FILE, logbackConfigurationPath) - .put(Environment.DAEMON_IDLE_TIMEOUT_MS, idleTimeout) - .put(Environment.DAEMON_KEEP_ALIVE_MS, keepAlive) + .put(Environment.DAEMON_IDLE_TIMEOUT, TimeUtils.printDuration(idleTimeout)) + .put(Environment.DAEMON_KEEP_ALIVE, TimeUtils.printDuration(keepAlive)) .put(Environment.DAEMON_MAX_LOST_KEEP_ALIVE, maxLostKeepAlive) .put(Environment.MVND_MIN_THREADS, TEST_MIN_THREADS)); this.testDir = testDir; From 3f1ecac68e8181a7b0f10de8280e60f819ff5006 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 12 Nov 2020 15:00:57 +0100 Subject: [PATCH 2/3] Provide an automatic purge of daemon logs, fixes #196 --- .../mvnd/client/DaemonParameters.java | 10 +- .../mvndaemon/mvnd/client/DefaultClient.java | 131 ++++++++++++++++-- .../mvndaemon/mvnd/common/Environment.java | 4 + 3 files changed, 129 insertions(+), 16 deletions(-) diff --git a/client/src/main/java/org/mvndaemon/mvnd/client/DaemonParameters.java b/client/src/main/java/org/mvndaemon/mvnd/client/DaemonParameters.java index db2335380..e4a363911 100644 --- a/client/src/main/java/org/mvndaemon/mvnd/client/DaemonParameters.java +++ b/client/src/main/java/org/mvndaemon/mvnd/client/DaemonParameters.java @@ -51,6 +51,8 @@ */ public class DaemonParameters { + public static final String LOG_EXTENSION = ".log"; + private static final Logger LOG = LoggerFactory.getLogger(DaemonParameters.class); private static final String EXT_CLASS_PATH = "maven.ext.class.path"; private static final String EXTENSIONS_FILENAME = ".mvn/extensions.xml"; @@ -184,11 +186,11 @@ public Path registry() { } public Path daemonLog(String daemon) { - return daemonStorage().resolve("daemon-" + daemon + ".log"); + return daemonStorage().resolve("daemon-" + daemon + LOG_EXTENSION); } public Path daemonOutLog(String daemon) { - return daemonStorage().resolve("daemon-" + daemon + ".out.log"); + return daemonStorage().resolve("daemon-" + daemon + ".out" + LOG_EXTENSION); } public Path multiModuleProjectDirectory() { @@ -286,6 +288,10 @@ public int rollingWindowSize() { return property(Environment.MVND_ROLLING_WINDOW_SIZE).orFail().asInt(); } + public Duration purgeLogPeriod() { + return property(Environment.MVND_LOG_PURGE_PERIOD).orFail().asDuration(); + } + public static String findDefaultMultimoduleProjectDirectory(Path pwd) { Path dir = pwd; do { diff --git a/client/src/main/java/org/mvndaemon/mvnd/client/DefaultClient.java b/client/src/main/java/org/mvndaemon/mvnd/client/DefaultClient.java index 05f0b3738..7981f16ff 100644 --- a/client/src/main/java/org/mvndaemon/mvnd/client/DefaultClient.java +++ b/client/src/main/java/org/mvndaemon/mvnd/client/DefaultClient.java @@ -15,13 +15,22 @@ */ package org.mvndaemon.mvnd.client; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileTime; +import java.time.Duration; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import org.fusesource.jansi.Ansi; import org.jline.utils.AttributedString; import org.jline.utils.AttributedStyle; @@ -33,11 +42,14 @@ import org.mvndaemon.mvnd.common.Message; import org.mvndaemon.mvnd.common.Message.BuildException; import org.mvndaemon.mvnd.common.OsUtils; +import org.mvndaemon.mvnd.common.TimeUtils; import org.mvndaemon.mvnd.common.logging.ClientOutput; import org.mvndaemon.mvnd.common.logging.TerminalOutput; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.mvndaemon.mvnd.client.DaemonParameters.LOG_EXTENSION; + public class DefaultClient implements Client { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultClient.class); @@ -183,6 +195,12 @@ public ExecutionResult execute(ClientOutput output, List argv) { } return new DefaultResult(argv, null); } + boolean purge = args.remove("--purge"); + if (purge) { + String result = purgeLogs(); + output.accept(Message.display(result != null ? result : "Nothing to purge")); + return new DefaultResult(argv, null); + } if (args.stream().noneMatch(arg -> arg.startsWith("-T") || arg.equals("--threads"))) { args.add("--threads"); @@ -216,27 +234,112 @@ public ExecutionResult execute(ClientOutput output, List argv) { output.accept(Message.buildStatus("Build request sent")); - while (true) { - final List messages = daemon.receive(); - output.accept(messages); - for (Message m : messages) { - switch (m.getType()) { - case Message.CANCEL_BUILD: - return new DefaultResult(argv, - new InterruptedException("The build was canceled")); - case Message.BUILD_EXCEPTION: - final BuildException e = (BuildException) m; - return new DefaultResult(argv, - new Exception(e.getClassName() + ": " + e.getMessage() + "\n" + e.getStackTrace())); - case Message.BUILD_STOPPED: - return new DefaultResult(argv, null); + // We've sent the request, so it gives us a bit of time to purge the logs + AtomicReference purgeMessage = new AtomicReference<>(); + Thread purgeLog = new Thread(() -> { + purgeMessage.set(purgeLogs()); + }, "Log purge"); + purgeLog.setDaemon(true); + purgeLog.start(); + + try { + while (true) { + final List messages = daemon.receive(); + output.accept(messages); + for (Message m : messages) { + switch (m.getType()) { + case Message.CANCEL_BUILD: + return new DefaultResult(argv, + new InterruptedException("The build was canceled")); + case Message.BUILD_EXCEPTION: + final BuildException e = (BuildException) m; + return new DefaultResult(argv, + new Exception(e.getClassName() + ": " + e.getMessage() + "\n" + e.getStackTrace())); + case Message.BUILD_STOPPED: + return new DefaultResult(argv, null); + } } } + } finally { + String msg = purgeMessage.get(); + if (msg != null) { + output.accept(Message.display(msg)); + } } } } } + private String purgeLogs() { + Path storage = parameters.daemonStorage(); + Duration purgeLogPeriod = parameters.purgeLogPeriod(); + if (!Files.isDirectory(storage) || !TimeUtils.isPositive(purgeLogPeriod)) { + return null; + } + String date = DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.systemDefault()).format(Instant.now()); + Path log = storage.resolve("purge-" + date + ".log"); + List deleted = new ArrayList<>(); + List exceptions = new ArrayList<>(); + FileTime limit = FileTime.from(Instant.now().minus(purgeLogPeriod)); + try { + Files.list(storage) + .filter(p -> p.getFileName().toString().endsWith(LOG_EXTENSION)) + .filter(p -> !log.equals(p)) + .filter(p -> { + try { + FileTime lmt = Files.getLastModifiedTime(p); + return lmt.compareTo(limit) < 0; + } catch (IOException e) { + exceptions.add(e); + return false; + } + }) + .forEach(p -> { + try { + Files.delete(p); + deleted.add(p); + } catch (IOException e) { + exceptions.add(e); + } + }); + } catch (Exception e) { + exceptions.add(e); + } + if (exceptions.isEmpty() && deleted.isEmpty()) { + return null; + } + String logMessage; + try (PrintWriter w = new PrintWriter(Files.newBufferedWriter(log, + StandardOpenOption.WRITE, StandardOpenOption.APPEND, StandardOpenOption.CREATE))) { + w.printf("Purge executed at %s%n", Instant.now().toString()); + if (deleted.isEmpty()) { + w.printf("No files deleted.%n"); + } else { + w.printf("Deleted files:%n"); + for (Path p : deleted) { + w.printf(" %s%n", p.toString()); + } + } + if (!exceptions.isEmpty()) { + w.printf("%d exception(s) occurred during the purge", exceptions.size()); + for (Throwable t : exceptions) { + t.printStackTrace(w); + } + } + char[] buf = new char[80]; + Arrays.fill(buf, '='); + w.printf("%s%n", new String(buf)); + logMessage = "log available in " + log.toString(); + } catch (IOException e) { + logMessage = "an exception occurred when writing log to " + log.toString() + ": " + e.toString(); + } + if (exceptions.isEmpty()) { + return String.format("Purged %d log files (%s)", deleted.size(), logMessage); + } else { + return String.format("Purged %d log files with %d exceptions (%s)", deleted.size(), exceptions.size(), logMessage); + } + } + private static class DefaultResult implements ExecutionResult { private final Exception exception; diff --git a/common/src/main/java/org/mvndaemon/mvnd/common/Environment.java b/common/src/main/java/org/mvndaemon/mvnd/common/Environment.java index b406b5ead..7732c85b5 100644 --- a/common/src/main/java/org/mvndaemon/mvnd/common/Environment.java +++ b/common/src/main/java/org/mvndaemon/mvnd/common/Environment.java @@ -68,6 +68,10 @@ public String asCommandLineProperty(String value) { * The number of log lines to display for each Maven module that is built in parallel. */ MVND_ROLLING_WINDOW_SIZE("mvnd.rollingWindowSize", null, "0", false), + /** + * The automatic log purge period + */ + MVND_LOG_PURGE_PERIOD("mvnd.logPurgePeriod", null, "1w", false), /** * The path to the daemon registry */ From 85d2189f79d36f7e50f71b0b04efdde910474d66 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 12 Nov 2020 17:14:26 +0100 Subject: [PATCH 3/3] Rewrite and simplify the TimeUtils class, add a bit of doc on the Environment class, add a unit test --- .../mvndaemon/mvnd/common/Environment.java | 10 +- .../org/mvndaemon/mvnd/common/TimeUtils.java | 255 +++++------------- .../mvndaemon/mvnd/common/TimeUtilsTest.java | 41 +++ 3 files changed, 115 insertions(+), 191 deletions(-) create mode 100644 common/src/test/java/org/mvndaemon/mvnd/common/TimeUtilsTest.java diff --git a/common/src/main/java/org/mvndaemon/mvnd/common/Environment.java b/common/src/main/java/org/mvndaemon/mvnd/common/Environment.java index 7732c85b5..e6eb6e3ff 100644 --- a/common/src/main/java/org/mvndaemon/mvnd/common/Environment.java +++ b/common/src/main/java/org/mvndaemon/mvnd/common/Environment.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,12 @@ /** * Collects system properties and environment variables used by mvnd client or server. + * + * Duration properties such as {@link #DAEMON_IDLE_TIMEOUT}, {@link #DAEMON_KEEP_ALIVE}, + * {@link #DAEMON_EXPIRATION_CHECK_DELAY} or {@link #MVND_LOG_PURGE_PERIOD} are expressed + * in a human readable format such as {@code 2h30m}, {@code 600ms} or {@code 10 seconds}. + * The available units are d/day/days, h/hour/hours, m/min/minute/minutes, + * s/sec/second/seconds and ms/millis/msec/milliseconds. */ public enum Environment { // @@ -106,7 +112,7 @@ public String asCommandLineProperty(String value) { /** * The maven builder name to use. Ignored if the user passes * - * @{@code -b} or @{@code --builder} on the command line + * {@code -b} or {@code --builder} on the command line */ MVND_BUILDER("mvnd.builder", null, "smart", false) { @Override diff --git a/common/src/main/java/org/mvndaemon/mvnd/common/TimeUtils.java b/common/src/main/java/org/mvndaemon/mvnd/common/TimeUtils.java index da72775d9..ea07effb7 100644 --- a/common/src/main/java/org/mvndaemon/mvnd/common/TimeUtils.java +++ b/common/src/main/java/org/mvndaemon/mvnd/common/TimeUtils.java @@ -1,10 +1,9 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -16,36 +15,33 @@ */ package org.mvndaemon.mvnd.common; -import java.text.DecimalFormat; -import java.text.DecimalFormatSymbols; -import java.text.NumberFormat; import java.time.Duration; -import java.util.Locale; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Time utils. - * - * Origin file: - * https://github.com/apache/camel/blob/4ea9e6c357371682b855d2d79655b41120331b7a/core/camel-util/src/main/java/org/apache/camel/util/TimeUtils.java */ public final class TimeUtils { - private static final Pattern NUMBERS_ONLY_STRING_PATTERN = Pattern.compile("^[-]?(\\d)+$", Pattern.CASE_INSENSITIVE); - private static final Pattern WEEK_REGEX_PATTERN = Pattern.compile("((\\d)*(\\d))\\s*w(eek(s)?)?(?=\\b|\\d|$)", - Pattern.CASE_INSENSITIVE); - private static final Pattern DAY_REGEX_PATTERN = Pattern.compile("((\\d)*(\\d))\\s*d(ay(s)?)?(?=\\b|\\d|$)", - Pattern.CASE_INSENSITIVE); - private static final Pattern HOUR_REGEX_PATTERN = Pattern.compile("((\\d)*(\\d))\\s*h(our(s)?)?(?=\\b|\\d|$)", - Pattern.CASE_INSENSITIVE); - private static final Pattern MINUTES_REGEX_PATTERN = Pattern.compile("((\\d)*(\\d))\\s*m(in(ute(s)?)?)?(?=\\b|\\d|$)", + private static final int ONE_UNIT = 1; + public static final long DAYS_MILLIS = TimeUnit.DAYS.toMillis(ONE_UNIT); + public static final long HOURS_MILLIS = TimeUnit.HOURS.toMillis(ONE_UNIT); + public static final long MINUTES_MILLIS = TimeUnit.MINUTES.toMillis(ONE_UNIT); + public static final long SECONDS_MILLIS = TimeUnit.SECONDS.toMillis(ONE_UNIT); + + private static final Pattern DURATION_PATTERN = Pattern.compile( + "(?-?\\d+)" + + "|" + + "(" + + "((?\\d+)\\s*d(ay(s)?)?)?" + "\\s*" + + "((?\\d+)\\s*h(our(s)?)?)?" + "\\s*" + + "((?\\d+)\\s*m(in(ute(s)?)?)?)?" + "\\s*" + + "((?\\d+(\\.\\d+)?)\\s*s(ec(ond(s)?)?)?)?" + "\\s*" + + "((?\\d+(\\.\\d+)?)\\s*m(illi)?s(ec(ond)?(s)?)?)?" + + ")", Pattern.CASE_INSENSITIVE); - private static final Pattern SECONDS_REGEX_PATTERN = Pattern - .compile("((\\d)(\\d)*)(\\.(\\d+))?\\s*s(ec(ond)?(s)?)?(?=\\b|\\d|$)", Pattern.CASE_INSENSITIVE); - private static final Pattern MILLIS_REGEX_PATTERN = Pattern - .compile("((\\d)(\\d)*)(\\.(\\d+))?\\s*m(illi)?s(ec(ond)?(s)?)?(?=\\b|\\d|$)", Pattern.CASE_INSENSITIVE); private TimeUtils() { } @@ -59,46 +55,41 @@ public static String printDuration(Duration uptime) { } /** - * Prints the duration in a human readable format as X days Y hours Z minutes etc. + * This will print time in human readable format from milliseconds. + * Examples: + * 500 -> 500ms + * 1300 -> 1s300ms + * 310300 -> 5m10s300ms + * 6600000 -> 1h50m * - * @param uptime the uptime in millis - * @return the time used for displaying on screen or in logs + * @param millis time in milliseconds + * @return time in string */ - public static String printDuration(double uptime) { - // Code taken from Karaf - // https://svn.apache.org/repos/asf/karaf/trunk/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/InfoAction.java - - NumberFormat fmtI = new DecimalFormat("###,###", new DecimalFormatSymbols(Locale.ENGLISH)); - NumberFormat fmtD = new DecimalFormat("###,##0.000", new DecimalFormatSymbols(Locale.ENGLISH)); - - uptime /= 1000; - if (uptime < 60) { - return fmtD.format(uptime) + " seconds"; + public static String printDuration(long millis) { + if (millis < 0) { + return Long.toString(millis); + } + final StringBuilder sb = new StringBuilder(); + if (millis >= DAYS_MILLIS) { + sb.append(millis / DAYS_MILLIS).append("d"); + millis %= DAYS_MILLIS; } - uptime /= 60; - if (uptime < 60) { - long minutes = (long) uptime; - String s = fmtI.format(minutes) + (minutes > 1 ? " minutes" : " minute"); - return s; + if (millis >= HOURS_MILLIS) { + sb.append(millis / HOURS_MILLIS).append("h"); + millis %= HOURS_MILLIS; } - uptime /= 60; - if (uptime < 24) { - long hours = (long) uptime; - long minutes = (long) ((uptime - hours) * 60); - String s = fmtI.format(hours) + (hours > 1 ? " hours" : " hour"); - if (minutes != 0) { - s += " " + fmtI.format(minutes) + (minutes > 1 ? " minutes" : " minute"); - } - return s; + if (millis >= MINUTES_MILLIS) { + sb.append(millis / MINUTES_MILLIS).append("m"); + millis %= MINUTES_MILLIS; } - uptime /= 24; - long days = (long) uptime; - long hours = (long) ((uptime - days) * 24); - String s = fmtI.format(days) + (days > 1 ? " days" : " day"); - if (hours != 0) { - s += " " + fmtI.format(hours) + (hours > 1 ? " hours" : " hour"); + if (millis >= SECONDS_MILLIS) { + sb.append(millis / SECONDS_MILLIS).append("s"); + millis %= SECONDS_MILLIS; } - return s; + if (millis >= ONE_UNIT || sb.length() == 0) { + sb.append(millis / ONE_UNIT).append("ms"); + } + return sb.toString(); } public static Duration toDuration(String source) throws IllegalArgumentException { @@ -106,139 +97,25 @@ public static Duration toDuration(String source) throws IllegalArgumentException } public static long toMilliSeconds(String source) throws IllegalArgumentException { - // quick conversion if its only digits - boolean digit = true; - for (int i = 0; i < source.length(); i++) { - char ch = source.charAt(i); - // special for first as it can be negative number - if (i == 0 && ch == '-') { - continue; - } - // quick check if its 0..9 - if (ch < '0' || ch > '9') { - digit = false; - break; - } - } - if (digit) { - return Long.parseLong(source); + Matcher matcher = DURATION_PATTERN.matcher(source); + if (!matcher.matches()) { + throw new IllegalArgumentException("Unable to parse duration: '" + source + "'"); } - - long milliseconds = 0; - boolean foundFlag = false; - - checkCorrectnessOfPattern(source); - Matcher matcher; - - matcher = createMatcher(NUMBERS_ONLY_STRING_PATTERN, source); - if (matcher.find()) { - // Note: This will also be used for regular numeric strings. - // This String -> long converter will be used for all strings. - milliseconds = Long.parseLong(source); + String n = matcher.group("n"); + if (n != null) { + return Long.parseLong(n); } else { - matcher = createMatcher(WEEK_REGEX_PATTERN, source); - if (matcher.find()) { - milliseconds += 7 * TimeUnit.DAYS.toMillis(Long.parseLong(matcher.group(1))); - foundFlag = true; - } - - matcher = createMatcher(DAY_REGEX_PATTERN, source); - if (matcher.find()) { - milliseconds += TimeUnit.DAYS.toMillis(Long.parseLong(matcher.group(1))); - foundFlag = true; - } - - matcher = createMatcher(HOUR_REGEX_PATTERN, source); - if (matcher.find()) { - milliseconds += TimeUnit.HOURS.toMillis(Long.parseLong(matcher.group(1))); - foundFlag = true; - } - - matcher = createMatcher(MINUTES_REGEX_PATTERN, source); - if (matcher.find()) { - milliseconds += TimeUnit.MINUTES.toMillis(Long.parseLong(matcher.group(1))); - foundFlag = true; - } - - matcher = createMatcher(SECONDS_REGEX_PATTERN, source); - if (matcher.find()) { - milliseconds += TimeUnit.SECONDS.toMillis(Long.parseLong(matcher.group(1))); - if (matcher.group(5) != null && !matcher.group(5).isEmpty()) { - milliseconds += TimeUnit.MILLISECONDS.toMillis(Long.parseLong(matcher.group(5))); - } - foundFlag = true; - } - - matcher = createMatcher(MILLIS_REGEX_PATTERN, source); - if (matcher.find()) { - milliseconds += TimeUnit.MILLISECONDS.toMillis(Long.parseLong(matcher.group(1))); - foundFlag = true; - } - - // No pattern matched... initiating fallback check and conversion (if required). - // The source at this point may contain illegal values or special characters - if (!foundFlag) { - milliseconds = Long.parseLong(source); - } + String d = matcher.group("d"); + String h = matcher.group("h"); + String m = matcher.group("m"); + String s = matcher.group("s"); + String l = matcher.group("l"); + return (d != null ? TimeUnit.DAYS.toMillis(Long.parseLong(d)) : 0) + + (h != null ? TimeUnit.HOURS.toMillis(Long.parseLong(h)) : 0) + + (m != null ? TimeUnit.MINUTES.toMillis(Long.parseLong(m)) : 0) + + (s != null ? TimeUnit.SECONDS.toMillis(Long.parseLong(s)) : 0) + + (l != null ? TimeUnit.MILLISECONDS.toMillis(Long.parseLong(l)) : 0); } - - return milliseconds; - } - - private static void checkCorrectnessOfPattern(String source) { - //replace only numbers once - Matcher matcher = createMatcher(NUMBERS_ONLY_STRING_PATTERN, source); - String replaceSource = matcher.replaceFirst(""); - - //replace week string once - matcher = createMatcher(WEEK_REGEX_PATTERN, replaceSource); - if (matcher.find() && matcher.find()) { - throw new IllegalArgumentException("Weeks should not be specified more then once: " + source); - } - replaceSource = matcher.replaceFirst(""); - - //replace day string once - matcher = createMatcher(DAY_REGEX_PATTERN, replaceSource); - if (matcher.find() && matcher.find()) { - throw new IllegalArgumentException("Days should not be specified more then once: " + source); - } - replaceSource = matcher.replaceFirst(""); - - //replace hour string once - matcher = createMatcher(HOUR_REGEX_PATTERN, replaceSource); - if (matcher.find() && matcher.find()) { - throw new IllegalArgumentException("Hours should not be specified more then once: " + source); - } - replaceSource = matcher.replaceFirst(""); - - //replace minutes once - matcher = createMatcher(MINUTES_REGEX_PATTERN, replaceSource); - if (matcher.find() && matcher.find()) { - throw new IllegalArgumentException("Minutes should not be specified more then once: " + source); - } - replaceSource = matcher.replaceFirst(""); - - //replace seconds once - matcher = createMatcher(SECONDS_REGEX_PATTERN, replaceSource); - if (matcher.find() && matcher.find()) { - throw new IllegalArgumentException("Seconds should not be specified more then once: " + source); - } - replaceSource = matcher.replaceFirst(""); - - //replace millis once - matcher = createMatcher(MILLIS_REGEX_PATTERN, replaceSource); - if (matcher.find() && matcher.find()) { - throw new IllegalArgumentException("Milliseconds should not be specified more then once: " + source); - } - replaceSource = matcher.replaceFirst(""); - - if (replaceSource.length() > 0) { - throw new IllegalArgumentException("Illegal characters: " + source); - } - } - - private static Matcher createMatcher(Pattern pattern, String source) { - return pattern.matcher(source); } } diff --git a/common/src/test/java/org/mvndaemon/mvnd/common/TimeUtilsTest.java b/common/src/test/java/org/mvndaemon/mvnd/common/TimeUtilsTest.java new file mode 100644 index 000000000..12bfcc25a --- /dev/null +++ b/common/src/test/java/org/mvndaemon/mvnd/common/TimeUtilsTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mvndaemon.mvnd.common; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TimeUtilsTest { + + @Test + public void testToTimeAsString() { + assertEquals("600ms", TimeUtils.printDuration(TimeUtils.toDuration("600ms"))); + assertEquals("-1", TimeUtils.printDuration(TimeUtils.toDuration("-1"))); + assertEquals("0ms", TimeUtils.printDuration(TimeUtils.toDuration("0ms"))); + assertEquals("1s", TimeUtils.printDuration(TimeUtils.toDuration("1000ms"))); + assertEquals("1m600ms", TimeUtils.printDuration(TimeUtils.toDuration("1minute 600ms"))); + assertEquals("1m1s100ms", TimeUtils.printDuration(TimeUtils.toDuration("1m1100ms"))); + assertEquals("5m10s300ms", TimeUtils.printDuration(310300)); + assertEquals("5s500ms", TimeUtils.printDuration(5500)); + assertEquals("1h50m", TimeUtils.printDuration(6600000)); + assertEquals("2d3h4m", TimeUtils.printDuration(Duration.parse("P2DT3H4M").toMillis())); + assertEquals("2d4m", TimeUtils.printDuration(Duration.parse("P2DT4M").toMillis())); + } + +}