GEODE-8623: Retry getting local host if it fails. (#5743)
Co-authored-by: Jacob Barrett <jbarrett@pivotal.io>
diff --git a/geode-common/build.gradle b/geode-common/build.gradle
index c61a74f..60f867d 100755
--- a/geode-common/build.gradle
+++ b/geode-common/build.gradle
@@ -30,6 +30,7 @@
// test
testImplementation('junit:junit')
testImplementation('org.assertj:assertj-core')
+ testImplementation('org.mockito:mockito-core')
// jmhTest
diff --git a/geode-common/src/main/java/org/apache/geode/annotations/internal/DeprecatedButRequiredForBackwardsCompatibilityTesting.java b/geode-common/src/main/java/org/apache/geode/annotations/internal/DeprecatedButRequiredForBackwardsCompatibilityTesting.java
new file mode 100644
index 0000000..de8f08f
--- /dev/null
+++ b/geode-common/src/main/java/org/apache/geode/annotations/internal/DeprecatedButRequiredForBackwardsCompatibilityTesting.java
@@ -0,0 +1,48 @@
+/*
+ * 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.annotations.internal;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PACKAGE;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.ElementType.TYPE;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotates a deprecated element that must remain in place for backwards compatibility testing.
+ * Tests which utilize this element should be marked with
+ * {@link SuppressDeprecationForBackwardsCompatibilityTesting}.
+ *
+ * Should be paired with {@link Deprecated}.
+ */
+@Documented
+@Retention(RetentionPolicy.SOURCE)
+@Target(value = {CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
+@Inherited
+public @interface DeprecatedButRequiredForBackwardsCompatibilityTesting {
+
+ /** Optional description */
+ String value() default "";
+
+}
diff --git a/geode-common/src/main/java/org/apache/geode/annotations/internal/SuppressDeprecationForBackwardsCompatibilityTesting.java b/geode-common/src/main/java/org/apache/geode/annotations/internal/SuppressDeprecationForBackwardsCompatibilityTesting.java
new file mode 100644
index 0000000..5a8af6c
--- /dev/null
+++ b/geode-common/src/main/java/org/apache/geode/annotations/internal/SuppressDeprecationForBackwardsCompatibilityTesting.java
@@ -0,0 +1,46 @@
+/*
+ * 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.annotations.internal;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PACKAGE;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.ElementType.TYPE;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates suppression of deprecation warning because the element is required for backwards
+ * compatibility testing. The element being suppressed should have a
+ * {@link DeprecatedButRequiredForBackwardsCompatibilityTesting} annotation.
+ *
+ * Should be paired with {@link SuppressWarnings}("deprecation") to actually suppress warning.
+ */
+@Documented
+@Retention(RetentionPolicy.SOURCE)
+@Target(value = {CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
+public @interface SuppressDeprecationForBackwardsCompatibilityTesting {
+
+ /** Optional description */
+ String value() default "";
+
+}
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 1e3a0f5..b947aaf 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;
@@ -24,7 +27,9 @@
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;
@@ -55,18 +60,26 @@
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());
@@ -115,9 +128,13 @@
}
} 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());
+ }
+}
diff --git a/geode-core/src/main/java/org/apache/geode/internal/net/SocketCreator.java b/geode-core/src/main/java/org/apache/geode/internal/net/SocketCreator.java
index 48c3e22..4f11c79 100755
--- a/geode-core/src/main/java/org/apache/geode/internal/net/SocketCreator.java
+++ b/geode-core/src/main/java/org/apache/geode/internal/net/SocketCreator.java
@@ -68,6 +68,7 @@
import org.apache.geode.GemFireConfigException;
import org.apache.geode.SystemFailure;
import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.annotations.internal.DeprecatedButRequiredForBackwardsCompatibilityTesting;
import org.apache.geode.annotations.internal.MakeNotStatic;
import org.apache.geode.cache.wan.GatewaySender;
import org.apache.geode.cache.wan.GatewayTransportFilter;
@@ -155,8 +156,10 @@
* This method has migrated to LocalHostUtil but is kept in place here for
* backward-compatibility testing.
*
- * @deprecated use LocalHostUtil.getLocalHost()
+ * @deprecated use {@link LocalHostUtil#getLocalHost()}
*/
+ @DeprecatedButRequiredForBackwardsCompatibilityTesting
+ @Deprecated
public static InetAddress getLocalHost() throws UnknownHostException {
return LocalHostUtil.getLocalHost();
}
diff --git a/geode-dunit/src/main/java/org/apache/geode/test/dunit/NetworkUtils.java b/geode-dunit/src/main/java/org/apache/geode/test/dunit/NetworkUtils.java
index 98e113e..11aaa66 100755
--- a/geode-dunit/src/main/java/org/apache/geode/test/dunit/NetworkUtils.java
+++ b/geode-dunit/src/main/java/org/apache/geode/test/dunit/NetworkUtils.java
@@ -21,6 +21,7 @@
import java.net.InetAddress;
import java.net.UnknownHostException;
+import org.apache.geode.annotations.internal.SuppressDeprecationForBackwardsCompatibilityTesting;
import org.apache.geode.internal.net.SocketCreator;
/**
@@ -50,6 +51,8 @@
*
* @return an IP literal which honors java.net.preferIPvAddresses
*/
+ @SuppressDeprecationForBackwardsCompatibilityTesting
+ @SuppressWarnings("deprecation")
public static String getIPLiteral() {
try {
return SocketCreator.getLocalHost().getHostAddress();
diff --git a/geode-tcp-server/src/distributedTest/java/org/apache/geode/distributed/internal/tcpserver/TcpServerProductVersionDUnitTest.java b/geode-tcp-server/src/distributedTest/java/org/apache/geode/distributed/internal/tcpserver/TcpServerProductVersionDUnitTest.java
index 3530ba6..9d404d1 100644
--- a/geode-tcp-server/src/distributedTest/java/org/apache/geode/distributed/internal/tcpserver/TcpServerProductVersionDUnitTest.java
+++ b/geode-tcp-server/src/distributedTest/java/org/apache/geode/distributed/internal/tcpserver/TcpServerProductVersionDUnitTest.java
@@ -40,6 +40,7 @@
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
+import org.apache.geode.annotations.internal.SuppressDeprecationForBackwardsCompatibilityTesting;
import org.apache.geode.distributed.Locator;
import org.apache.geode.distributed.internal.DistributionConfigImpl;
import org.apache.geode.distributed.internal.InternalLocator;
@@ -193,6 +194,7 @@
tcpClient = getLegacyTcpClient();
}
+ @SuppressDeprecationForBackwardsCompatibilityTesting
@SuppressWarnings("deprecation")
final InetAddress localHost = SocketCreator.getLocalHost();
Object response;