diff --git a/src/main/java/io/lettuce/core/AbstractRedisClient.java b/src/main/java/io/lettuce/core/AbstractRedisClient.java index b734fbd138..99cd97a4c8 100644 --- a/src/main/java/io/lettuce/core/AbstractRedisClient.java +++ b/src/main/java/io/lettuce/core/AbstractRedisClient.java @@ -356,7 +356,7 @@ public void shutdown(long quietPeriod, long timeout, TimeUnit timeUnit) { Thread.currentThread().interrupt(); throw new RedisCommandInterruptedException(e); } catch (Exception e) { - throw new RedisCommandExecutionException(e); + throw ExceptionFactory.createExecutionException(null, e); } } diff --git a/src/main/java/io/lettuce/core/ExceptionFactory.java b/src/main/java/io/lettuce/core/ExceptionFactory.java index de0f707cce..3f8e265a8f 100644 --- a/src/main/java/io/lettuce/core/ExceptionFactory.java +++ b/src/main/java/io/lettuce/core/ExceptionFactory.java @@ -15,6 +15,12 @@ */ package io.lettuce.core; +import java.time.Duration; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoField; + /** * Factory for Redis exceptions. * @@ -23,9 +29,74 @@ */ public abstract class ExceptionFactory { + private static final DateTimeFormatter MINUTES = new DateTimeFormatterBuilder().appendText(ChronoField.MINUTE_OF_DAY) + .appendLiteral(" minute(s)").toFormatter(); + + private static final DateTimeFormatter SECONDS = new DateTimeFormatterBuilder().appendText(ChronoField.SECOND_OF_DAY) + .appendLiteral(" second(s)").toFormatter(); + + private static final DateTimeFormatter MILLISECONDS = new DateTimeFormatterBuilder().appendText(ChronoField.MILLI_OF_DAY) + .appendLiteral(" millisecond(s)").toFormatter(); + private ExceptionFactory() { } + /** + * Create a {@link RedisCommandTimeoutException} with a detail message given the timeout. + * + * @param timeout the timeout value. + * @return the {@link RedisCommandTimeoutException}. + */ + public static RedisCommandTimeoutException createTimeoutException(Duration timeout) { + return new RedisCommandTimeoutException(String.format("Command timed out after %s", formatTimeout(timeout))); + } + + /** + * Create a {@link RedisCommandTimeoutException} with a detail message given the message and timeout. + * + * @param message the detail message. + * @param timeout the timeout value. + * @return the {@link RedisCommandTimeoutException}. + */ + public static RedisCommandTimeoutException createTimeoutException(String message, Duration timeout) { + return new RedisCommandTimeoutException( + String.format("%s. Command timed out after %s", message, formatTimeout(timeout))); + } + + static String formatTimeout(Duration duration) { + + if (duration.isZero()) { + return "no timeout"; + } + + LocalTime time = LocalTime.MIDNIGHT.plus(duration); + if (isExactMinutes(duration)) { + return MINUTES.format(time); + } + + if (isExactSeconds(duration)) { + return SECONDS.format(time); + } + + if (isExactMillis(duration)) { + return MILLISECONDS.format(time); + } + + return String.format("%d ns", duration.toNanos()); + } + + private static boolean isExactMinutes(Duration duration) { + return duration.toMillis() % (1000 * 60) == 0 && duration.getNano() == 0; + } + + private static boolean isExactSeconds(Duration duration) { + return duration.toMillis() % (1000) == 0 && duration.getNano() == 0; + } + + private static boolean isExactMillis(Duration duration) { + return duration.toNanos() % (1000 * 1000) == 0; + } + /** * Create a {@link RedisCommandExecutionException} with a detail message. Specific Redis error messages may create subtypes * of {@link RedisCommandExecutionException}. @@ -38,11 +109,11 @@ public static RedisCommandExecutionException createExecutionException(String mes } /** - * Create a {@link RedisCommandExecutionException} with a detail message. Specific Redis error messages may create subtypes - * of {@link RedisCommandExecutionException}. + * Create a {@link RedisCommandExecutionException} with a detail message and optionally a {@link Throwable cause}. Specific + * Redis error messages may create subtypes of {@link RedisCommandExecutionException}. * * @param message the detail message. - * @param cause the nested exception. + * @param cause the nested exception, may be {@literal null}. * @return the {@link RedisCommandExecutionException}. */ public static RedisCommandExecutionException createExecutionException(String message, Throwable cause) { diff --git a/src/main/java/io/lettuce/core/LettuceFutures.java b/src/main/java/io/lettuce/core/LettuceFutures.java index 5137decbd3..95e08faddb 100644 --- a/src/main/java/io/lettuce/core/LettuceFutures.java +++ b/src/main/java/io/lettuce/core/LettuceFutures.java @@ -91,7 +91,7 @@ public static boolean awaitAll(long timeout, TimeUnit unit, Future... futures Thread.currentThread().interrupt(); throw new RedisCommandInterruptedException(e); } catch (Exception e) { - throw new RedisCommandExecutionException(e); + throw ExceptionFactory.createExecutionException(null, e); } } @@ -111,7 +111,7 @@ public static T awaitOrCancel(RedisFuture cmd, long timeout, TimeUnit uni try { if (!cmd.await(timeout, unit)) { cmd.cancel(true); - throw new RedisCommandTimeoutException(); + throw ExceptionFactory.createTimeoutException(Duration.ofNanos(unit.toNanos(timeout))); } return cmd.get(); } catch (RuntimeException e) { @@ -128,7 +128,7 @@ public static T awaitOrCancel(RedisFuture cmd, long timeout, TimeUnit uni Thread.currentThread().interrupt(); throw new RedisCommandInterruptedException(e); } catch (Exception e) { - throw new RedisCommandExecutionException(e); + throw ExceptionFactory.createExecutionException(null, e); } } } diff --git a/src/main/java/io/lettuce/core/PlainChannelInitializer.java b/src/main/java/io/lettuce/core/PlainChannelInitializer.java index f306e6dc45..ac0ba775f2 100644 --- a/src/main/java/io/lettuce/core/PlainChannelInitializer.java +++ b/src/main/java/io/lettuce/core/PlainChannelInitializer.java @@ -132,9 +132,9 @@ static void pingBeforeActivate(AsyncCommand cmd, CompletableFuture { diff --git a/src/main/java/io/lettuce/core/RedisCommandExecutionException.java b/src/main/java/io/lettuce/core/RedisCommandExecutionException.java index 168b3f9176..cccd744443 100644 --- a/src/main/java/io/lettuce/core/RedisCommandExecutionException.java +++ b/src/main/java/io/lettuce/core/RedisCommandExecutionException.java @@ -23,16 +23,33 @@ @SuppressWarnings("serial") public class RedisCommandExecutionException extends RedisException { - public RedisCommandExecutionException(Throwable cause) { - super(cause); - } - + /** + * Create a {@code RedisCommandExecutionException} with the specified detail message. + * + * @param msg the detail message. + */ public RedisCommandExecutionException(String msg) { super(msg); } - public RedisCommandExecutionException(String msg, Throwable e) { - super(msg, e); + /** + * Create a {@code RedisCommandExecutionException} with the specified detail message and nested exception. + * + * @param msg the detail message. + * @param cause the nested exception. + */ + public RedisCommandExecutionException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Create a {@code RedisCommandExecutionException} with the specified nested exception. + * + * @param msg the detail message. + * @param cause the nested exception. + */ + public RedisCommandExecutionException(Throwable cause) { + super(cause); } } diff --git a/src/main/java/io/lettuce/core/RedisCommandInterruptedException.java b/src/main/java/io/lettuce/core/RedisCommandInterruptedException.java index 6e9c3a84c6..35feed641e 100644 --- a/src/main/java/io/lettuce/core/RedisCommandInterruptedException.java +++ b/src/main/java/io/lettuce/core/RedisCommandInterruptedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2016 the original author or authors. + * Copyright 2011-2017 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. @@ -19,11 +19,17 @@ * Exception thrown when the thread executing a redis command is interrupted. * * @author Will Glozer + * @author Mark Paluch */ @SuppressWarnings("serial") public class RedisCommandInterruptedException extends RedisException { - public RedisCommandInterruptedException(Throwable e) { - super("Command interrupted", e); + /** + * Create a {@code RedisCommandInterruptedException} with the specified nested exception. + * + * @param cause the nested exception. + */ + public RedisCommandInterruptedException(Throwable cause) { + super("Command interrupted", cause); } } diff --git a/src/main/java/io/lettuce/core/RedisCommandTimeoutException.java b/src/main/java/io/lettuce/core/RedisCommandTimeoutException.java index 334860135b..864ca72b20 100644 --- a/src/main/java/io/lettuce/core/RedisCommandTimeoutException.java +++ b/src/main/java/io/lettuce/core/RedisCommandTimeoutException.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2016 the original author or authors. + * Copyright 2011-2017 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. @@ -23,10 +23,18 @@ @SuppressWarnings("serial") public class RedisCommandTimeoutException extends RedisException { + /** + * Create a {@code RedisCommandTimeoutException} with a default message. + */ public RedisCommandTimeoutException() { super("Command timed out"); } + /** + * Create a {@code RedisCommandTimeoutException} with the specified detail message. + * + * @param msg the detail message. + */ public RedisCommandTimeoutException(String msg) { super(msg); } diff --git a/src/main/java/io/lettuce/core/RedisConnectionException.java b/src/main/java/io/lettuce/core/RedisConnectionException.java index b1a5556855..043b1fc245 100644 --- a/src/main/java/io/lettuce/core/RedisConnectionException.java +++ b/src/main/java/io/lettuce/core/RedisConnectionException.java @@ -25,20 +25,31 @@ @SuppressWarnings("serial") public class RedisConnectionException extends RedisException { + /** + * Create a {@code RedisConnectionException} with the specified detail message. + * + * @param msg the detail message. + */ public RedisConnectionException(String msg) { super(msg); } - public RedisConnectionException(String msg, Throwable e) { - super(msg, e); + /** + * Create a {@code RedisConnectionException} with the specified detail message and nested exception. + * + * @param msg the detail message. + * @param cause the nested exception. + */ + public RedisConnectionException(String msg, Throwable cause) { + super(msg, cause); } /** * Create a new {@link RedisConnectionException} given {@link SocketAddress} and the {@link Throwable cause}. * - * @param remoteAddress - * @param cause - * @return + * @param remoteAddress remote socket address. + * @param cause the nested exception. + * @return the {@link RedisConnectionException}. * @since 4.4 */ public static RedisConnectionException create(SocketAddress remoteAddress, Throwable cause) { diff --git a/src/main/java/io/lettuce/core/RedisException.java b/src/main/java/io/lettuce/core/RedisException.java index 2258211e09..a85dca86d1 100644 --- a/src/main/java/io/lettuce/core/RedisException.java +++ b/src/main/java/io/lettuce/core/RedisException.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2016 the original author or authors. + * Copyright 2011-2017 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. @@ -19,18 +19,36 @@ * Exception thrown when Redis returns an error message, or when the client fails for any reason. * * @author Will Glozer + * @author Mark Paluch */ @SuppressWarnings("serial") public class RedisException extends RuntimeException { + /** + * Create a {@code RedisException} with the specified detail message. + * + * @param msg the detail message. + */ public RedisException(String msg) { super(msg); } + /** + * Create a {@code RedisException} with the specified detail message and nested exception. + * + * @param msg the detail message. + * @param cause the nested exception. + */ public RedisException(String msg, Throwable e) { super(msg, e); } + /** + * Create a {@code RedisException} with the specified nested exception. + * + * @param msg the detail message. + * @param cause the nested exception. + */ public RedisException(Throwable cause) { super(cause); } diff --git a/src/main/java/io/lettuce/core/cluster/NodeSelectionInvocationHandler.java b/src/main/java/io/lettuce/core/cluster/NodeSelectionInvocationHandler.java index 10cb22e9de..3cb1b251bd 100644 --- a/src/main/java/io/lettuce/core/cluster/NodeSelectionInvocationHandler.java +++ b/src/main/java/io/lettuce/core/cluster/NodeSelectionInvocationHandler.java @@ -160,7 +160,7 @@ private Object getExecutions(Map executions) throws Ex if (executionModel == ExecutionModel.SYNC) { if (!awaitAll(timeout.toNanos(), TimeUnit.NANOSECONDS, asyncExecutions.values())) { - throw createTimeoutException(asyncExecutions); + throw createTimeoutException(asyncExecutions, timeout); } if (atLeastOneFailed(asyncExecutions)) { @@ -210,7 +210,8 @@ private boolean atLeastOneFailed(Map> execu .anyMatch(completionStage -> completionStage.toCompletableFuture().isCompletedExceptionally()); } - private RedisCommandTimeoutException createTimeoutException(Map> executions) { + private RedisCommandTimeoutException createTimeoutException(Map> executions, + Duration timeout) { List notFinished = new ArrayList<>(); executions.forEach((redisClusterNode, completionStage) -> { @@ -220,7 +221,7 @@ private RedisCommandTimeoutException createTimeoutException(Map> executions) { diff --git a/src/test/java/io/lettuce/core/ExceptionFactoryTest.java b/src/test/java/io/lettuce/core/ExceptionFactoryTest.java index 62b71122f8..7d1584d405 100644 --- a/src/test/java/io/lettuce/core/ExceptionFactoryTest.java +++ b/src/test/java/io/lettuce/core/ExceptionFactoryTest.java @@ -17,9 +17,8 @@ import static org.assertj.core.api.Assertions.assertThat; -import io.lettuce.core.ExceptionFactory; -import io.lettuce.core.RedisBusyException; -import io.lettuce.core.RedisNoScriptException; +import java.time.Duration; + import org.junit.Test; /** @@ -58,4 +57,30 @@ public void shouldCreateExecutionException() { assertThat(ExceptionFactory.createExecutionException(null, new IllegalStateException())).isInstanceOf( RedisCommandExecutionException.class).hasRootCauseInstanceOf(IllegalStateException.class); } + + @Test + public void shouldFormatExactUnits() { + + assertThat(ExceptionFactory.formatTimeout(Duration.ofMinutes(2))).isEqualTo("2 minute(s)"); + assertThat(ExceptionFactory.formatTimeout(Duration.ofMinutes(1))).isEqualTo("1 minute(s)"); + assertThat(ExceptionFactory.formatTimeout(Duration.ofMinutes(0))).isEqualTo("no timeout"); + + assertThat(ExceptionFactory.formatTimeout(Duration.ofSeconds(2))).isEqualTo("2 second(s)"); + assertThat(ExceptionFactory.formatTimeout(Duration.ofSeconds(1))).isEqualTo("1 second(s)"); + assertThat(ExceptionFactory.formatTimeout(Duration.ofSeconds(0))).isEqualTo("no timeout"); + + assertThat(ExceptionFactory.formatTimeout(Duration.ofMillis(2))).isEqualTo("2 millisecond(s)"); + assertThat(ExceptionFactory.formatTimeout(Duration.ofMillis(1))).isEqualTo("1 millisecond(s)"); + assertThat(ExceptionFactory.formatTimeout(Duration.ofMillis(0))).isEqualTo("no timeout"); + } + + @Test + public void shouldFormatToMinmalApplicableTimeunit() { + + assertThat(ExceptionFactory.formatTimeout(Duration.ofMinutes(2).plus(Duration.ofSeconds(10)))).isEqualTo( + "130 second(s)"); + assertThat(ExceptionFactory.formatTimeout(Duration.ofSeconds(2).plus(Duration.ofMillis(5)))).isEqualTo( + "2005 millisecond(s)"); + assertThat(ExceptionFactory.formatTimeout(Duration.ofNanos(2))).isEqualTo("2 ns"); + } } \ No newline at end of file