Skip to content

Commit 05a74c4

Browse files
committed
feat(hunspell): Add cache info and invalidation REST API endpoints
- Add GET /_hunspell/cache endpoint for viewing cached dictionary keys (cluster:monitor/hunspell/cache) - Add POST /_hunspell/cache/_invalidate endpoint for cache invalidation (cluster:admin/hunspell/cache/invalidate) - Support invalidation by package_id, locale, cache_key, or invalidate_all - Add TransportHunspellCacheInfoAction and TransportHunspellCacheInvalidateAction - Consistent response schema with all fields always present - Register actions and REST handler in ActionModule - Add comprehensive unit tests, REST handler tests, and integration test Depends on #20840 Signed-off-by: Ayush Sharma <118544643+shayush622@users.noreply.github.com> Signed-off-by: shayush622 <ayush5267@gmail.com>
1 parent c77e078 commit 05a74c4

File tree

16 files changed

+1219
-3
lines changed

16 files changed

+1219
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
4040
- Added support of WarmerRefreshListener in NRTReplicationEngine to trigger warmer after replication on replica shards ([#20650](https://github.com/opensearch-project/OpenSearch/pull/20650))
4141
- WLM group custom search settings - groundwork and timeout ([#20536](https://github.com/opensearch-project/OpenSearch/issues/20536))
4242
- Add ref_path support for package-based hunspell dictionary loading ([#20840](https://github.com/opensearch-project/OpenSearch/pull/20840))
43+
- Add hunspell cache info and invalidation REST API endpoints ([#20841](https://github.com/opensearch-project/OpenSearch/pull/20841))
4344

4445
### Changed
4546
- Make telemetry `Tags` immutable ([#20788](https://github.com/opensearch-project/OpenSearch/pull/20788))
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.action.admin.indices.cache.hunspell;
10+
11+
import org.opensearch.common.settings.Settings;
12+
import org.opensearch.indices.analysis.HunspellService;
13+
import org.opensearch.test.OpenSearchIntegTestCase;
14+
import org.opensearch.test.OpenSearchIntegTestCase.ClusterScope;
15+
16+
import java.io.IOException;
17+
import java.nio.charset.StandardCharsets;
18+
import java.nio.file.Files;
19+
import java.nio.file.Path;
20+
21+
import static org.hamcrest.Matchers.equalTo;
22+
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
23+
24+
/**
25+
* Integration tests for hunspell cache info and invalidation APIs.
26+
*
27+
* <p>Tests the full REST→Transport→HunspellService flow on a real cluster:
28+
* <ul>
29+
* <li>GET /_hunspell/cache (HunspellCacheInfoAction)</li>
30+
* <li>POST /_hunspell/cache/_invalidate (HunspellCacheInvalidateAction)</li>
31+
* </ul>
32+
*/
33+
@ClusterScope(scope = OpenSearchIntegTestCase.Scope.TEST)
34+
public class HunspellCacheIT extends OpenSearchIntegTestCase {
35+
36+
@Override
37+
protected Settings nodeSettings(int nodeOrdinal) {
38+
// Enable lazy loading so dictionaries are only loaded on-demand
39+
return Settings.builder().put(super.nodeSettings(nodeOrdinal)).put(HunspellService.HUNSPELL_LAZY_LOAD.getKey(), true).build();
40+
}
41+
42+
/**
43+
* Sets up hunspell dictionary files in the node's config directory.
44+
* Creates both traditional and package-based dictionaries.
45+
*/
46+
private void setupDictionaries(Path configDir) throws IOException {
47+
// Traditional dictionary: config/hunspell/en_US/
48+
Path traditionalDir = configDir.resolve("hunspell").resolve("en_US");
49+
Files.createDirectories(traditionalDir);
50+
Files.write(traditionalDir.resolve("en_US.aff"), "SET UTF-8\n".getBytes(StandardCharsets.UTF_8));
51+
Files.write(traditionalDir.resolve("en_US.dic"), "1\nhello\n".getBytes(StandardCharsets.UTF_8));
52+
53+
// Package-based dictionary: config/packages/test-pkg/hunspell/en_US/
54+
Path packageDir = configDir.resolve("packages").resolve("test-pkg").resolve("hunspell").resolve("en_US");
55+
Files.createDirectories(packageDir);
56+
Files.write(packageDir.resolve("en_US.aff"), "SET UTF-8\n".getBytes(StandardCharsets.UTF_8));
57+
Files.write(packageDir.resolve("en_US.dic"), "1\nworld\n".getBytes(StandardCharsets.UTF_8));
58+
}
59+
60+
// ==================== Cache Info Tests ====================
61+
62+
public void testCacheInfoReturnsEmptyWhenNoDictionariesLoaded() {
63+
HunspellCacheInfoResponse response = client().execute(HunspellCacheInfoAction.INSTANCE, new HunspellCacheInfoRequest()).actionGet();
64+
65+
assertThat(response.getTotalCachedCount(), equalTo(0));
66+
assertThat(response.getPackageBasedCount(), equalTo(0));
67+
assertThat(response.getTraditionalLocaleCount(), equalTo(0));
68+
assertTrue(response.getPackageBasedKeys().isEmpty());
69+
assertTrue(response.getTraditionalLocaleKeys().isEmpty());
70+
}
71+
72+
// ==================== Cache Invalidation Tests ====================
73+
74+
public void testInvalidateAllOnEmptyCache() {
75+
HunspellCacheInvalidateRequest request = new HunspellCacheInvalidateRequest();
76+
request.setInvalidateAll(true);
77+
78+
HunspellCacheInvalidateResponse response = client().execute(HunspellCacheInvalidateAction.INSTANCE, request).actionGet();
79+
80+
assertTrue(response.isAcknowledged());
81+
assertThat(response.getInvalidatedCount(), equalTo(0));
82+
}
83+
84+
public void testInvalidateByPackageIdOnEmptyCache() {
85+
HunspellCacheInvalidateRequest request = new HunspellCacheInvalidateRequest();
86+
request.setPackageId("nonexistent-pkg");
87+
88+
HunspellCacheInvalidateResponse response = client().execute(HunspellCacheInvalidateAction.INSTANCE, request).actionGet();
89+
90+
assertTrue(response.isAcknowledged());
91+
assertThat(response.getInvalidatedCount(), equalTo(0));
92+
assertThat(response.getPackageId(), equalTo("nonexistent-pkg"));
93+
}
94+
95+
public void testInvalidateByCacheKeyOnEmptyCache() {
96+
HunspellCacheInvalidateRequest request = new HunspellCacheInvalidateRequest();
97+
request.setCacheKey("nonexistent-key");
98+
99+
HunspellCacheInvalidateResponse response = client().execute(HunspellCacheInvalidateAction.INSTANCE, request).actionGet();
100+
101+
assertTrue(response.isAcknowledged());
102+
assertThat(response.getInvalidatedCount(), equalTo(0));
103+
}
104+
105+
// ==================== Request Validation Tests ====================
106+
107+
public void testInvalidateRequestValidationFailsWithNoParameters() {
108+
HunspellCacheInvalidateRequest request = new HunspellCacheInvalidateRequest();
109+
// No parameters set — should fail validation
110+
111+
assertNotNull(request.validate());
112+
}
113+
114+
public void testInvalidateRequestValidationFailsWithConflictingParameters() {
115+
HunspellCacheInvalidateRequest request = new HunspellCacheInvalidateRequest();
116+
request.setInvalidateAll(true);
117+
request.setPackageId("some-pkg");
118+
119+
assertNotNull(request.validate());
120+
}
121+
122+
public void testInvalidateRequestValidationFailsWithEmptyPackageId() {
123+
HunspellCacheInvalidateRequest request = new HunspellCacheInvalidateRequest();
124+
request.setPackageId(" ");
125+
126+
assertNotNull(request.validate());
127+
}
128+
129+
public void testInvalidateRequestValidationPassesWithPackageIdOnly() {
130+
HunspellCacheInvalidateRequest request = new HunspellCacheInvalidateRequest();
131+
request.setPackageId("valid-pkg");
132+
133+
assertNull(request.validate());
134+
}
135+
136+
public void testInvalidateRequestValidationPassesWithPackageIdAndLocale() {
137+
HunspellCacheInvalidateRequest request = new HunspellCacheInvalidateRequest();
138+
request.setPackageId("valid-pkg");
139+
request.setLocale("en_US");
140+
141+
assertNull(request.validate());
142+
}
143+
144+
public void testInvalidateRequestValidationFailsWithLocaleWithoutPackageId() {
145+
HunspellCacheInvalidateRequest request = new HunspellCacheInvalidateRequest();
146+
request.setLocale("en_US");
147+
148+
assertNotNull(request.validate());
149+
}
150+
151+
// ==================== Response Schema Tests ====================
152+
153+
public void testInvalidateResponseAlwaysIncludesAllFields() {
154+
HunspellCacheInvalidateRequest request = new HunspellCacheInvalidateRequest();
155+
request.setInvalidateAll(true);
156+
157+
HunspellCacheInvalidateResponse response = client().execute(HunspellCacheInvalidateAction.INSTANCE, request).actionGet();
158+
159+
// Response should always include these fields for consistent schema
160+
assertTrue(response.isAcknowledged());
161+
assertThat(response.getInvalidatedCount(), greaterThanOrEqualTo(0));
162+
// Null fields should still be accessible (consistent schema)
163+
assertNull(response.getPackageId());
164+
assertNull(response.getLocale());
165+
assertNull(response.getCacheKey());
166+
}
167+
168+
public void testCacheInfoResponseSchema() {
169+
HunspellCacheInfoResponse response = client().execute(HunspellCacheInfoAction.INSTANCE, new HunspellCacheInfoRequest()).actionGet();
170+
171+
// Verify response schema has all expected fields
172+
assertThat(response.getTotalCachedCount(), greaterThanOrEqualTo(0));
173+
assertThat(response.getPackageBasedCount(), greaterThanOrEqualTo(0));
174+
assertThat(response.getTraditionalLocaleCount(), greaterThanOrEqualTo(0));
175+
assertNotNull(response.getPackageBasedKeys());
176+
assertNotNull(response.getTraditionalLocaleKeys());
177+
}
178+
}

server/src/main/java/org/opensearch/action/ActionModule.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@
138138
import org.opensearch.action.admin.indices.analyze.TransportAnalyzeAction;
139139
import org.opensearch.action.admin.indices.cache.clear.ClearIndicesCacheAction;
140140
import org.opensearch.action.admin.indices.cache.clear.TransportClearIndicesCacheAction;
141+
import org.opensearch.action.admin.indices.cache.hunspell.HunspellCacheInfoAction;
142+
import org.opensearch.action.admin.indices.cache.hunspell.HunspellCacheInvalidateAction;
143+
import org.opensearch.action.admin.indices.cache.hunspell.TransportHunspellCacheInfoAction;
144+
import org.opensearch.action.admin.indices.cache.hunspell.TransportHunspellCacheInvalidateAction;
141145
import org.opensearch.action.admin.indices.close.CloseIndexAction;
142146
import org.opensearch.action.admin.indices.close.TransportCloseIndexAction;
143147
import org.opensearch.action.admin.indices.create.AutoCreateAction;
@@ -333,6 +337,7 @@
333337
import org.opensearch.identity.IdentityService;
334338
import org.opensearch.index.seqno.RetentionLeaseActions;
335339
import org.opensearch.indices.SystemIndices;
340+
import org.opensearch.indices.analysis.HunspellService;
336341
import org.opensearch.persistent.CompletionPersistentTaskAction;
337342
import org.opensearch.persistent.RemovePersistentTaskAction;
338343
import org.opensearch.persistent.StartPersistentTaskAction;
@@ -419,6 +424,7 @@
419424
import org.opensearch.rest.action.admin.indices.RestGetIngestionStateAction;
420425
import org.opensearch.rest.action.admin.indices.RestGetMappingAction;
421426
import org.opensearch.rest.action.admin.indices.RestGetSettingsAction;
427+
import org.opensearch.rest.action.admin.indices.RestHunspellCacheInvalidateAction;
422428
import org.opensearch.rest.action.admin.indices.RestIndexDeleteAliasesAction;
423429
import org.opensearch.rest.action.admin.indices.RestIndexPutAliasAction;
424430
import org.opensearch.rest.action.admin.indices.RestIndicesAliasesAction;
@@ -557,6 +563,7 @@ public class ActionModule extends AbstractModule {
557563
private final ThreadPool threadPool;
558564
private final ExtensionsManager extensionsManager;
559565
private final ResponseLimitSettings responseLimitSettings;
566+
private final HunspellService hunspellService;
560567

561568
public ActionModule(
562569
Settings settings,
@@ -571,7 +578,8 @@ public ActionModule(
571578
UsageService usageService,
572579
SystemIndices systemIndices,
573580
IdentityService identityService,
574-
ExtensionsManager extensionsManager
581+
ExtensionsManager extensionsManager,
582+
HunspellService hunspellService
575583
) {
576584
this.settings = settings;
577585
this.indexNameExpressionResolver = indexNameExpressionResolver;
@@ -581,6 +589,7 @@ public ActionModule(
581589
this.actionPlugins = actionPlugins;
582590
this.threadPool = threadPool;
583591
this.extensionsManager = extensionsManager;
592+
this.hunspellService = hunspellService;
584593
actions = setupActions(actionPlugins);
585594
actionFilters = setupActionFilters(actionPlugins);
586595
dynamicActionRegistry = new DynamicActionRegistry();
@@ -725,6 +734,8 @@ public <Request extends ActionRequest, Response extends ActionResponse> void reg
725734
actions.register(UpgradeStatusAction.INSTANCE, TransportUpgradeStatusAction.class);
726735
actions.register(UpgradeSettingsAction.INSTANCE, TransportUpgradeSettingsAction.class);
727736
actions.register(ClearIndicesCacheAction.INSTANCE, TransportClearIndicesCacheAction.class);
737+
actions.register(HunspellCacheInfoAction.INSTANCE, TransportHunspellCacheInfoAction.class);
738+
actions.register(HunspellCacheInvalidateAction.INSTANCE, TransportHunspellCacheInvalidateAction.class);
728739
actions.register(GetAliasesAction.INSTANCE, TransportGetAliasesAction.class);
729740
actions.register(GetSettingsAction.INSTANCE, TransportGetSettingsAction.class);
730741

@@ -1075,6 +1086,9 @@ public void initRestHandlers(Supplier<DiscoveryNodes> nodesInCluster) {
10751086
registerHandler.accept(new RestPauseIngestionAction());
10761087
registerHandler.accept(new RestResumeIngestionAction());
10771088
registerHandler.accept(new RestGetIngestionStateAction());
1089+
1090+
// Hunspell cache management API
1091+
registerHandler.accept(new RestHunspellCacheInvalidateAction());
10781092
}
10791093

10801094
@Override
@@ -1110,6 +1124,11 @@ protected void configure() {
11101124
bind(DynamicActionRegistry.class).toInstance(dynamicActionRegistry);
11111125

11121126
bind(ResponseLimitSettings.class).toInstance(responseLimitSettings);
1127+
1128+
// Bind HunspellService for TransportHunspellCacheInvalidateAction injection
1129+
if (hunspellService != null) {
1130+
bind(HunspellService.class).toInstance(hunspellService);
1131+
}
11131132
}
11141133

11151134
public ActionFilters getActionFilters() {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.action.admin.indices.cache.hunspell;
10+
11+
import org.opensearch.action.ActionType;
12+
13+
/**
14+
* Action for retrieving Hunspell cache information.
15+
*
16+
* <p>This action requires "cluster:monitor/hunspell/cache" permission when security is enabled.
17+
*
18+
* @opensearch.internal
19+
*/
20+
public class HunspellCacheInfoAction extends ActionType<HunspellCacheInfoResponse> {
21+
22+
public static final HunspellCacheInfoAction INSTANCE = new HunspellCacheInfoAction();
23+
public static final String NAME = "cluster:monitor/hunspell/cache";
24+
25+
private HunspellCacheInfoAction() {
26+
super(NAME, HunspellCacheInfoResponse::new);
27+
}
28+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.action.admin.indices.cache.hunspell;
10+
11+
import org.opensearch.action.ActionRequest;
12+
import org.opensearch.action.ActionRequestValidationException;
13+
import org.opensearch.core.common.io.stream.StreamInput;
14+
import org.opensearch.core.common.io.stream.StreamOutput;
15+
16+
import java.io.IOException;
17+
18+
/**
19+
* Request for retrieving Hunspell cache information.
20+
*
21+
* @opensearch.internal
22+
*/
23+
public class HunspellCacheInfoRequest extends ActionRequest {
24+
25+
public HunspellCacheInfoRequest() {}
26+
27+
public HunspellCacheInfoRequest(StreamInput in) throws IOException {
28+
super(in);
29+
}
30+
31+
@Override
32+
public void writeTo(StreamOutput out) throws IOException {
33+
super.writeTo(out);
34+
}
35+
36+
@Override
37+
public ActionRequestValidationException validate() {
38+
return null;
39+
}
40+
}

0 commit comments

Comments
 (0)