From 81a8427ad4b115419560546180d5c08265092a3f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Sep 2025 12:17:39 +0000 Subject: [PATCH 1/3] Initial plan From 2c70e34c9d3b66e658f0b1d410203ecf18c0b248 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Sep 2025 12:37:47 +0000 Subject: [PATCH 2/3] Implement RestPruneCacheAction with FileCache prune API endpoint Co-authored-by: x-INFiN1TY-x <111151372+x-INFiN1TY-x@users.noreply.github.com> --- .../org/opensearch/action/ActionModule.java | 7 +- .../main/java/org/opensearch/node/Node.java | 2 +- .../admin/cluster/RestPruneCacheAction.java | 65 +++++++++++++++++++ .../opensearch/action/ActionModuleTests.java | 7 +- .../cluster/RestPruneCacheActionTests.java | 61 +++++++++++++++++ 5 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 server/src/main/java/org/opensearch/rest/action/admin/cluster/RestPruneCacheAction.java create mode 100644 server/src/test/java/org/opensearch/rest/action/admin/cluster/RestPruneCacheActionTests.java diff --git a/server/src/main/java/org/opensearch/action/ActionModule.java b/server/src/main/java/org/opensearch/action/ActionModule.java index 4755eb8d21999..c57beb6b68463 100644 --- a/server/src/main/java/org/opensearch/action/ActionModule.java +++ b/server/src/main/java/org/opensearch/action/ActionModule.java @@ -330,6 +330,7 @@ import org.opensearch.extensions.rest.RestSendToExtensionAction; import org.opensearch.identity.IdentityService; import org.opensearch.index.seqno.RetentionLeaseActions; +import org.opensearch.index.store.remote.filecache.FileCache; import org.opensearch.indices.SystemIndices; import org.opensearch.persistent.CompletionPersistentTaskAction; import org.opensearch.persistent.RemovePersistentTaskAction; @@ -378,6 +379,7 @@ import org.opensearch.rest.action.admin.cluster.RestNodesStatsAction; import org.opensearch.rest.action.admin.cluster.RestNodesUsageAction; import org.opensearch.rest.action.admin.cluster.RestPendingClusterTasksAction; +import org.opensearch.rest.action.admin.cluster.RestPruneCacheAction; import org.opensearch.rest.action.admin.cluster.RestPutRepositoryAction; import org.opensearch.rest.action.admin.cluster.RestPutStoredScriptAction; import org.opensearch.rest.action.admin.cluster.RestReloadSecureSettingsAction; @@ -839,7 +841,7 @@ private ActionFilters setupActionFilters(List actionPlugins) { ); } - public void initRestHandlers(Supplier nodesInCluster) { + public void initRestHandlers(Supplier nodesInCluster, FileCache fileCache) { List catActions = new ArrayList<>(); List listActions = new ArrayList<>(); Consumer registerHandler = handler -> { @@ -1064,6 +1066,9 @@ public void initRestHandlers(Supplier nodesInCluster) { registerHandler.accept(new RestRestoreRemoteStoreAction()); registerHandler.accept(new RestRemoteStoreMetadataAction()); + // FileCache API + registerHandler.accept(new RestPruneCacheAction(fileCache)); + // pull-based ingestion API registerHandler.accept(new RestPauseIngestionAction()); registerHandler.accept(new RestResumeIngestionAction()); diff --git a/server/src/main/java/org/opensearch/node/Node.java b/server/src/main/java/org/opensearch/node/Node.java index 638af26ffddb9..0339c82f1ff91 100644 --- a/server/src/main/java/org/opensearch/node/Node.java +++ b/server/src/main/java/org/opensearch/node/Node.java @@ -1718,7 +1718,7 @@ protected Node(final Environment initialEnvironment, Collection clas this.namedWriteableRegistry = namedWriteableRegistry; logger.debug("initializing HTTP handlers ..."); - actionModule.initRestHandlers(() -> clusterService.state().nodes()); + actionModule.initRestHandlers(() -> clusterService.state().nodes(), fileCache()); logger.info("initialized"); success = true; diff --git a/server/src/main/java/org/opensearch/rest/action/admin/cluster/RestPruneCacheAction.java b/server/src/main/java/org/opensearch/rest/action/admin/cluster/RestPruneCacheAction.java new file mode 100644 index 0000000000000..f77698cb828a7 --- /dev/null +++ b/server/src/main/java/org/opensearch/rest/action/admin/cluster/RestPruneCacheAction.java @@ -0,0 +1,65 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.rest.action.admin.cluster; + +import org.opensearch.core.rest.RestStatus; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.index.store.remote.filecache.FileCache; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestRequest.Method; +import org.opensearch.transport.client.node.NodeClient; + +import java.io.IOException; +import java.util.List; + +import static java.util.Collections.singletonList; + +/** + * REST action to manually trigger FileCache prune operation. + * This endpoint allows administrators to clear out non-referenced cache entries on demand. + * + * @opensearch.api + */ +public class RestPruneCacheAction extends BaseRestHandler { + + private final FileCache fileCache; + + public RestPruneCacheAction(FileCache fileCache) { + this.fileCache = fileCache; + } + + @Override + public List routes() { + return singletonList(new Route(Method.POST, "/_cache/remote/prune")); + } + + @Override + public String getName() { + return "prune_cache_action"; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + return channel -> { + // Execute the prune operation + long prunedBytes = fileCache.prune(); + + // Build JSON response + XContentBuilder builder = channel.newBuilder(); + builder.startObject(); + builder.field("acknowledged", true); + builder.field("pruned_bytes", prunedBytes); + builder.endObject(); + + channel.sendResponse(new BytesRestResponse(RestStatus.OK, builder)); + }; + } +} \ No newline at end of file diff --git a/server/src/test/java/org/opensearch/action/ActionModuleTests.java b/server/src/test/java/org/opensearch/action/ActionModuleTests.java index 0c1377cb0c6b2..c4121dca6490a 100644 --- a/server/src/test/java/org/opensearch/action/ActionModuleTests.java +++ b/server/src/test/java/org/opensearch/action/ActionModuleTests.java @@ -48,6 +48,7 @@ import org.opensearch.core.action.ActionResponse; import org.opensearch.extensions.ExtensionsManager; import org.opensearch.identity.IdentityService; +import org.opensearch.index.store.remote.filecache.FileCache; import org.opensearch.plugins.ActionPlugin; import org.opensearch.plugins.ActionPlugin.ActionHandler; import org.opensearch.rest.RestChannel; @@ -146,7 +147,7 @@ public void testSetupRestHandlerContainsKnownBuiltin() throws IOException { new IdentityService(Settings.EMPTY, mock(ThreadPool.class), new ArrayList<>()), new ExtensionsManager(Set.of(), new IdentityService(Settings.EMPTY, mock(ThreadPool.class), List.of())) ); - actionModule.initRestHandlers(null); + actionModule.initRestHandlers(null, mock(FileCache.class)); // At this point the easiest way to confirm that a handler is loaded is to try to register another one on top of it and to fail Exception e = expectThrows( IllegalArgumentException.class, @@ -204,7 +205,7 @@ public String getName() { null, null ); - Exception e = expectThrows(IllegalArgumentException.class, () -> actionModule.initRestHandlers(null)); + Exception e = expectThrows(IllegalArgumentException.class, () -> actionModule.initRestHandlers(null, mock(FileCache.class))); assertThat(e.getMessage(), startsWith("Cannot replace existing handler for [/] for method: GET")); } finally { threadPool.shutdown(); @@ -255,7 +256,7 @@ public List getRestHandlers( null, null ); - actionModule.initRestHandlers(null); + actionModule.initRestHandlers(null, mock(FileCache.class)); // At this point the easiest way to confirm that a handler is loaded is to try to register another one on top of it and to fail Exception e = expectThrows( IllegalArgumentException.class, diff --git a/server/src/test/java/org/opensearch/rest/action/admin/cluster/RestPruneCacheActionTests.java b/server/src/test/java/org/opensearch/rest/action/admin/cluster/RestPruneCacheActionTests.java new file mode 100644 index 0000000000000..2b5b6ce4636e2 --- /dev/null +++ b/server/src/test/java/org/opensearch/rest/action/admin/cluster/RestPruneCacheActionTests.java @@ -0,0 +1,61 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.rest.action.admin.cluster; + +import org.opensearch.core.rest.RestStatus; +import org.opensearch.index.store.remote.filecache.FileCache; +import org.opensearch.rest.RestRequest; +import org.opensearch.test.rest.FakeRestRequest; +import org.opensearch.test.rest.RestActionTestCase; +import org.opensearch.threadpool.TestThreadPool; +import org.opensearch.threadpool.ThreadPool; +import org.junit.After; +import org.junit.Before; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RestPruneCacheActionTests extends RestActionTestCase { + + private ThreadPool threadPool; + + @Before + public void setup() { + threadPool = new TestThreadPool(this.getClass().getSimpleName()); + } + + @After + public void tearDown() throws Exception { + super.tearDown(); + terminate(threadPool); + } + + public void testRoutes() { + RestPruneCacheAction action = new RestPruneCacheAction(mock(FileCache.class)); + assertEquals(1, action.routes().size()); + assertEquals(RestRequest.Method.POST, action.routes().get(0).getMethod()); + assertEquals("/_cache/remote/prune", action.routes().get(0).getPath()); + } + + public void testGetName() { + RestPruneCacheAction action = new RestPruneCacheAction(mock(FileCache.class)); + assertEquals("prune_cache_action", action.getName()); + } + + public void testPrepareRequest() throws Exception { + FileCache mockFileCache = mock(FileCache.class); + when(mockFileCache.prune()).thenReturn(12345678L); + + RestPruneCacheAction action = new RestPruneCacheAction(mockFileCache); + RestRequest request = new FakeRestRequest(); + + // Test that the action prepares correctly without throwing exceptions + assertNotNull(action.prepareRequest(request, null)); + } +} \ No newline at end of file From 02abc042c537718a160690a901ae2e0631a188ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Sep 2025 13:57:49 +0000 Subject: [PATCH 3/3] Implement Service Locator Pattern for RestPruneCacheAction Co-authored-by: x-INFiN1TY-x <111151372+x-INFiN1TY-x@users.noreply.github.com> --- .../org/opensearch/action/ActionModule.java | 5 +++-- .../src/main/java/org/opensearch/node/Node.java | 2 +- .../admin/cluster/RestPruneCacheAction.java | 7 +++++-- .../opensearch/action/ActionModuleTests.java | 7 ++++--- .../cluster/RestPruneCacheActionTests.java | 17 +++++++++++++---- 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/org/opensearch/action/ActionModule.java b/server/src/main/java/org/opensearch/action/ActionModule.java index c57beb6b68463..416896076f9fd 100644 --- a/server/src/main/java/org/opensearch/action/ActionModule.java +++ b/server/src/main/java/org/opensearch/action/ActionModule.java @@ -331,6 +331,7 @@ import org.opensearch.identity.IdentityService; import org.opensearch.index.seqno.RetentionLeaseActions; import org.opensearch.index.store.remote.filecache.FileCache; +import org.opensearch.node.Node; import org.opensearch.indices.SystemIndices; import org.opensearch.persistent.CompletionPersistentTaskAction; import org.opensearch.persistent.RemovePersistentTaskAction; @@ -841,7 +842,7 @@ private ActionFilters setupActionFilters(List actionPlugins) { ); } - public void initRestHandlers(Supplier nodesInCluster, FileCache fileCache) { + public void initRestHandlers(Supplier nodesInCluster, Node node) { List catActions = new ArrayList<>(); List listActions = new ArrayList<>(); Consumer registerHandler = handler -> { @@ -1067,7 +1068,7 @@ public void initRestHandlers(Supplier nodesInCluster, FileCache registerHandler.accept(new RestRemoteStoreMetadataAction()); // FileCache API - registerHandler.accept(new RestPruneCacheAction(fileCache)); + registerHandler.accept(new RestPruneCacheAction(node)); // pull-based ingestion API registerHandler.accept(new RestPauseIngestionAction()); diff --git a/server/src/main/java/org/opensearch/node/Node.java b/server/src/main/java/org/opensearch/node/Node.java index 0339c82f1ff91..4a2e8bb70af8e 100644 --- a/server/src/main/java/org/opensearch/node/Node.java +++ b/server/src/main/java/org/opensearch/node/Node.java @@ -1718,7 +1718,7 @@ protected Node(final Environment initialEnvironment, Collection clas this.namedWriteableRegistry = namedWriteableRegistry; logger.debug("initializing HTTP handlers ..."); - actionModule.initRestHandlers(() -> clusterService.state().nodes(), fileCache()); + actionModule.initRestHandlers(() -> clusterService.state().nodes(), this); logger.info("initialized"); success = true; diff --git a/server/src/main/java/org/opensearch/rest/action/admin/cluster/RestPruneCacheAction.java b/server/src/main/java/org/opensearch/rest/action/admin/cluster/RestPruneCacheAction.java index f77698cb828a7..522ceb1df5937 100644 --- a/server/src/main/java/org/opensearch/rest/action/admin/cluster/RestPruneCacheAction.java +++ b/server/src/main/java/org/opensearch/rest/action/admin/cluster/RestPruneCacheAction.java @@ -11,6 +11,7 @@ import org.opensearch.core.rest.RestStatus; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.index.store.remote.filecache.FileCache; +import org.opensearch.node.Node; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestRequest; @@ -32,8 +33,10 @@ public class RestPruneCacheAction extends BaseRestHandler { private final FileCache fileCache; - public RestPruneCacheAction(FileCache fileCache) { - this.fileCache = fileCache; + // The constructor now accepts the 'Node' object as the Service Locator. + public RestPruneCacheAction(Node node) { + // The handler uses the locator to retrieve the specific service it needs. + this.fileCache = node.fileCache(); } @Override diff --git a/server/src/test/java/org/opensearch/action/ActionModuleTests.java b/server/src/test/java/org/opensearch/action/ActionModuleTests.java index c4121dca6490a..b055ca8790cd2 100644 --- a/server/src/test/java/org/opensearch/action/ActionModuleTests.java +++ b/server/src/test/java/org/opensearch/action/ActionModuleTests.java @@ -49,6 +49,7 @@ import org.opensearch.extensions.ExtensionsManager; import org.opensearch.identity.IdentityService; import org.opensearch.index.store.remote.filecache.FileCache; +import org.opensearch.node.Node; import org.opensearch.plugins.ActionPlugin; import org.opensearch.plugins.ActionPlugin.ActionHandler; import org.opensearch.rest.RestChannel; @@ -147,7 +148,7 @@ public void testSetupRestHandlerContainsKnownBuiltin() throws IOException { new IdentityService(Settings.EMPTY, mock(ThreadPool.class), new ArrayList<>()), new ExtensionsManager(Set.of(), new IdentityService(Settings.EMPTY, mock(ThreadPool.class), List.of())) ); - actionModule.initRestHandlers(null, mock(FileCache.class)); + actionModule.initRestHandlers(null, mock(Node.class)); // At this point the easiest way to confirm that a handler is loaded is to try to register another one on top of it and to fail Exception e = expectThrows( IllegalArgumentException.class, @@ -205,7 +206,7 @@ public String getName() { null, null ); - Exception e = expectThrows(IllegalArgumentException.class, () -> actionModule.initRestHandlers(null, mock(FileCache.class))); + Exception e = expectThrows(IllegalArgumentException.class, () -> actionModule.initRestHandlers(null, mock(Node.class))); assertThat(e.getMessage(), startsWith("Cannot replace existing handler for [/] for method: GET")); } finally { threadPool.shutdown(); @@ -256,7 +257,7 @@ public List getRestHandlers( null, null ); - actionModule.initRestHandlers(null, mock(FileCache.class)); + actionModule.initRestHandlers(null, mock(Node.class)); // At this point the easiest way to confirm that a handler is loaded is to try to register another one on top of it and to fail Exception e = expectThrows( IllegalArgumentException.class, diff --git a/server/src/test/java/org/opensearch/rest/action/admin/cluster/RestPruneCacheActionTests.java b/server/src/test/java/org/opensearch/rest/action/admin/cluster/RestPruneCacheActionTests.java index 2b5b6ce4636e2..2704cb4dabc90 100644 --- a/server/src/test/java/org/opensearch/rest/action/admin/cluster/RestPruneCacheActionTests.java +++ b/server/src/test/java/org/opensearch/rest/action/admin/cluster/RestPruneCacheActionTests.java @@ -10,6 +10,7 @@ import org.opensearch.core.rest.RestStatus; import org.opensearch.index.store.remote.filecache.FileCache; +import org.opensearch.node.Node; import org.opensearch.rest.RestRequest; import org.opensearch.test.rest.FakeRestRequest; import org.opensearch.test.rest.RestActionTestCase; @@ -37,22 +38,30 @@ public void tearDown() throws Exception { } public void testRoutes() { - RestPruneCacheAction action = new RestPruneCacheAction(mock(FileCache.class)); + Node mockNode = mock(Node.class); + RestPruneCacheAction action = new RestPruneCacheAction(mockNode); assertEquals(1, action.routes().size()); assertEquals(RestRequest.Method.POST, action.routes().get(0).getMethod()); assertEquals("/_cache/remote/prune", action.routes().get(0).getPath()); } public void testGetName() { - RestPruneCacheAction action = new RestPruneCacheAction(mock(FileCache.class)); + Node mockNode = mock(Node.class); + RestPruneCacheAction action = new RestPruneCacheAction(mockNode); assertEquals("prune_cache_action", action.getName()); } public void testPrepareRequest() throws Exception { + // 1. Mock the FileCache service FileCache mockFileCache = mock(FileCache.class); - when(mockFileCache.prune()).thenReturn(12345678L); + final long bytesPruned = 12345678L; + when(mockFileCache.prune()).thenReturn(bytesPruned); - RestPruneCacheAction action = new RestPruneCacheAction(mockFileCache); + // 2. Mock the Node (Service Locator) to return the mocked service + Node mockNode = mock(Node.class); + when(mockNode.fileCache()).thenReturn(mockFileCache); + + RestPruneCacheAction action = new RestPruneCacheAction(mockNode); RestRequest request = new FakeRestRequest(); // Test that the action prepares correctly without throwing exceptions