[Resource Sharing] Reverts @Inject pattern usage for ResourceSharingExtension to client accessor pattern.#5576
Merged
DarshitChanpura merged 7 commits intoAug 19, 2025
Conversation
Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
Member
Author
|
This changes reverts back to the changes made here: #5541 The only other alternative is to have plugins implement their own version of: ResourceExtensionWrapper.java/*
* 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.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/
package org.opensearch.sample;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.opensearch.common.inject.Inject;
import org.opensearch.common.inject.Injector;
import java.lang.reflect.Method;
public final class ResourceExtensionWrapper {
private static final Logger log = LogManager.getLogger(ResourceExtensionWrapper.class);
// Class names kept as strings to avoid compile-time coupling
private static final String SPI_IFACE =
"org.opensearch.security.spi.resources.ResourceSharingExtension";
private static final String IMPL_CLASS =
"org.opensearch.sample.SampleResourceExtension";
@Inject
public Injector injector;
// Init state
private volatile boolean initialized; // set to true when we have a definitive state
private volatile boolean spiAbsentForever; // true iff SPI not on classpath
private volatile Object extension; // the extension instance or null
private volatile Method getClientMethod; // cached "getResourceSharingClient" Method
/** Visible for tests: allow explicit instance injection. */
public void setExtension(Object extension) {
this.extension = extension;
// cache method if possible
if (extension != null) {
try {
this.getClientMethod = extension.getClass().getMethod("getResourceSharingClient");
} catch (ReflectiveOperationException e) {
log.debug("Timeseries RS extension present but no getResourceSharingClient()", e);
this.getClientMethod = null;
}
} else {
this.getClientMethod = null;
}
this.initialized = true;
}
/** Returns true if SPI is on the classpath AND an extension instance is available. */
public boolean isPresent() {
ensureInitialized();
return extension != null && getClientMethod != null;
}
/**
* Returns the ResourceSharingClient (as Object) or null if SPI/extension is not available.
* Intentionally returns Object to avoid compile-time coupling to SPI types.
*/
public Object getResourceSharingClient() {
ensureInitialized();
Object ext = extension;
Method m = getClientMethod;
if (ext == null || m == null) return null;
try {
return m.invoke(ext);
} catch (ReflectiveOperationException e) {
// treat as absent at runtime; don't flip initialized so future retries could still happen if state changes
log.debug("Invoking getResourceSharingClient() failed; treating as absent.", e);
return null;
}
}
private void ensureInitialized() {
if (initialized) return;
synchronized (this) {
if (initialized) return;
// 0) If we previously discovered SPI is missing, we can finalize immediately.
if (spiAbsentForever) {
initialized = true;
return;
}
// 1) Check SPI presence using TCCL first, then our loader.
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
ClassLoader self = getClass().getClassLoader();
if (classAbsent(tccl) && classAbsent(self)) {
// SPI truly not installed — no need to ever retry
spiAbsentForever = true;
extension = null;
getClientMethod = null;
initialized = true;
log.debug("Security SPI not present on classpath; RS extension unavailable.");
return;
}
// 2) SPI present; try to obtain our implementation instance from Guice.
// If this fails due to binding timing, leave 'initialized'=false so we retry on next call.
if (injector == null) {
// This shouldn't happen with ctor injection, but be defensive if refactored.
log.debug("Injector not available yet; deferring RS extension initialization.");
return; // DO NOT set initialized — we want to retry later
}
try {
Class<?> implClazz = Class.forName(IMPL_CLASS, /* initialize */ false, self);
Object instance = injector.getInstance((Class<?>) implClazz);
Method m = implClazz.getMethod("getResourceSharingClient");
this.extension = instance;
this.getClientMethod = m;
this.initialized = true; // success — we can finalize
log.debug("Initialized Timeseries RS extension via Guice: {}", IMPL_CLASS);
} catch (ClassNotFoundException e) {
// Our impl isn't on the classpath (shouldn't happen since it's our own class) — finalize as absent
log.debug("RS extension impl class not found; treating as unavailable.", e);
this.extension = null;
this.getClientMethod = null;
this.initialized = true;
} catch (Throwable t) {
// Provisioning/binding not ready yet or other runtime issue — DO NOT finalize; allow retrial.
log.debug("RS extension not ready yet (will retry on next call).", t);
// leave initialized=false to allow a later retry
}
}
}
private static boolean classAbsent(ClassLoader cl) {
try {
if (cl == null) return true;
Class.forName(ResourceExtensionWrapper.SPI_IFACE, /* initialize */ false, cl);
return false;
} catch (ClassNotFoundException e) {
return true;
} catch (LinkageError e) {
// present but broken — treat as absent to be safe
return true;
}
}
}and then inject it in their createComponents like: try {
// This loader will throw class not found exception if SPI doesn't exist (feature disabled or security not installed)
Class.forName("org.opensearch.security.spi.resources.ResourceSharingExtension");
// if no exception was thrown we make the wrapper an injectable component
components.add(new ResourceExtensionWrapper());
} catch (ClassNotFoundException e) {
// spi is not loaded into classpath, i.e. the feature is disabled or security plugin is absent, do nothing
}I'm not in favor of that approach. |
Cherry-picked changes from opensearch-project#5541 Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
e94197f to
420874e
Compare
Member
|
Thank you for trying @DarshitChanpura . Unfortunately, it seems like our fork of Guice from 10 years ago doesn't have the optional bindings necessary for such a use-case. |
cwperks
approved these changes
Aug 19, 2025
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #5576 +/- ##
=======================================
Coverage 73.06% 73.07%
=======================================
Files 407 408 +1
Lines 25255 25259 +4
Branches 3842 3842
=======================================
+ Hits 18452 18457 +5
- Misses 4931 4934 +3
+ Partials 1872 1868 -4
🚀 New features to boost your workflow:
|
RyanL1997
approved these changes
Aug 19, 2025
017065c
into
opensearch-project:main
70 of 71 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
After trying to implement
@Injectpattern to inject the extension and use the Guice injection to retrieve client from extension in plugin's transport actions, we discovered that cluster bootstrap would always fails withNoClasDefFoundErrorwhenever SPI is not present in the classpath (i.e. security is not present).We experimented with this annotation as discussed in comments of #5541, but were not successful.
Hence, we are reverting to client accessor singleton pattern.
Category: Bug fix
Issues Resolved
Unblocks plugin CIs and allows clusters without security plugin to bootstrap.
Testing
Automated tests
Check List
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.