This is an automated email from the ASF dual-hosted git repository. jinmeiliao pushed a commit to branch support/1.12 in repository https://gitbox.apache.org/repos/asf/geode.git
The following commit(s) were added to refs/heads/support/1.12 by this push: new 0cbf412 GEODE-8623: Retry getting local host if it fails. (#5743) 0cbf412 is described below commit 0cbf41293b49b4b3af7eca0a132f494190bbac81 Author: Jinmei Liao <jil...@pivotal.io> AuthorDate: Mon Nov 30 11:39:18 2020 -0800 GEODE-8623: Retry getting local host if it fails. (#5743) Co-authored-by: Jacob Barrett <jbarr...@pivotal.io> (cherry picked from commit 5deb409fe5498845b1365463b11f7a8d558c55f7) # Conflicts: # geode-common/build.gradle # geode-core/src/main/java/org/apache/geode/internal/net/SocketCreator.java # geode-tcp-server/src/distributedTest/java/org/apache/geode/distributed/internal/tcpserver/TcpServerProductVersionDUnitTest.java --- geode-common/build.gradle | 1 + .../main/java/org/apache/geode/internal/Retry.java | 101 +++++++++++++++++++++ .../apache/geode/internal/inet/LocalHostUtil.java | 31 +++++-- .../java/org/apache/geode/internal/RetryTest.java | 93 +++++++++++++++++++ 4 files changed, 219 insertions(+), 7 deletions(-) diff --git a/geode-common/build.gradle b/geode-common/build.gradle index c083f01..ced5435 100755 --- a/geode-common/build.gradle +++ b/geode-common/build.gradle @@ -24,4 +24,5 @@ dependencies { compile('com.fasterxml.jackson.core:jackson-databind') testCompile('junit:junit') testCompile('org.assertj:assertj-core') + testCompile('org.mockito:mockito-core') } diff --git a/geode-common/src/main/java/org/apache/geode/internal/Retry.java b/geode-common/src/main/java/org/apache/geode/internal/Retry.java new file mode 100644 index 0000000..6189c36 --- /dev/null +++ b/geode-common/src/main/java/org/apache/geode/internal/Retry.java @@ -0,0 +1,101 @@ +/* + * 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.apache.geode.internal; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import org.apache.geode.annotations.VisibleForTesting; + +/** + * Utility class for retrying operations. + */ +public class Retry { + + interface Timer { + long nanoTime(); + + void sleep(long sleepTimeInNano) throws InterruptedException; + } + + static class SteadyTimer implements Timer { + @Override + public long nanoTime() { + return System.nanoTime(); + } + + @Override + public void sleep(long sleepTimeInNano) throws InterruptedException { + long millis = NANOSECONDS.toMillis(sleepTimeInNano); + // avoid throwing IllegalArgumentException + if (millis > 0) { + Thread.sleep(millis); + } + } + } + + private static final SteadyTimer steadyClock = new SteadyTimer(); + + /** + * Try the supplier function until the predicate is true or timeout occurs. + * + * @param timeout to retry for + * @param timeoutUnit the unit for timeout + * @param interval time between each try + * @param intervalUnit the unit for interval + * @param supplier to execute until predicate is true or times out + * @param predicate to test for retry + * @param <T> type of return value + * @return value from supplier after it passes predicate or times out. + */ + public static <T> T tryFor(long timeout, TimeUnit timeoutUnit, + long interval, TimeUnit intervalUnit, + Supplier<T> supplier, + Predicate<T> predicate) throws TimeoutException, InterruptedException { + return tryFor(timeout, timeoutUnit, interval, intervalUnit, supplier, predicate, steadyClock); + } + + @VisibleForTesting + static <T> T tryFor(long timeout, TimeUnit timeoutUnit, + long interval, TimeUnit intervalUnit, + Supplier<T> supplier, + Predicate<T> predicate, + Timer timer) throws TimeoutException, InterruptedException { + long until = timer.nanoTime() + NANOSECONDS.convert(timeout, timeoutUnit); + long intervalNano = NANOSECONDS.convert(interval, intervalUnit); + + T value; + for (;;) { + value = supplier.get(); + if (predicate.test(value)) { + return value; + } else { + // if there is still more time left after we sleep for interval period, then sleep and retry + // otherwise break out and throw TimeoutException + if ((timer.nanoTime() + intervalNano) < until) { + timer.sleep(intervalNano); + } else { + break; + } + } + } + + throw new TimeoutException(); + } +} diff --git a/geode-common/src/main/java/org/apache/geode/internal/inet/LocalHostUtil.java b/geode-common/src/main/java/org/apache/geode/internal/inet/LocalHostUtil.java index f904c55..b906b68 100644 --- a/geode-common/src/main/java/org/apache/geode/internal/inet/LocalHostUtil.java +++ b/geode-common/src/main/java/org/apache/geode/internal/inet/LocalHostUtil.java @@ -14,6 +14,9 @@ */ package org.apache.geode.internal.inet; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.apache.geode.internal.Retry.tryFor; + import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; @@ -23,7 +26,9 @@ import java.net.UnknownHostException; import java.util.Enumeration; import java.util.HashSet; import java.util.Hashtable; +import java.util.Objects; import java.util.Set; +import java.util.concurrent.TimeoutException; import javax.naming.Context; import javax.naming.NamingEnumeration; @@ -54,18 +59,26 @@ public class LocalHostUtil { Boolean.getBoolean(USE_LINK_LOCAL_ADDRESSES_PROPERTY); /** - * we cache localHost to avoid bug #40619, access-violation in native code - */ - private static final InetAddress localHost; - - /** * all classes should use this variable to determine whether to use IPv4 or IPv6 addresses */ @MakeNotStatic private static boolean useIPv6Addresses = !Boolean.getBoolean("java.net.preferIPv4Stack") && Boolean.getBoolean("java.net.preferIPv6Addresses"); - static { + /** + * Resolves local host. Will retry if resolution fails. + * + * @return local host if resolved otherwise null. + */ + private static InetAddress tryToResolveLocalHost() { + try { + return tryFor(60, SECONDS, 1, SECONDS, LocalHostUtil::resolveLocalHost, Objects::nonNull); + } catch (TimeoutException | InterruptedException ignored) { + } + return null; + } + + private static InetAddress resolveLocalHost() { InetAddress inetAddress = null; try { inetAddress = InetAddress.getByAddress(InetAddress.getLocalHost().getAddress()); @@ -114,9 +127,13 @@ public class LocalHostUtil { } } catch (UnknownHostException ignored) { } - localHost = inetAddress; + return inetAddress; } + /** + * Cache local host to avoid lookup costs. + */ + private static final InetAddress localHost = tryToResolveLocalHost(); /** * returns a set of the non-loopback InetAddresses for this machine diff --git a/geode-common/src/test/java/org/apache/geode/internal/RetryTest.java b/geode-common/src/test/java/org/apache/geode/internal/RetryTest.java new file mode 100644 index 0000000..09f0892 --- /dev/null +++ b/geode-common/src/test/java/org/apache/geode/internal/RetryTest.java @@ -0,0 +1,93 @@ +/* + * 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.apache.geode.internal; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Objects; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import org.junit.Before; +import org.junit.Test; + +public class RetryTest { + Retry.Timer timer; + + @Before + public void before() throws Exception { + AtomicLong atomicLong = new AtomicLong(); + timer = mock(Retry.Timer.class); + when(timer.nanoTime()).thenReturn(0L, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L); + } + + @Test + public void tryForReturnsImmediatelyOnPredicateMatch() + throws TimeoutException, InterruptedException { + final Integer value = + Retry.tryFor(1, NANOSECONDS, 1, NANOSECONDS, () -> 10, (v) -> v == 10, timer); + assertThat(value).isEqualTo(10); + // nanoTime is only called one time if predicate match immediately + verify(timer, times(1)).nanoTime(); + // sleep is never called if predicate matches immediately + verify(timer, never()).sleep(anyLong()); + } + + @Test + public void tryForReturnsAfterRetries() throws TimeoutException, InterruptedException { + final AtomicInteger shared = new AtomicInteger(); + final Integer value = + Retry.tryFor(10, NANOSECONDS, 1, NANOSECONDS, shared::getAndIncrement, (v) -> v == 3, + timer); + assertThat(value).isEqualTo(3); + verify(timer, times(4)).nanoTime(); + verify(timer, times(3)).sleep(1L); + } + + @Test + public void tryForThrowsAfterTimeout() throws InterruptedException { + assertThatThrownBy( + () -> Retry.tryFor(3, NANOSECONDS, 1, NANOSECONDS, () -> null, Objects::nonNull, timer)) + .isInstanceOf(TimeoutException.class); + verify(timer, times(3)).nanoTime(); + verify(timer, times(1)).sleep(1L); + } + + @Test + public void timerSleepCanTakeNegativeArgument() throws Exception { + Retry.SteadyTimer steadyTimer = new Retry.SteadyTimer(); + assertThatNoException().isThrownBy(() -> steadyTimer.sleep(-2)); + } + + @Test + public void lastIterationSleepForLessThanIntervalTime() throws Exception { + assertThatThrownBy( + () -> Retry.tryFor(2, NANOSECONDS, 3, NANOSECONDS, () -> null, Objects::nonNull, timer)) + .isInstanceOf(TimeoutException.class); + verify(timer, times(2)).nanoTime(); + verify(timer, never()).sleep(anyLong()); + } +}