From 75e04e49429fd8b7c1a384bd108efb11c558b10b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Vl=C4=8Dek?= <lukas.vlcek@aiven.io>
Date: Mon, 8 Jul 2024 22:54:02 +0200
Subject: [PATCH] Deprecating static getFSInfo method
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Deprecating `FsInfo.Path#getFSInfo(NodePath nodePath)` method because without
FileCache instance it produces incomplete Path object that does not have initialized
some file cache related variables.

WIP: We still need better tests running multinode cluster with FileCache.

Signed-off-by: Lukáš Vlček <lukas.vlcek@aiven.io>
---
 .../test/nodes.stats/70_file_cache.yml        |  4 +-
 .../org/opensearch/env/NodeEnvironment.java   |  5 ++-
 .../store/remote/filecache/FileCache.java     | 25 +++++++++++
 .../org/opensearch/monitor/fs/FsProbe.java    | 32 ++++++++++++--
 .../opensearch/monitor/fs/FsProbeTests.java   | 42 ++++++++++---------
 5 files changed, 81 insertions(+), 27 deletions(-)

diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/nodes.stats/70_file_cache.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/nodes.stats/70_file_cache.yml
index 2282b27a7cded..9ed45d7a4f396 100644
--- a/rest-api-spec/src/main/resources/rest-api-spec/test/nodes.stats/70_file_cache.yml
+++ b/rest-api-spec/src/main/resources/rest-api-spec/test/nodes.stats/70_file_cache.yml
@@ -5,8 +5,8 @@
 ---
 "File Cache stats":
   - skip:
-      version: " - 2.16.99"
-      reason: "file cache statistics fields were added in 2.7 (#6485) but the fix (#13232) was merged after 2.16.0"
+      version: " - 2.15.99"
+      reason: "file cache statistics fields were added in 2.7 (#6485) but the fix (#13232) was merged in 2.16.0"
       features: [arbitrary_key, node_selector]
 
   - do:
diff --git a/server/src/main/java/org/opensearch/env/NodeEnvironment.java b/server/src/main/java/org/opensearch/env/NodeEnvironment.java
index e13e5e5f47cf9..b127400ba4712 100644
--- a/server/src/main/java/org/opensearch/env/NodeEnvironment.java
+++ b/server/src/main/java/org/opensearch/env/NodeEnvironment.java
@@ -71,6 +71,7 @@
 import org.opensearch.index.IndexSettings;
 import org.opensearch.index.shard.ShardPath;
 import org.opensearch.index.store.FsDirectoryFactory;
+import org.opensearch.index.store.remote.filecache.FileCache;
 import org.opensearch.monitor.fs.FsInfo;
 import org.opensearch.monitor.fs.FsProbe;
 import org.opensearch.monitor.jvm.JvmInfo;
@@ -424,7 +425,7 @@ private void maybeLogPathDetails() throws IOException {
             for (NodePath nodePath : nodePaths) {
                 sb.append('\n').append(" -> ").append(nodePath.path.toAbsolutePath());
 
-                FsInfo.Path fsPath = FsProbe.getFSInfo(nodePath);
+                FsInfo.Path fsPath = FsProbe.getFSInfo(nodePath, FileCache.NOOP_FILE_CACHE);
                 sb.append(", free_space [")
                     .append(fsPath.getFree())
                     .append("], usable_space [")
@@ -443,7 +444,7 @@ private void maybeLogPathDetails() throws IOException {
             Set<String> allTypes = new HashSet<>();
             Set<String> allMounts = new HashSet<>();
             for (NodePath nodePath : nodePaths) {
-                FsInfo.Path fsPath = FsProbe.getFSInfo(nodePath);
+                FsInfo.Path fsPath = FsProbe.getFSInfo(nodePath, FileCache.NOOP_FILE_CACHE);
                 String mount = fsPath.getMount();
                 if (allMounts.contains(mount) == false) {
                     allMounts.add(mount);
diff --git a/server/src/main/java/org/opensearch/index/store/remote/filecache/FileCache.java b/server/src/main/java/org/opensearch/index/store/remote/filecache/FileCache.java
index a8aa33a977cb8..2ca39e09c4518 100644
--- a/server/src/main/java/org/opensearch/index/store/remote/filecache/FileCache.java
+++ b/server/src/main/java/org/opensearch/index/store/remote/filecache/FileCache.java
@@ -14,6 +14,8 @@
 import org.opensearch.common.annotation.PublicApi;
 import org.opensearch.core.common.breaker.CircuitBreaker;
 import org.opensearch.core.common.breaker.CircuitBreakingException;
+import org.opensearch.core.common.breaker.NoopCircuitBreaker;
+import org.opensearch.env.NodeEnvironment;
 import org.opensearch.index.store.remote.utils.cache.CacheUsage;
 import org.opensearch.index.store.remote.utils.cache.RefCountedCache;
 import org.opensearch.index.store.remote.utils.cache.SegmentedCache;
@@ -49,6 +51,29 @@
  */
 @PublicApi(since = "2.7.0")
 public class FileCache implements RefCountedCache<Path, CachedIndexInput> {
+
+    /**
+     * A minimalistic instance of the File Cache (FC) which is not useful for any real caching task
+     * but is useful as a placeholder in API calls that require FC instance, for example
+     * {@link org.opensearch.monitor.fs.FsProbe#getFSInfo(NodeEnvironment.NodePath, FileCache)}.
+     * <br>
+     * REMEMBER to use a valid FC instance if the output of such API call needs to reflect its state.
+     *
+     * @opensearch.internal
+     */
+    public static FileCache NOOP_FILE_CACHE = null;
+
+    // Perhaps we shall create a new class with static instance like NoopFileCache.INSTANCE ???
+    static {
+        int CONCURRENCY_LEVEL = 1;
+        int CAPACITY = 0;
+        NOOP_FILE_CACHE = FileCacheFactory.createConcurrentLRUFileCache(
+            CAPACITY,
+            CONCURRENCY_LEVEL,
+            new NoopCircuitBreaker(CircuitBreaker.REQUEST)
+        );
+    }
+
     private static final Logger logger = LogManager.getLogger(FileCache.class);
     private final SegmentedCache<Path, CachedIndexInput> theCache;
 
diff --git a/server/src/main/java/org/opensearch/monitor/fs/FsProbe.java b/server/src/main/java/org/opensearch/monitor/fs/FsProbe.java
index c715874c93689..d17fa9fbec77e 100644
--- a/server/src/main/java/org/opensearch/monitor/fs/FsProbe.java
+++ b/server/src/main/java/org/opensearch/monitor/fs/FsProbe.java
@@ -37,6 +37,7 @@
 import org.apache.logging.log4j.message.ParameterizedMessage;
 import org.apache.lucene.util.Constants;
 import org.opensearch.common.SuppressForbidden;
+import org.opensearch.common.annotation.DeprecatedApi;
 import org.opensearch.common.collect.Tuple;
 import org.opensearch.common.io.PathUtils;
 import org.opensearch.core.common.unit.ByteSizeValue;
@@ -77,7 +78,7 @@ public FsInfo stats(FsInfo previous) throws IOException {
         NodePath[] dataLocations = nodeEnv.nodePaths();
         FsInfo.Path[] paths = new FsInfo.Path[dataLocations.length];
         for (int i = 0; i < dataLocations.length; i++) {
-            paths[i] = getFSInfo(dataLocations[i]);
+            paths[i] = getFSInfo(dataLocations[i], fileCache == null ? FileCache.NOOP_FILE_CACHE : fileCache);
             if (fileCache != null && dataLocations[i].fileCacheReservedSize.compareTo(ByteSizeValue.ZERO) >= 0) {
                 paths[i].fileCacheReserved = adjustForHugeFilesystems(dataLocations[i].fileCacheReservedSize.getBytes());
                 paths[i].fileCacheUtilized = adjustForHugeFilesystems(fileCache.usage().usage());
@@ -203,6 +204,14 @@ static long adjustForHugeFilesystems(long bytes) {
         return bytes;
     }
 
+    /**
+     * Retrieves information about the path associated with the given node path.
+     *
+     * @deprecated This method has been deprecated since version 2.16.0 and will be removed in version 3.0.0, use {@link #getFSInfo(NodePath, FileCache)} instead.
+     * @see #getFSInfo(NodePath, FileCache)
+     */
+    @Deprecated
+    @DeprecatedApi(since = "2.16.0", forRemoval = "3.0.0")
     public static FsInfo.Path getFSInfo(NodePath nodePath) throws IOException {
         FsInfo.Path fsPath = new FsInfo.Path();
         fsPath.path = nodePath.path.toString();
@@ -214,14 +223,29 @@ public static FsInfo.Path getFSInfo(NodePath nodePath) throws IOException {
         fsPath.free = adjustForHugeFilesystems(nodePath.fileStore.getUnallocatedSpace());
         fsPath.available = adjustForHugeFilesystems(nodePath.fileStore.getUsableSpace());
         fsPath.fileCacheReserved = adjustForHugeFilesystems(nodePath.fileCacheReservedSize.getBytes());
-        // fsPath.fileCacheUtilized = adjustForHugeFilesystems(...);
-        // We can not do this ^^ here because information about utilization of file cache is hold by FileCache
-        // which is not accessible here and since this method is static we can not assume relevant context.
+        // The fsPath.fileCacheUtilized cannot be calculated because the information about file cache utilization
+        // is hold by FileCache which is not available in this context.
         fsPath.type = nodePath.fileStore.type();
         fsPath.mount = nodePath.fileStore.toString();
         return fsPath;
     }
 
+    /**
+     * Retrieves information about the path associated with the given node path.
+     *
+     * @param nodePath The node path to retrieve the path information for.
+     * @param fileCache The FileCache object used for adjusting the file cache utilization.
+     * @return The path information for the given node path.
+     * @throws IOException If an I/O error occurs while retrieving the path information.
+     */
+    public static FsInfo.Path getFSInfo(NodePath nodePath, FileCache fileCache) throws IOException {
+        assert fileCache != null : "FileCache cannot be null";
+        FsInfo.Path fsPath = getFSInfo(nodePath);
+        fsPath.fileCacheUtilized = adjustForHugeFilesystems(fileCache.usage().usage());
+        fsPath.available -= (fsPath.fileCacheReserved - fsPath.fileCacheUtilized);
+        return fsPath;
+    }
+
     public static long getTotalSize(NodePath nodePath) throws IOException {
         return adjustForHugeFilesystems(nodePath.fileStore.getTotalSpace());
     }
diff --git a/server/src/test/java/org/opensearch/monitor/fs/FsProbeTests.java b/server/src/test/java/org/opensearch/monitor/fs/FsProbeTests.java
index ab9b864b5d93d..5f8691129a235 100644
--- a/server/src/test/java/org/opensearch/monitor/fs/FsProbeTests.java
+++ b/server/src/test/java/org/opensearch/monitor/fs/FsProbeTests.java
@@ -72,18 +72,7 @@ public class FsProbeTests extends OpenSearchTestCase {
     public void testFsInfo() throws IOException {
 
         try (NodeEnvironment env = newNodeEnvironment()) {
-            // Question: Shall we expose a public method in FileCacheTests to enable creation of FileCache
-            // so that it can be used by other testing classes?
-            int CONCURRENCY_LEVEL = 16; // not important
-            int CAPACITY = 1 * 1024; // not important
-            FileCache fileCache = FileCacheFactory.createConcurrentLRUFileCache(
-                CAPACITY,
-                CONCURRENCY_LEVEL,
-                new NoopCircuitBreaker(CircuitBreaker.REQUEST)
-            );
-            // We need to pass a real FileCache object to FsProbe ctor to have it safeguard "path.fileCacheUtilized" values properly!
-            FsProbe probe = new FsProbe(env, fileCache);
-
+            FsProbe probe = new FsProbe(env, FileCache.NOOP_FILE_CACHE);
             FsInfo stats = probe.stats(null);
             assertNotNull(stats);
             assertThat(stats.getTimestamp(), greaterThan(0L));
@@ -120,6 +109,9 @@ public void testFsInfo() throws IOException {
             assertThat(total.total, greaterThan(0L));
             assertThat(total.free, greaterThan(0L));
             assertThat(total.available, greaterThan(0L));
+            // Total file cache (sum over all "paths"):
+            assertThat(total.fileCacheReserved, equalTo(0L));
+            assertThat(total.fileCacheUtilized, equalTo(0L));
 
             // The convention for "total" Path object is that some fields are not set
             // which means they will not be included in output of toXContent method.
@@ -127,10 +119,6 @@ public void testFsInfo() throws IOException {
             assertNull(total.mount);
             assertNull(total.type);
 
-            // Total file cache (sum over all "paths"):
-            assertEquals(total.getFileCacheReserved().getBytes(), 0);
-            assertEquals(total.getFileCacheUtilized().getBytes(), 0);
-
             for (FsInfo.Path path : stats) {
                 assertNotNull(path);
                 assertThat(path.getPath(), is(not(emptyOrNullString())));
@@ -439,9 +427,25 @@ List<String> readProcDiskStats() throws IOException {
 
     public void testAdjustForHugeFilesystems() throws Exception {
         NodePath np = new FakeNodePath(createTempDir());
-        assertThat(FsProbe.getFSInfo(np).total, greaterThanOrEqualTo(0L));
-        assertThat(FsProbe.getFSInfo(np).free, greaterThanOrEqualTo(0L));
-        assertThat(FsProbe.getFSInfo(np).available, greaterThanOrEqualTo(0L));
+
+        FsInfo.Path path = FsProbe.getFSInfo(np, FileCache.NOOP_FILE_CACHE);
+        assertThat(path.total, greaterThanOrEqualTo(0L));
+        assertThat(path.free, greaterThanOrEqualTo(0L));
+        assertThat(path.available, greaterThanOrEqualTo(0L));
+        assertThat(path.fileCacheReserved, greaterThanOrEqualTo(0L));
+        assertThat(path.fileCacheUtilized, greaterThanOrEqualTo(0L));
+
+        /** The following test demonstrates that a call to {@link FsProbe#getFSInfo(NodePath)}
+         * leaves the file cache utilization value uninitialized which can have unexpected effects.
+         * Use of that method was deprecated and replaced by {@link FsProbe#getFSInfo(NodePath, FileCache)}.
+         * {@see https://github.com/opensearch-project/OpenSearch/pull/13232}
+         */
+        path = FsProbe.getFSInfo(np);
+        assertThat(path.total, greaterThanOrEqualTo(0L));
+        assertThat(path.free, greaterThanOrEqualTo(0L));
+        assertThat(path.available, greaterThanOrEqualTo(0L));
+        assertThat(path.fileCacheReserved, greaterThanOrEqualTo(0L));
+        assertThat(path.fileCacheUtilized, greaterThanOrEqualTo(-1L)); // <-- !!
     }
 
     static class FakeNodePath extends NodeEnvironment.NodePath {