Skip to content

Commit fcde585

Browse files
committed
Remove license state listeners on closables (#36308)
We have a few places where we register license state listeners on transient components (i.e., resources that can be open and closed during the lifecycle of the server). In one case (the opt-out query cache) we were never removing the registered listener, effectively a terrible memory leak. In another case, we were not un-registered the listener that we registered, since we were not referencing the same instance of Runnable. This commit does two things: - introduces a marker interface LicenseStateListener so that it is easier to identify these listeners in the codebase and avoid classes that need to register a license state listener from having to implement Runnable which carries a different semantic meaning than we want here - fixes the two places where we are currently leaking license state listeners
1 parent a553168 commit fcde585

File tree

6 files changed

+97
-12
lines changed

6 files changed

+97
-12
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.license;
8+
9+
import org.elasticsearch.Version;
10+
11+
/**
12+
* Marker interface for callbacks that are invoked when the license state changes.
13+
*/
14+
@FunctionalInterface
15+
public interface LicenseStateListener {
16+
17+
/**
18+
* Callback when the license state changes. See {@link XPackLicenseState#update(License.OperationMode, boolean, Version)}.
19+
*/
20+
void licenseStateChanged();
21+
22+
}

x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ private static class Status {
266266
}
267267
}
268268

269-
private final List<Runnable> listeners;
269+
private final List<LicenseStateListener> listeners;
270270
private final boolean isSecurityEnabled;
271271
private final boolean isSecurityExplicitlyEnabled;
272272

@@ -315,17 +315,17 @@ void update(OperationMode mode, boolean active, @Nullable Version mostRecentTria
315315
}
316316
}
317317
}
318-
listeners.forEach(Runnable::run);
318+
listeners.forEach(LicenseStateListener::licenseStateChanged);
319319
}
320320

321321
/** Add a listener to be notified on license change */
322-
public void addListener(Runnable runnable) {
323-
listeners.add(Objects.requireNonNull(runnable));
322+
public void addListener(final LicenseStateListener listener) {
323+
listeners.add(Objects.requireNonNull(listener));
324324
}
325325

326326
/** Remove a listener */
327-
public void removeListener(Runnable runnable) {
328-
listeners.remove(runnable);
327+
public void removeListener(final LicenseStateListener listener) {
328+
listeners.remove(Objects.requireNonNull(listener));
329329
}
330330

331331
/** Return the current license type. */

x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/exporter/local/LocalExporter.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.elasticsearch.index.IndexNotFoundException;
3535
import org.elasticsearch.ingest.IngestMetadata;
3636
import org.elasticsearch.ingest.PipelineConfiguration;
37+
import org.elasticsearch.license.LicenseStateListener;
3738
import org.elasticsearch.license.XPackLicenseState;
3839
import org.elasticsearch.protocol.xpack.watcher.DeleteWatchRequest;
3940
import org.elasticsearch.protocol.xpack.watcher.PutWatchRequest;
@@ -78,7 +79,7 @@
7879
import static org.elasticsearch.xpack.core.monitoring.exporter.MonitoringTemplateUtils.pipelineName;
7980
import static org.elasticsearch.xpack.monitoring.Monitoring.CLEAN_WATCHER_HISTORY;
8081

81-
public class LocalExporter extends Exporter implements ClusterStateListener, CleanerService.Listener {
82+
public class LocalExporter extends Exporter implements ClusterStateListener, CleanerService.Listener, LicenseStateListener {
8283

8384
private static final Logger logger = LogManager.getLogger(LocalExporter.class);
8485

@@ -106,9 +107,10 @@ public LocalExporter(Exporter.Config config, Client client, CleanerService clean
106107
this.clusterAlertBlacklist = ClusterAlertsUtil.getClusterAlertsBlacklist(config);
107108
this.cleanerService = cleanerService;
108109
this.dateTimeFormatter = dateTimeFormatter(config);
110+
// if additional listeners are added here, adjust LocalExporterTests#testLocalExporterRemovesListenersOnClose accordingly
109111
clusterService.addListener(this);
110112
cleanerService.add(this);
111-
licenseState.addListener(this::licenseChanged);
113+
licenseState.addListener(this);
112114
}
113115

114116
@Override
@@ -121,7 +123,8 @@ public void clusterChanged(ClusterChangedEvent event) {
121123
/**
122124
* When the license changes, we need to ensure that Watcher is setup properly.
123125
*/
124-
private void licenseChanged() {
126+
@Override
127+
public void licenseStateChanged() {
125128
watcherSetup.set(false);
126129
}
127130

@@ -153,7 +156,7 @@ public void doClose() {
153156
// we also remove the listener in resolveBulk after we get to RUNNING, but it's okay to double-remove
154157
clusterService.removeListener(this);
155158
cleanerService.remove(this);
156-
licenseState.removeListener(this::licenseChanged);
159+
licenseState.removeListener(this);
157160
}
158161
}
159162

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.monitoring.exporter.local;
8+
9+
import org.elasticsearch.client.Client;
10+
import org.elasticsearch.cluster.service.ClusterService;
11+
import org.elasticsearch.common.settings.Settings;
12+
import org.elasticsearch.license.XPackLicenseState;
13+
import org.elasticsearch.test.ESTestCase;
14+
import org.elasticsearch.xpack.monitoring.cleaner.CleanerService;
15+
import org.elasticsearch.xpack.monitoring.exporter.Exporter;
16+
17+
import static org.mockito.Mockito.mock;
18+
import static org.mockito.Mockito.verify;
19+
20+
public class LocalExporterTests extends ESTestCase {
21+
22+
public void testLocalExporterRemovesListenersOnClose() {
23+
final ClusterService clusterService = mock(ClusterService.class);
24+
final XPackLicenseState licenseState = mock(XPackLicenseState.class);
25+
final Exporter.Config config = new Exporter.Config("name", "type", Settings.EMPTY, clusterService, licenseState);
26+
final CleanerService cleanerService = mock(CleanerService.class);
27+
final LocalExporter exporter = new LocalExporter(config, mock(Client.class), cleanerService);
28+
verify(clusterService).addListener(exporter);
29+
verify(cleanerService).add(exporter);
30+
verify(licenseState).addListener(exporter);
31+
exporter.close();
32+
verify(clusterService).removeListener(exporter);
33+
verify(cleanerService).remove(exporter);
34+
verify(licenseState).removeListener(exporter);
35+
}
36+
37+
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/accesscontrol/OptOutQueryCache.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.elasticsearch.index.IndexSettings;
1515
import org.elasticsearch.index.cache.query.QueryCache;
1616
import org.elasticsearch.indices.IndicesQueryCache;
17+
import org.elasticsearch.license.LicenseStateListener;
1718
import org.elasticsearch.license.XPackLicenseState;
1819
import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField;
1920
import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl;
@@ -26,7 +27,7 @@
2627
* Opts out of the query cache if field level security is active for the current request,
2728
* and its unsafe to cache.
2829
*/
29-
public final class OptOutQueryCache extends AbstractIndexComponent implements QueryCache {
30+
public final class OptOutQueryCache extends AbstractIndexComponent implements LicenseStateListener, QueryCache {
3031

3132
private final IndicesQueryCache indicesQueryCache;
3233
private final ThreadContext context;
@@ -43,14 +44,20 @@ public OptOutQueryCache(
4344
this.context = Objects.requireNonNull(context, "threadContext must not be null");
4445
this.indexName = indexSettings.getIndex().getName();
4546
this.licenseState = Objects.requireNonNull(licenseState, "licenseState");
46-
licenseState.addListener(() -> this.clear("license state changed"));
47+
licenseState.addListener(this);
4748
}
4849

4950
@Override
5051
public void close() throws ElasticsearchException {
52+
licenseState.removeListener(this);
5153
clear("close");
5254
}
5355

56+
@Override
57+
public void licenseStateChanged() {
58+
clear("license state changed");
59+
}
60+
5461
@Override
5562
public void clear(String reason) {
5663
logger.debug("full cache clear, reason [{}]", reason);

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/OptOutQueryCacheTests.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,22 @@ public void testOptOutQueryCacheIndexDoesNotHaveFieldLevelSecurity() {
183183
verify(indicesQueryCache).doCache(same(weight), same(policy));
184184
}
185185

186+
public void testOptOutQueryCacheRemovesLicenseStateListenerOnClose() {
187+
final Settings.Builder settings = Settings.builder()
188+
.put("index.version.created", Version.CURRENT)
189+
.put("index.number_of_shards", 1)
190+
.put("index.number_of_replicas", 0);
191+
final IndexMetaData indexMetaData = IndexMetaData.builder("index").settings(settings).build();
192+
final IndexSettings indexSettings = new IndexSettings(indexMetaData, Settings.EMPTY);
193+
final IndicesQueryCache indicesQueryCache = mock(IndicesQueryCache.class);
194+
final ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
195+
final XPackLicenseState licenseState = mock(XPackLicenseState.class);
196+
final OptOutQueryCache cache = new OptOutQueryCache(indexSettings, indicesQueryCache, threadContext, licenseState);
197+
verify(licenseState).addListener(cache);
198+
cache.close();
199+
verify(licenseState).removeListener(cache);
200+
}
201+
186202
private static FieldPermissionsDefinition fieldPermissionDef(String[] granted, String[] denied) {
187203
return new FieldPermissionsDefinition(granted, denied);
188204
}

0 commit comments

Comments
 (0)