Skip to content

Do not start redis dev services in augmentation (plus supporting framework) #47610

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,34 @@

import java.io.Closeable;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.jboss.logging.Logger;

import io.quarkus.builder.item.MultiBuildItem;

/**
* BuildItem for running dev services.
* Combines injected configs to the application with container id (if it exists).
*
* <p>
* Processors are expected to return this build item not only when the dev service first starts,
* but also if a running dev service already exists.
*
* <p>
* {@link RunningDevService} helps to manage the lifecycle of the running dev service.
*/
public final class DevServicesResultBuildItem extends MultiBuildItem {

private static final Logger log = Logger.getLogger(DevServicesResultBuildItem.class);

private final String name;
private final String description;
private final String containerId;
private final Map<String, String> config;
protected final Map<String, String> config;
protected RunnableDevService runnableDevService;

public DevServicesResultBuildItem(String name, String containerId, Map<String, String> config) {
this(name, null, containerId, config);
Expand All @@ -35,6 +42,12 @@ public DevServicesResultBuildItem(String name, String description, String contai
this.config = config;
}

public DevServicesResultBuildItem(String name, String description, String containerId, Map<String, String> config,
RunnableDevService runnableDevService) {
this(name, description, containerId, config);
this.runnableDevService = runnableDevService;
}

public String getName() {
return name;
}
Expand All @@ -51,13 +64,22 @@ public Map<String, String> getConfig() {
return config;
}

public void start() {
if (runnableDevService != null) {
runnableDevService.start();
} else {
log.debugf("Not starting %s because runnable dev service is null (it is probably a running dev service.", name);
}
}

public static class RunningDevService implements Closeable {

private final String name;
private final String description;
private final String containerId;
private final Map<String, String> config;
private final Closeable closeable;
protected final String name;
protected final String description;
protected final String containerId;
protected final Map<String, String> config;
protected final Closeable closeable;
protected volatile boolean isRunning = true;

private static Map<String, String> mapOf(String key, String value) {
Map<String, String> map = new HashMap<>();
Expand Down Expand Up @@ -117,11 +139,71 @@ public boolean isOwner() {
public void close() throws IOException {
if (this.closeable != null) {
this.closeable.close();
isRunning = false;
}
}

public DevServicesResultBuildItem toBuildItem() {
return new DevServicesResultBuildItem(name, description, containerId, config);
}
}

public static class RunnableDevService extends RunningDevService {
final DevServicesTrackerBuildItem tracker;

private final Startable container;

public RunnableDevService(String name, String containerId, Startable container, Map config,
DevServicesTrackerBuildItem tracker) {
super(name, containerId, container::close, config);

this.container = container;
this.tracker = tracker;
isRunning = false;
}

public boolean isRunning() {
return isRunning;
}

public void start() {
// We want to do two things; find things with the same config as us to reuse them, and find things with different config to close them
// We figure out if we need to shut down existing redis containers that might have been started in previous profiles or restarts

List<RunnableDevService> matchedDevServices = tracker.getRunningServices(name, config);
// if the redis containers have already started we just return; if we wanted to be very cautious we could check the entries for an isRunningStatus, but they might be in the wrong classloader, so that's hard work

if (matchedDevServices == null || matchedDevServices.size() > 0) {
// There isn't a running container that has the right config, we need to do work
// Let's get all the running dev services associated with this feature
Collection<Closeable> unusableDevServices = tracker.getAllServices(name);
if (unusableDevServices != null) {
for (Closeable closeable : unusableDevServices) {
try {
closeable.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

if (container != null) {
container.start();
// tell the tracker that we started
tracker.addRunningService(name, config, this);
isRunning = true;
}
}
}

@Override
public void close() throws IOException {
super.close();
tracker.removeRunningService(name, config, this);
}

public DevServicesResultBuildItem toBuildItem() {
return new DevServicesResultBuildItem(name, description, containerId, config, this);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package io.quarkus.deployment.builditem;

import java.io.Closeable;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import io.quarkus.builder.item.SimpleBuildItem;

// Ideally we would have a unique build item for each processor/feature, but that would need a new KeyedBuildItem or FeatureBuildItem type
public final class DevServicesTrackerBuildItem extends SimpleBuildItem {

// A map of a map of a list? What?!?
// The reason this is like this, rather than being broken out into types, is because this map gets shared between classloaders, so language-level constructs work best
private volatile static Map<String, Map<Map, List<Closeable>>> systemInstance = null;

public DevServicesTrackerBuildItem() {

if (systemInstance == null) {
try {
Class s = ClassLoader.getSystemClassLoader().loadClass(DevServicesTrackerBuildItem.class.getName());
systemInstance = (Map<String, Map<Map, List<Closeable>>>) s.getMethod("getBackingMap")
.invoke(null);
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}

/**
* Public so it can be used across classloaders. Should not be used except by this class.
* Invoked reflectively.
*/
public static Map getBackingMap() {
if (systemInstance == null) {
systemInstance = new ConcurrentHashMap<>();
}
return systemInstance;
}

public List getRunningServices(String featureName,
Map config) {
Map<Map, List<Closeable>> services = systemInstance.get(featureName);
if (services != null) {
return services.get(config);
}
return null;
}

public Set<Closeable> getAllServices(String featureName) {
Map<Map, List<Closeable>> services = systemInstance.get(featureName);
if (services == null) {
return Set.of();
} else {
// Flatten
Set<Closeable> ls = new HashSet<>();
services.values().forEach(ls::addAll);
return ls;
}
}

public void addRunningService(String name, Map<String, String> config,
DevServicesResultBuildItem.RunnableDevService service) {
Map<Map, List<Closeable>> services = systemInstance.get(name);

if (services == null) {
services = new HashMap<>();
systemInstance.put(name, services);
}

// Make a list so that we can add and remove to it
List<Closeable> list = new ArrayList<>();
list.add(service);
services.put(config, list);
}

public void removeRunningService(String name, Map<String, String> config,
Closeable service) {
Map<Map, List<Closeable>> services = systemInstance.get(name);

if (services != null) {
List servicesForConfig = services.get(config);
if (servicesForConfig != null) {
servicesForConfig.remove(service);
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.quarkus.deployment.builditem;

import java.io.Closeable;

public interface Startable extends Closeable {
void start();
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import io.quarkus.deployment.builditem.ApplicationClassNameBuildItem;
import io.quarkus.deployment.builditem.DevServicesLauncherConfigResultBuildItem;
import io.quarkus.deployment.builditem.DevServicesNetworkIdBuildItem;
import io.quarkus.deployment.builditem.DevServicesResultBuildItem;
import io.quarkus.deployment.builditem.GeneratedClassBuildItem;
import io.quarkus.deployment.builditem.GeneratedResourceBuildItem;
import io.quarkus.deployment.builditem.MainClassBuildItem;
Expand All @@ -54,6 +55,7 @@ public class StartupActionImpl implements StartupAction {
private final String mainClassName;
private final String applicationClassName;
private final Map<String, String> devServicesProperties;
private final List<DevServicesResultBuildItem> devServicesResultBuildItems;
private final String devServicesNetworkId;
private final List<RuntimeApplicationShutdownBuildItem> runtimeApplicationShutdownBuildItems;
private final List<Closeable> runtimeCloseTasks = new ArrayList<>();
Expand All @@ -67,6 +69,9 @@ public StartupActionImpl(CuratedApplication curatedApplication, BuildResult buil
this.devServicesNetworkId = extractDevServicesNetworkId(buildResult);
this.runtimeApplicationShutdownBuildItems = buildResult.consumeMulti(RuntimeApplicationShutdownBuildItem.class);

devServicesResultBuildItems = buildResult
.consumeMulti(DevServicesResultBuildItem.class);

Map<String, byte[]> transformedClasses = extractTransformedClasses(buildResult);
QuarkusClassLoader baseClassLoader = curatedApplication.getOrCreateBaseRuntimeClassLoader();
QuarkusClassLoader runtimeClassLoader;
Expand Down Expand Up @@ -99,6 +104,8 @@ public StartupActionImpl(CuratedApplication curatedApplication, BuildResult buil
* of the JVM will exit when the app stops.
*/
public RunningQuarkusApplication runMainClass(String... args) throws Exception {
// Start dev services that weren't started in the augmentation phase
startDevServices(devServicesResultBuildItems);

//first we hack around class loading in the fork join pool
ForkJoinClassLoading.setForkJoinClassLoader(runtimeClassLoader);
Expand Down Expand Up @@ -206,6 +213,9 @@ private void doClose() {

@Override
public int runMainClassBlocking(String... args) throws Exception {
// Start dev services that weren't started in the augmentation phase
startDevServices(devServicesResultBuildItems);

//first we hack around class loading in the fork join pool
ForkJoinClassLoading.setForkJoinClassLoader(runtimeClassLoader);

Expand Down Expand Up @@ -276,10 +286,19 @@ public void overrideConfig(Map<String, String> config) {
RuntimeOverrideConfigSource.setConfig(runtimeClassLoader, config);
}

private static void startDevServices(List<DevServicesResultBuildItem> devServicesResultBuildItems) {
for (DevServicesResultBuildItem devServicesResultBuildItem : devServicesResultBuildItems) {
devServicesResultBuildItem.start();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will start services one after the other and take too much time when multiple dev services are involved.
We need to start them in parallel and wait until they are ready.

When services are started at augmentation time, we didn't have the problem because the build steps are called using the build executor service.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! As an opposite problem, @cescoffier pointed out that some dev services might have a desired ordering, which would be honoured when they're started as part of a build chain, and not honoured when I just iterate through a collection. But we agreed that (a) people probably shouldn't expect that level of orchestration and (b) if we needed to, we could add a prioritisation mechanism later. Just replicating a priority from the Processor order might be enough.

So I'll switch to doing some kind of parallelisation, and if we need an ordered parallelisation later we can cross that bridge when we come to it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, if we wanted to, we could be incremental iterate and do that as an (immediately after) follow-on work item. For now only one dev service (Redis) would actually start in the start step, and for all the others, it's a fast no-op. So we'll only see this (definitely important) problem once we start scaling this out to other dev services.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we agreed that (a) people probably shouldn't expect that level of orchestration and (b) if we needed to, we could add a prioritisation mechanism later.

I would say that if they need that, they need to use the Compose Dev Services feature.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that was sort of where Clement and I got to.

}
}

/**
* Runs the application, and returns a handle that can be used to shut it down.
*/
public RunningQuarkusApplication run(String... args) throws Exception {
// Start dev services that weren't started in the augmentation phase
startDevServices(devServicesResultBuildItems);

//first we hack around class loading in the fork join pool
ForkJoinClassLoading.setForkJoinClassLoader(runtimeClassLoader);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.quarkus.devservices.common;

import java.io.IOException;
import java.net.ServerSocket;

import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;

import io.quarkus.deployment.builditem.Startable;

public abstract class LazyContainer<SELF extends LazyContainer<SELF>> extends GenericContainer<SELF> implements
Startable {

public LazyContainer(DockerImageName dockerImageName) {
super(dockerImageName);
}

public void close() {
// Trivial, but needed to avoid ambiguous inheritance
super.close();
}

/**
* Testcontainers docs: Because the randomised port mapping happens during container startup, the container must
* be running at the time getMappedPort is called. You may need to ensure that the startup order of components
* in your tests caters for this.
*
* We need the port before the container starts, so invent them ourselves
*/
protected int findFreePort() {

try (ServerSocket socket = new ServerSocket(0);) {
// Make sure the socket becomes free as soon as we start the close operation
socket.setReuseAddress(true);
return socket.getLocalPort();
} catch (IOException e) {
throw new RuntimeException(e);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@
import com.github.dockerjava.api.model.ContainerNetworkSettings;

import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.IsNormal;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.ConsoleCommandBuildItem;
import io.quarkus.deployment.builditem.DevServicesComposeProjectBuildItem;
import io.quarkus.deployment.builditem.DevServicesLauncherConfigResultBuildItem;
import io.quarkus.deployment.builditem.DevServicesNetworkIdBuildItem;
import io.quarkus.deployment.builditem.DevServicesResultBuildItem;
import io.quarkus.deployment.builditem.DevServicesTrackerBuildItem;
import io.quarkus.deployment.builditem.DockerStatusBuildItem;
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
import io.quarkus.deployment.console.ConsoleCommand;
Expand Down Expand Up @@ -102,6 +104,11 @@ private Optional<String> getSharedNetworkId() {
}
}

@BuildStep(onlyIfNot = IsNormal.class)
DevServicesTrackerBuildItem getDevServicesTracker() {
return new DevServicesTrackerBuildItem();
}

@BuildStep(onlyIf = { IsDevelopment.class })
public List<DevServiceDescriptionBuildItem> config(
DockerStatusBuildItem dockerStatusBuildItem,
Expand Down
Loading
Loading