GEODE-7341: Provide a way to avoid memory lock if over committed (#4210)


diff --git a/geode-core/src/main/java/org/apache/geode/distributed/internal/InternalDistributedSystem.java b/geode-core/src/main/java/org/apache/geode/distributed/internal/InternalDistributedSystem.java
index b7f238c..aed3e68 100644
--- a/geode-core/src/main/java/org/apache/geode/distributed/internal/InternalDistributedSystem.java
+++ b/geode-core/src/main/java/org/apache/geode/distributed/internal/InternalDistributedSystem.java
@@ -136,11 +136,6 @@
 public class InternalDistributedSystem extends DistributedSystem
     implements LogConfigSupplier {
 
-  /**
-   * True if the user is allowed lock when memory resources appear to be overcommitted.
-   */
-  private static final boolean ALLOW_MEMORY_LOCK_WHEN_OVERCOMMITTED =
-      Boolean.getBoolean(GEMFIRE_PREFIX + "Cache.ALLOW_MEMORY_OVERCOMMIT");
   private static final Logger logger = LogService.getLogger();
 
   private static final String DISABLE_MANAGEMENT_PROPERTY =
@@ -149,6 +144,11 @@
   public static final String ALLOW_MULTIPLE_SYSTEMS_PROPERTY =
       GEMFIRE_PREFIX + "ALLOW_MULTIPLE_SYSTEMS";
 
+  public static final String ALLOW_MEMORY_OVERCOMMIT =
+      GEMFIRE_PREFIX + "Cache.ALLOW_MEMORY_OVERCOMMIT";
+  public static final String AVOID_MEMORY_LOCK_WHEN_OVERCOMMIT =
+      GEMFIRE_PREFIX + "Cache.AVOID_MEMORY_LOCK_WHEN_OVERCOMMIT";
+
   /**
    * If auto-reconnect is going on this will hold a reference to it
    */
@@ -174,6 +174,16 @@
   private final StatisticsManager statisticsManager;
   private MetricsService metricsService;
   private final FunctionStatsManager functionStatsManager;
+  /**
+   * True if the user is allowed lock when memory resources appear to be overcommitted.
+   */
+  private final boolean allowMemoryLockWhenOvercommitted =
+      Boolean.getBoolean(ALLOW_MEMORY_OVERCOMMIT);
+  /**
+   * True if memory lock is avoided when memory resources appear to be overcommitted.
+   */
+  private final boolean avoidMemoryLockWhenOvercommitted =
+      Boolean.getBoolean(AVOID_MEMORY_LOCK_WHEN_OVERCOMMIT);
 
   /**
    * The distribution manager that is used to communicate with the distributed system.
@@ -728,28 +738,7 @@
         // included the available memory calculation.
         long avail = LinuxProcFsStatistics.getAvailableMemory(logger);
         long size = offHeapMemorySize + Runtime.getRuntime().totalMemory();
-        if (avail < size) {
-          if (ALLOW_MEMORY_LOCK_WHEN_OVERCOMMITTED) {
-            logger.warn(
-                "System memory appears to be over committed by {} bytes.  You may experience "
-                    + "instability, performance issues, or terminated processes due to the Linux "
-                    + "OOM killer.",
-                size - avail);
-          } else {
-            throw new IllegalStateException(
-                String.format(
-                    "Insufficient free memory (%s) when attempting to lock %s bytes.  Either "
-                        + "reduce the amount of heap or off-heap memory requested or free up "
-                        + "additional system memory.  You may also specify -Dgemfire.Cache"
-                        + ".ALLOW_MEMORY_OVERCOMMIT=true on the command-line to override the "
-                        + "constraint check.",
-                    avail, size));
-          }
-        }
-
-        logger.info("Locking memory. This may take a while...");
-        GemFireCacheImpl.lockMemory();
-        logger.info("Finished locking memory.");
+        lockMemory(avail, size);
       }
 
       try {
@@ -830,6 +819,34 @@
     reconnectAttemptCounter.set(0);
   }
 
+  void lockMemory(long avail, long size) {
+    if (avail < size) {
+      if (avoidMemoryLockWhenOvercommitted) {
+        logger.warn(
+            "System memory appears to be over committed by {} bytes. Memory will not be locked because -D{} is set to true.",
+            size - avail, AVOID_MEMORY_LOCK_WHEN_OVERCOMMIT);
+      } else if (allowMemoryLockWhenOvercommitted) {
+        logger.warn(
+            "System memory appears to be over committed by {} bytes. Memory is locked anyway because -D{} is set to true. You may experience instability, performance issues, or terminated processes due to the Linux OOM killer.",
+            size - avail, ALLOW_MEMORY_OVERCOMMIT);
+        lockMemory();
+      } else {
+        throw new IllegalStateException(
+            String.format(
+                "Insufficient free memory (%s) when attempting to lock %s bytes.  Either reduce the amount of heap or off-heap memory requested or free up additional system memory.  You may also specify -D%s=true on the command-line to override the constraint check.",
+                avail, size, ALLOW_MEMORY_OVERCOMMIT));
+      }
+    } else {
+      lockMemory();
+    }
+  }
+
+  void lockMemory() {
+    logger.info("Locking memory. This may take a while...");
+    GemFireCacheImpl.lockMemory();
+    logger.info("Finished locking memory.");
+  }
+
   private void startSampler() {
     if (statsDisabled) {
       return;
diff --git a/geode-core/src/test/java/org/apache/geode/distributed/internal/InternalDistributedSystemTest.java b/geode-core/src/test/java/org/apache/geode/distributed/internal/InternalDistributedSystemTest.java
new file mode 100644
index 0000000..ad68bb5
--- /dev/null
+++ b/geode-core/src/test/java/org/apache/geode/distributed/internal/InternalDistributedSystemTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.distributed.internal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.catchThrowable;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Properties;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.contrib.java.lang.system.RestoreSystemProperties;
+
+import org.apache.geode.metrics.internal.MetricsService;
+
+
+public class InternalDistributedSystemTest {
+  private MetricsService.Builder builder;
+
+  @Before
+  public void setup() {
+    builder = mock(MetricsService.Builder.class);
+    when(builder.build(any())).thenReturn(mock(MetricsService.class));
+  }
+
+  @Rule
+  public RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties();
+
+  @Test
+  public void lockMemoryAllowedIfAllowMemoryOverCommitIsSet() {
+    System.setProperty(DistributionConfig.GEMFIRE_PREFIX + "Cache.ALLOW_MEMORY_OVERCOMMIT", "true");
+    InternalDistributedSystem system =
+        spy(new InternalDistributedSystem.Builder(new Properties(), builder).build());
+    doNothing().when(system).lockMemory();
+
+    system.lockMemory(100, 200);
+
+    verify(system).lockMemory();
+  }
+
+  @Test
+  public void lockMemoryAvoidedIfAvoidMemoryLockWhenOverCommitIsSet() {
+    System.setProperty(
+        DistributionConfig.GEMFIRE_PREFIX + "Cache.AVOID_MEMORY_LOCK_WHEN_OVERCOMMIT", "true");
+    InternalDistributedSystem system =
+        spy(new InternalDistributedSystem.Builder(new Properties(), builder).build());
+
+    system.lockMemory(100, 200);
+
+    verify(system, never()).lockMemory();
+  }
+
+  @Test
+  public void lockMemoryAvoidedIfAvoidAndAllowMemoryLockWhenOverCommitBothSet() {
+    System.setProperty(DistributionConfig.GEMFIRE_PREFIX + "Cache.ALLOW_MEMORY_OVERCOMMIT", "true");
+    System.setProperty(
+        DistributionConfig.GEMFIRE_PREFIX + "Cache.AVOID_MEMORY_LOCK_WHEN_OVERCOMMIT", "true");
+    InternalDistributedSystem system =
+        spy(new InternalDistributedSystem.Builder(new Properties(), builder).build());
+
+    system.lockMemory(100, 200);
+
+    verify(system, never()).lockMemory();
+  }
+
+
+  @Test
+  public void lockMemoryThrowsIfMemoryOverCommit() {
+    InternalDistributedSystem system =
+        spy(new InternalDistributedSystem.Builder(new Properties(), builder).build());
+
+    Throwable caughtException = catchThrowable(() -> system.lockMemory(100, 200));
+
+    assertThat(caughtException).isInstanceOf(IllegalStateException.class);
+    verify(system, never()).lockMemory();
+  }
+
+  @Test
+  public void locksMemoryIfMemoryNotOverCommit() {
+    InternalDistributedSystem system =
+        spy(new InternalDistributedSystem.Builder(new Properties(), builder).build());
+    doNothing().when(system).lockMemory();
+
+    system.lockMemory(200, 100);
+
+    verify(system).lockMemory();
+  }
+}