Skip to content

Commit

Permalink
refactor: optimize service extensions boot process (#4590)
Browse files Browse the repository at this point in the history
refactor: simplify service extensions lifecycle
  • Loading branch information
ndr-brt authored Oct 30, 2024
1 parent 31ad561 commit ecb2d09
Show file tree
Hide file tree
Showing 29 changed files with 450 additions and 815 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.eclipse.edc.boot.system.injection.InjectionPointScanner;
import org.eclipse.edc.boot.system.injection.ProviderMethod;
import org.eclipse.edc.boot.system.injection.ProviderMethodScanner;
import org.eclipse.edc.boot.system.injection.lifecycle.ServiceProvider;
import org.eclipse.edc.boot.util.CyclicDependencyException;
import org.eclipse.edc.boot.util.TopologicalSort;
import org.eclipse.edc.runtime.metamodel.annotation.BaseExtension;
Expand Down Expand Up @@ -74,7 +75,24 @@ public DependencyGraph(ServiceExtensionContext context) {
*/
public List<InjectionContainer<ServiceExtension>> of(List<ServiceExtension> loadedExtensions) {
var extensions = sortByType(loadedExtensions);
var dependencyMap = createDependencyMap(extensions);
Map<Class<?>, ServiceProvider> defaultServiceProviders = new HashMap<>();
Map<ServiceExtension, List<ServiceProvider>> serviceProviders = new HashMap<>();
Map<Class<?>, List<ServiceExtension>> dependencyMap = new HashMap<>();
extensions.forEach(extension -> {
getProvidedFeatures(extension).forEach(feature -> dependencyMap.computeIfAbsent(feature, k -> new ArrayList<>()).add(extension));
// check all @Provider methods
new ProviderMethodScanner(extension).allProviders()
.peek(providerMethod -> {
var serviceProvider = new ServiceProvider(providerMethod, extension);
if (providerMethod.isDefault()) {
defaultServiceProviders.put(providerMethod.getReturnType(), serviceProvider);
} else {
serviceProviders.computeIfAbsent(extension, k -> new ArrayList<>()).add(serviceProvider);
}
})
.map(ProviderMethod::getReturnType)
.forEach(feature -> dependencyMap.computeIfAbsent(feature, k -> new ArrayList<>()).add(extension));
});

var sort = new TopologicalSort<ServiceExtension>();

Expand All @@ -86,10 +104,10 @@ public List<InjectionContainer<ServiceExtension>> of(List<ServiceExtension> load
.collect(toMap(identity(), ext -> {

//check that all the @Required features are there
getRequiredFeatures(ext.getClass()).forEach(feature -> {
var dependencies = dependencyMap.get(feature);
getRequiredFeatures(ext.getClass()).forEach(serviceClass -> {
var dependencies = dependencyMap.get(serviceClass);
if (dependencies == null) {
unsatisfiedRequirements.add(feature.getName());
unsatisfiedRequirements.add(serviceClass.getName());
} else {
dependencies.forEach(dependency -> sort.addDependency(ext, dependency));
}
Expand All @@ -108,6 +126,11 @@ public List<InjectionContainer<ServiceExtension>> of(List<ServiceExtension> load
.filter(d -> !Objects.equals(d, ext)) // remove dependencies onto oneself
.forEach(provider -> sort.addDependency(ext, provider)));
}

var defaultServiceProvider = defaultServiceProviders.get(injectionPoint.getType());
if (defaultServiceProvider != null) {
injectionPoint.setDefaultServiceProvider(defaultServiceProvider);
}
})
.collect(toSet());
}));
Expand All @@ -127,26 +150,20 @@ public List<InjectionContainer<ServiceExtension>> of(List<ServiceExtension> load

// convert the sorted list of extensions into an equally sorted list of InjectionContainers
return extensions.stream()
.map(key -> new InjectionContainer<>(key, injectionPoints.get(key)))
.map(key -> new InjectionContainer<>(key, injectionPoints.get(key), serviceProviders.get(key)))
.toList();
}

private boolean canResolve(Map<Class<?>, List<ServiceExtension>> dependencyMap, Class<?> featureName) {
var providers = dependencyMap.get(featureName);
private boolean canResolve(Map<Class<?>, List<ServiceExtension>> dependencyMap, Class<?> serviceClass) {
var providers = dependencyMap.get(serviceClass);
if (providers != null) {
return true;
} else {
// attempt to interpret the feature name as class name, instantiate it and see if the context has that service
return context.hasService(featureName);
return context.hasService(serviceClass);
}
}

private Map<Class<?>, List<ServiceExtension>> createDependencyMap(List<ServiceExtension> extensions) {
Map<Class<?>, List<ServiceExtension>> dependencyMap = new HashMap<>();
extensions.forEach(ext -> getProvidedFeatures(ext).forEach(feature -> dependencyMap.computeIfAbsent(feature, k -> new ArrayList<>()).add(ext)));
return dependencyMap;
}

private Stream<Class<?>> getRequiredFeatures(Class<?> clazz) {
var requiresAnnotation = clazz.getAnnotation(Requires.class);
if (requiresAnnotation != null) {
Expand All @@ -168,8 +185,6 @@ private Set<Class<?>> getProvidedFeatures(ServiceExtension ext) {
allProvides.addAll(Arrays.asList(providesAnnotation.value()));
}

// check all @Provider methods
new ProviderMethodScanner(ext).allProviders().map(ProviderMethod::getReturnType).forEach(allProvides::add);
return allProvides;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@
import io.opentelemetry.api.OpenTelemetry;
import org.eclipse.edc.boot.monitor.MultiplexingMonitor;
import org.eclipse.edc.boot.system.injection.InjectionContainer;
import org.eclipse.edc.boot.system.injection.InjectorImpl;
import org.eclipse.edc.boot.system.injection.ProviderMethod;
import org.eclipse.edc.boot.system.injection.ProviderMethodScanner;
import org.eclipse.edc.boot.system.injection.lifecycle.ExtensionLifecycleManager;
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.monitor.ConsoleMonitor;
import org.eclipse.edc.spi.monitor.Monitor;
Expand All @@ -33,11 +29,9 @@
import org.eclipse.edc.spi.telemetry.Telemetry;
import org.jetbrains.annotations.NotNull;

import java.util.HashMap;
import java.util.List;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand All @@ -49,42 +43,6 @@ public ExtensionLoader(ServiceLocator serviceLocator) {
this.serviceLocator = serviceLocator;
}

/**
* Convenience method for loading service extensions.
*/
public static void bootServiceExtensions(List<InjectionContainer<ServiceExtension>> containers, ServiceExtensionContext context) {
//construct a list of default providers, which are invoked, if a particular service is not present in the context
var defaultServices = new HashMap<Class<?>, Supplier<Object>>();
containers.forEach(se -> {
var pm = new ProviderMethodScanner(se.getInjectionTarget()).defaultProviders();
pm.forEach(p -> defaultServices.put(p.getReturnType(), getDefaultProviderInvoker(context, se, p)));
});

var injector = new InjectorImpl(defaultServices);

// go through the extension initialization lifecycle
var lifeCycles = containers.stream()
.map(c -> new ExtensionLifecycleManager(c, context, injector))
.map(ExtensionLifecycleManager::inject)
.map(ExtensionLifecycleManager::initialize)
.map(ExtensionLifecycleManager::provide)
.collect(Collectors.toList());

context.freeze();

var preparedExtensions = lifeCycles.stream().map(ExtensionLifecycleManager::prepare).collect(Collectors.toList());
preparedExtensions.forEach(ExtensionLifecycleManager::start);
}

@NotNull
private static Supplier<Object> getDefaultProviderInvoker(ServiceExtensionContext context, InjectionContainer<ServiceExtension> se, ProviderMethod p) {
return () -> {
var d = p.invoke(se.getInjectionTarget(), context);
context.registerService(p.getReturnType(), d);
return d;
};
}

public static @NotNull Monitor loadMonitor(String... programArgs) {
var loader = ServiceLoader.load(MonitorExtension.class);
return loadMonitor(loader.stream().map(ServiceLoader.Provider::get).collect(Collectors.toList()), programArgs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

package org.eclipse.edc.boot.system.injection;

import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.jetbrains.annotations.Nullable;

/**
Expand All @@ -30,5 +31,5 @@ public interface DefaultServiceSupplier {
* @return a default service, null if not found
*/
@Nullable
Object provideFor(Class<?> type);
Object provideFor(InjectionPoint<?> type, ServiceExtensionContext context);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

package org.eclipse.edc.boot.system.injection;

import org.eclipse.edc.boot.system.injection.lifecycle.ServiceProvider;
import org.eclipse.edc.spi.system.ServiceExtension;

import java.lang.reflect.Field;
Expand All @@ -30,6 +31,7 @@ public class FieldInjectionPoint<T> implements InjectionPoint<T> {
private final T instance;
private final Field injectedField;
private final boolean isRequired;
private ServiceProvider defaultServiceProvider;

public FieldInjectionPoint(T instance, Field injectedField) {
this(instance, injectedField, true);
Expand Down Expand Up @@ -62,6 +64,17 @@ public void setTargetValue(Object service) throws IllegalAccessException {
injectedField.set(instance, service);
}

@Override
public ServiceProvider getDefaultServiceProvider() {
return defaultServiceProvider;
}

@Override
public void setDefaultServiceProvider(ServiceProvider defaultServiceProvider) {
this.defaultServiceProvider = defaultServiceProvider;

}

@Override
public String toString() {
return format("Field \"%s\" of type [%s] required by %s", injectedField.getName(), getType(), instance.getClass().getName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,25 @@

package org.eclipse.edc.boot.system.injection;

import org.eclipse.edc.runtime.metamodel.annotation.Provides;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.boot.system.injection.lifecycle.ServiceProvider;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;

import java.util.Objects;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* Represents one {@link ServiceExtension} with a description of all its auto-injectable fields, which in turn are
* represented by {@link FieldInjectionPoint}s.
*/
public class InjectionContainer<T> {
private final T injectionTarget;
private final List<ServiceProvider> serviceProviders;
private final Set<InjectionPoint<T>> injectionPoint;

public InjectionContainer(T target, Set<InjectionPoint<T>> injectionPoint) {
public InjectionContainer(T target, Set<InjectionPoint<T>> injectionPoint, List<ServiceProvider> serviceProviders) {
injectionTarget = target;
if (injectionPoint.stream().anyMatch(ip -> ip.getInstance() != target)) {
throw new EdcInjectionException("Injection target must match all InjectionPoints!");
}
this.serviceProviders = serviceProviders;
this.injectionPoint = injectionPoint;

}

public T getInjectionTarget() {
Expand All @@ -49,30 +43,15 @@ public Set<InjectionPoint<T>> getInjectionPoints() {
return injectionPoint;
}

public List<ServiceProvider> getServiceProviders() {
return serviceProviders;
}

@Override
public String toString() {
return getClass().getSimpleName() + "{" +
"injectionTarget=" + injectionTarget +
'}';
}

/**
* checks that all there is a corresponding service instance for every entry in the @Provides annotation list.
*/
public Result<Void> validate(ServiceExtensionContext context) {

var providesAnnotation = injectionTarget.getClass().getAnnotation(Provides.class);
if (providesAnnotation == null) {
return Result.success();
}

var providedClasses = Stream.of(providesAnnotation.value());

var errors = providedClasses
.map(clazz -> context.hasService(clazz) ? null : clazz.getName())
.filter(Objects::nonNull)
.collect(Collectors.toList());

return errors.isEmpty() ? Result.success() : Result.failure(errors);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

package org.eclipse.edc.boot.system.injection;

import org.eclipse.edc.boot.system.injection.lifecycle.ServiceProvider;

/**
* Represents an auto-injectable property. Possible implementors are field injection points, constructor injection points, etc.
*
Expand All @@ -27,4 +29,8 @@ public interface InjectionPoint<T> {
boolean isRequired();

void setTargetValue(Object service) throws IllegalAccessException;

ServiceProvider getDefaultServiceProvider();

void setDefaultServiceProvider(ServiceProvider defaultServiceProvider);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2024 Cofinity-X
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Cofinity-X - initial API and implementation
*
*/

package org.eclipse.edc.boot.system.injection;

import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.jetbrains.annotations.Nullable;

/**
* Supplies the default {@link org.eclipse.edc.boot.system.injection.lifecycle.ServiceProvider} that has been stored in
* the {@link InjectionPoint}
*/
public class InjectionPointDefaultServiceSupplier implements DefaultServiceSupplier {

@Override
public @Nullable Object provideFor(InjectionPoint<?> injectionPoint, ServiceExtensionContext context) {
var defaultService = injectionPoint.getDefaultServiceProvider();
if (injectionPoint.isRequired() && defaultService == null) {
throw new EdcInjectionException("No default provider for required service " + injectionPoint.getType());
}
return defaultService == null ? null : defaultService.register(context);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,10 @@
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.system.ServiceExtensionContext;

import java.util.Map;
import java.util.function.Supplier;

import static java.util.Optional.ofNullable;

public final class InjectorImpl implements Injector {

private final DefaultServiceSupplier defaultServiceSupplier;

/**
* Constructs a new Injector instance, which can either resolve services from the {@link ServiceExtensionContext}, or -
* if the required service is not present - use the default implementations provided in the map.
*
* @param defaultSuppliers A map that contains dependency types as key, and default service objects as value.
*/
public InjectorImpl(Map<Class<?>, Supplier<Object>> defaultSuppliers) {
this(type -> ofNullable(defaultSuppliers.get(type)).map(Supplier::get).orElse(null));
}

/**
* Constructs a new Injector instance, which can either resolve services from the {@link ServiceExtensionContext}, or -
* if the required service is not present - use the default implementations provided in the map.
Expand All @@ -52,7 +37,7 @@ public <T> T inject(InjectionContainer<T> container, ServiceExtensionContext con

container.getInjectionPoints().forEach(ip -> {
try {
Object service = resolveService(context, ip.getType(), ip.isRequired());
var service = resolveService(context, ip);
if (service != null) { //can only be if not required
ip.setTargetValue(service);
}
Expand All @@ -70,15 +55,13 @@ public <T> T inject(InjectionContainer<T> container, ServiceExtensionContext con
return container.getInjectionTarget();
}

private Object resolveService(ServiceExtensionContext context, Class<?> serviceClass, boolean isRequired) {
private Object resolveService(ServiceExtensionContext context, InjectionPoint<?> injectionPoint) {
var serviceClass = injectionPoint.getType();
if (context.hasService(serviceClass)) {
return context.getService(serviceClass, !isRequired);
return context.getService(serviceClass, !injectionPoint.isRequired());
} else {
Object defaultService = defaultServiceSupplier.provideFor(serviceClass);
if (isRequired && defaultService == null) {
throw new EdcInjectionException("No default provider for required service " + serviceClass);
}
return defaultService;
return defaultServiceSupplier.provideFor(injectionPoint, context);
}
}

}
Loading

0 comments on commit ecb2d09

Please sign in to comment.