elastic/elasticsearch · critical · IllegalStateException
Expected at most one {} implementation, found: {}
Error message
Expected at most one {} implementation, found: {} What it means
Thrown by DataStreamsPlugin.loadSingleExtension when more than one plugin registers an implementation of a single-slot extension point (DownsamplingOperations, FrozenTransitionInfoProvider, etc.). These extension points are designed for at most one implementation; multiple registrations are an ambiguous, unsupported deployment state and fail fast at plugin load.
Source
Thrown at modules/data-streams/src/main/java/org/elasticsearch/datastreams/DataStreamsPlugin.java:178
public DataStreamsPlugin(Settings settings) {
this.settings = settings;
}
@Override
public void loadExtensions(ExtensionLoader loader) {
downsamplingOperations = loadSingleExtension(loader, DownsamplingOperations.class, downsamplingOperations);
frozenTransitionInfoProvider = loadSingleExtension(loader, FrozenTransitionInfoProvider.class, frozenTransitionInfoProvider);
}
/**
* Loads at most one implementation of the given extension point, falling back to {@code fallback} if none is
* registered. Throws if more than one implementation is found, since these extension points are meant to be
* implemented by a single optional plugin.
*/
private static <T> T loadSingleExtension(ExtensionLoader loader, Class<T> type, T fallback) {
List<T> extensions = loader.loadExtensions(type);
if (extensions.size() > 1) {
throw new IllegalStateException(
"Expected at most one "
+ type.getSimpleName()
+ " implementation, found: "
+ extensions.stream().map(Object::getClass).toList()
);
}
return extensions.isEmpty() ? fallback : extensions.get(0);
}
protected Clock getClock() {
return Clock.systemUTC();
}
// The dependency of index.look_ahead_time is a cluster setting and currently there is no clean validation approach for this:
static void additionalLookAheadTimeValidation(TimeValue lookAhead, TimeValue timeSeriesPollInterval) {
if (lookAhead.compareTo(timeSeriesPollInterval) < 0) {
final String message = String.format(
Locale.ROOT,View on GitHub (pinned to db6a809a66)
Solutions
- Remove one of the conflicting plugins so exactly one (or zero) implementations of the named extension point is installed.
- If you need custom behavior, have your plugin replace, not augment, the bundled implementation.
- Inspect the loaded extensions list in the error message to identify both conflicting plugin classes.
Defensive patterns
Strategy: validation
Validate before calling
// At install time, enumerate plugins and assert at most one implements each single-slot extension.
Set<Class<?>> singleSlot = Set.of(DownsamplingOperations.class, FrozenTransitionInfoProvider.class);
for (Class<?> t : singleSlot) {
long count = installedPlugins.stream().filter(p -> Arrays.asList(p.getClass().getInterfaces()).contains(t)).count();
if (count > 1) throw new IllegalStateException("multiple " + t.getSimpleName() + " implementations installed");
} Prevention
- Audit installed plugins before deploy; ensure only one (or zero) single-slot extension implementation is present.
- When adding a custom extension plugin, disable/remove the bundled equivalent first.
When it happens
Trigger: Installing two plugins that each provide a DownsamplingOperations or FrozenTransitionInfoProvider; a custom plugin duplicating functionality already provided by a bundled plugin.
Common situations: Custom plugin ships alongside an Elastic plugin that already implements the same extension point; upgrading and accidentally leaving an old custom extension plugin installed.
Related errors
- backing index [%s] in tsdb mode doesn't have the [%s] index
- Cannot add dynamic templates that define dimension fields on
- failed to parse value%s for setting [%s], must be lower than
- data streams {} not found
- Failed to determine indexMode for data stream: {}
AI-assisted analysis of elastic/elasticsearch@db6a809a66 (2026-08-12).
Data as JSON: /api/errors/0446c2792fdc6ce2.
Report an issue: GitHub.