Skip to content

Commit cf4afd2

Browse files
Merge pull request #47610 from holly-cummins/do-not-start-dev-services-in-augmentation
Do not start redis dev services in augmentation (plus supporting framework)
2 parents bcf2b44 + f85cd0d commit cf4afd2

File tree

27 files changed

+1013
-143
lines changed

27 files changed

+1013
-143
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package io.quarkus.deployment.builditem;
2+
3+
import java.util.UUID;
4+
5+
import io.quarkus.builder.item.SimpleBuildItem;
6+
7+
/**
8+
* A unique identifier for an instance of an application.
9+
* Development and test modes will have different IDs.
10+
* The application ID will persist across continuous test restarts and dev mode restarts.
11+
* It mirrors the lifecycle of a curated application.
12+
*
13+
*/
14+
public final class ApplicationInstanceIdBuildItem extends SimpleBuildItem {
15+
16+
final UUID UUID;
17+
18+
public ApplicationInstanceIdBuildItem(UUID uuid) {
19+
this.UUID = uuid;
20+
}
21+
22+
public UUID getUUID() {
23+
return UUID;
24+
}
25+
}

core/deployment/src/main/java/io/quarkus/deployment/builditem/CuratedApplicationShutdownBuildItem.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* <p>
1414
* For production applications, this will be at the end of the Maven/Gradle build. For dev mode applications, this will be
1515
* when dev mode shuts down. For tests, it will generally be at the end of the test run. However, for continuous testing this
16-
* will be when the outer dev mode process shuts down. For unit style tests, this will usually be the end of the test.
16+
* will be when the outer dev mode process shuts down. For extension unit tests, this will usually be the end of the test.
1717
*/
1818
public final class CuratedApplicationShutdownBuildItem extends SimpleBuildItem {
1919

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package io.quarkus.deployment.builditem;
2+
3+
import java.io.Closeable;
4+
import java.util.Set;
5+
import java.util.UUID;
6+
7+
import io.quarkus.builder.item.SimpleBuildItem;
8+
import io.quarkus.deployment.dev.devservices.DevServicesConfig;
9+
import io.quarkus.devservices.crossclassloader.runtime.ComparableDevServicesConfig;
10+
import io.quarkus.devservices.crossclassloader.runtime.DevServiceOwner;
11+
import io.quarkus.devservices.crossclassloader.runtime.RunningDevServicesRegistry;
12+
import io.quarkus.runtime.LaunchMode;
13+
14+
// Ideally we would have a unique build item for each processor/feature, but that would need a new KeyedBuildItem or FeatureBuildItem type
15+
// Needs to be in core because DevServicesResultBuildItem is in core
16+
public final class DevServicesRegistryBuildItem extends SimpleBuildItem {
17+
18+
// This is a fairly thin wrapper around the tracker, so the tracker can be loaded with the system classloader
19+
// The QuarkusClassLoader takes care of loading the tracker with the right classloader
20+
private final RunningDevServicesRegistry tracker;
21+
private final UUID uuid;
22+
private final DevServicesConfig globalConfig;
23+
private final LaunchMode launchMode;
24+
25+
public DevServicesRegistryBuildItem(UUID uuid, DevServicesConfig globalDevServicesConfig, LaunchMode launchMode) {
26+
this.launchMode = launchMode;
27+
this.tracker = new RunningDevServicesRegistry();
28+
this.uuid = uuid;
29+
this.globalConfig = globalDevServicesConfig;
30+
}
31+
32+
public Set<Closeable> getRunningServices(String featureName, String configName, Object identifyingConfig) {
33+
DevServiceOwner owner = new DevServiceOwner(featureName, launchMode.name(), configName);
34+
ComparableDevServicesConfig key = new ComparableDevServicesConfig(uuid, owner, globalConfig, identifyingConfig);
35+
return tracker.getRunningServices(key);
36+
}
37+
38+
public Set<Closeable> getAllRunningServices(String featureName, String configName) {
39+
DevServiceOwner owner = new DevServiceOwner(featureName, launchMode.name(), configName);
40+
return tracker.getAllRunningServices(owner);
41+
}
42+
43+
public void addRunningService(String featureName, String configName, Object identifyingConfig,
44+
DevServicesResultBuildItem.RunnableDevService service) {
45+
DevServiceOwner owner = new DevServiceOwner(featureName, launchMode.name(), configName);
46+
ComparableDevServicesConfig key = new ComparableDevServicesConfig(uuid, owner, globalConfig, identifyingConfig);
47+
tracker.addRunningService(key, service);
48+
}
49+
50+
public void removeRunningService(String featureName, String configName, Object identifyingConfig,
51+
DevServicesResultBuildItem.RunnableDevService service) {
52+
DevServiceOwner owner = new DevServiceOwner(featureName, launchMode.name(), configName);
53+
ComparableDevServicesConfig key = new ComparableDevServicesConfig(uuid, owner, globalConfig, identifyingConfig);
54+
tracker.removeRunningService(key, service);
55+
}
56+
57+
public void closeAllRunningServices() {
58+
tracker.closeAllRunningServices(launchMode.name());
59+
}
60+
61+
}

core/deployment/src/main/java/io/quarkus/deployment/builditem/DevServicesResultBuildItem.java

Lines changed: 172 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,35 @@
22

33
import java.io.Closeable;
44
import java.io.IOException;
5+
import java.util.Collection;
56
import java.util.Collections;
67
import java.util.HashMap;
78
import java.util.Map;
9+
import java.util.function.Supplier;
10+
11+
import org.jboss.logging.Logger;
812

913
import io.quarkus.builder.item.MultiBuildItem;
1014

1115
/**
1216
* BuildItem for running dev services.
1317
* Combines injected configs to the application with container id (if it exists).
14-
*
18+
* <p>
1519
* Processors are expected to return this build item not only when the dev service first starts,
1620
* but also if a running dev service already exists.
17-
*
21+
* <p>
1822
* {@link RunningDevService} helps to manage the lifecycle of the running dev service.
1923
*/
2024
public final class DevServicesResultBuildItem extends MultiBuildItem {
2125

26+
private static final Logger log = Logger.getLogger(DevServicesResultBuildItem.class);
27+
2228
private final String name;
2329
private final String description;
30+
// Will be null if there is a runnable dev service
2431
private final String containerId;
25-
private final Map<String, String> config;
32+
protected final Map<String, String> config;
33+
protected RunnableDevService runnableDevService;
2634

2735
public DevServicesResultBuildItem(String name, String containerId, Map<String, String> config) {
2836
this(name, null, containerId, config);
@@ -35,6 +43,12 @@ public DevServicesResultBuildItem(String name, String description, String contai
3543
this.config = config;
3644
}
3745

46+
public DevServicesResultBuildItem(String name, String description, Map<String, String> config,
47+
RunnableDevService runnableDevService) {
48+
this(name, description, null, config);
49+
this.runnableDevService = runnableDevService;
50+
}
51+
3852
public String getName() {
3953
return name;
4054
}
@@ -44,20 +58,42 @@ public String getDescription() {
4458
}
4559

4660
public String getContainerId() {
47-
return containerId;
61+
if (runnableDevService != null) {
62+
return runnableDevService.getContainerId();
63+
} else {
64+
return containerId;
65+
}
4866
}
4967

5068
public Map<String, String> getConfig() {
5169
return config;
5270
}
5371

72+
public void start() {
73+
if (runnableDevService != null) {
74+
runnableDevService.start();
75+
} else {
76+
log.debugf("Not starting %s because runnable dev service is null (it is probably a running dev service.", name);
77+
}
78+
}
79+
80+
// Ideally everyone would use the config source, but if people need to ask for config directly, make it possible
81+
public Map<String, String> getDynamicConfig() {
82+
if (runnableDevService != null && runnableDevService.isRunning()) {
83+
return runnableDevService.get();
84+
} else {
85+
return Collections.emptyMap();
86+
}
87+
}
88+
5489
public static class RunningDevService implements Closeable {
5590

56-
private final String name;
57-
private final String description;
58-
private final String containerId;
59-
private final Map<String, String> config;
60-
private final Closeable closeable;
91+
protected final String name;
92+
protected final String description;
93+
protected final String containerId;
94+
protected final Map<String, String> config;
95+
protected final Closeable closeable;
96+
protected volatile boolean isRunning = true;
6197

6298
private static Map<String, String> mapOf(String key, String value) {
6399
Map<String, String> map = new HashMap<>();
@@ -109,6 +145,9 @@ public Closeable getCloseable() {
109145
return closeable;
110146
}
111147

148+
// This method should be on RunningDevService, but not on RunnableDevService, where we use different logic to
149+
// decide when it's time to close a container. For now, leave it where it is and hope it doesn't get called when it shouldn't.
150+
// We can either make a common parent class or throw unsupported when this is called from Runnable.
112151
public boolean isOwner() {
113152
return closeable != null;
114153
}
@@ -117,11 +156,135 @@ public boolean isOwner() {
117156
public void close() throws IOException {
118157
if (this.closeable != null) {
119158
this.closeable.close();
159+
isRunning = false;
120160
}
121161
}
122162

123163
public DevServicesResultBuildItem toBuildItem() {
124164
return new DevServicesResultBuildItem(name, description, containerId, config);
125165
}
126166
}
167+
168+
public static class RunnableDevService extends RunningDevService implements Supplier<Map<String, String>> {
169+
170+
private final DevServicesRegistryBuildItem tracker;
171+
private final Startable container;
172+
private final Object identifyingConfig;
173+
private final String featureName;
174+
private final String configName;
175+
private final Map<String, Supplier<String>> lazyConfig;
176+
177+
/**
178+
* There are several configs in this argument, but there's a reason! (For now, at least.)
179+
* The identifying config object is the user-defined config, and are what we use to establish ownership and reusability.
180+
* The config name is used to identify sub-configuration.
181+
* The first feature name is generated by the processor.
182+
*/
183+
public RunnableDevService(String featureName, String configName, Startable container,
184+
Map lazyConfig,
185+
Object identifyingConfig,
186+
DevServicesRegistryBuildItem tracker) {
187+
super(featureName, null, container::close, Collections.emptyMap());
188+
189+
this.featureName = featureName;
190+
this.configName = configName;
191+
this.container = container;
192+
this.tracker = tracker;
193+
isRunning = false;
194+
this.lazyConfig = lazyConfig;
195+
this.identifyingConfig = identifyingConfig;
196+
}
197+
198+
public boolean isRunning() {
199+
return isRunning;
200+
}
201+
202+
@Override
203+
public String getContainerId() {
204+
return container != null ? container.getContainerId() : null;
205+
}
206+
207+
/**
208+
* Starts the service, after first checking for a compatible service in the tracker.
209+
* Calling classes may wish to do their own checks for compatible services before calling start().
210+
*/
211+
public void start() {
212+
// 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
213+
// We figure out if we need to shut down existing redis containers that might have been started in previous profiles or restarts
214+
215+
// These RunnableDevService classes could be from another classloader, so don't make assumptions about the class
216+
Collection<?> matchedDevServices = tracker.getRunningServices(featureName, configName, identifyingConfig);
217+
// 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
218+
if (matchedDevServices == null || matchedDevServices.isEmpty()) {
219+
// There isn't a running container that has the right config, we need to do work
220+
// Let's get all the running dev services associated with this feature (+ launch mode plus named section), so we can close them
221+
closeOwnedServices();
222+
223+
reallyStart();
224+
}
225+
}
226+
227+
private void closeOwnedServices() {
228+
Collection<Closeable> unusableDevServices = tracker.getAllRunningServices(featureName, configName);
229+
if (unusableDevServices != null) {
230+
for (Closeable closeable : unusableDevServices) {
231+
try {
232+
closeable.close();
233+
} catch (IOException e) {
234+
throw new RuntimeException(e);
235+
}
236+
}
237+
}
238+
}
239+
240+
/**
241+
* Starts, without doing any duplicate checking, and without doing any cleanup.
242+
* The duplicate checking is optional, the cleanup is not.
243+
*/
244+
private void reallyStart() {
245+
if (container != null) {
246+
synchronized (this) {
247+
container.start();
248+
249+
// tell the tracker that we started
250+
isRunning = true;
251+
tracker.addRunningService(featureName, configName, identifyingConfig, this);
252+
}
253+
// Ideally we'd print out a port number here, but we can only do that if we add a dependency on GenericContainer (or update startable to add a method)
254+
255+
log.infof("The %s dev service is ready to accept connections on %s", name, container.getConnectionInfo());
256+
} else {
257+
throw new IllegalStateException("Internal error: attempted to start a null container.");
258+
}
259+
}
260+
261+
@Override
262+
public void close() throws IOException {
263+
tracker.removeRunningService(featureName, configName, identifyingConfig, this);
264+
super.close();
265+
}
266+
267+
public DevServicesResultBuildItem toBuildItem() {
268+
return new DevServicesResultBuildItem(name, description, config, this);
269+
}
270+
271+
/**
272+
* This is a supplier interface to maintain type-compatibility across classloaders.
273+
* What this is actually giving is an aggregate of the hardcoded and lazy (dynamic at runtime) config.
274+
*
275+
*/
276+
@Override
277+
public Map<String, String> get() {
278+
// printlns show this gets called super often, so want to be as efficient as we can in this code
279+
Map<String, String> newConfig = new HashMap<>(getConfig());
280+
// We don't want to be returning config while the container is in the process of starting, so synchronize
281+
synchronized (this) {
282+
for (Map.Entry<String, Supplier<String>> entry : lazyConfig.entrySet()) {
283+
newConfig.put(entry.getKey(), entry.getValue().get());
284+
}
285+
}
286+
return newConfig;
287+
}
288+
289+
}
127290
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.quarkus.deployment.builditem;
2+
3+
import java.io.Closeable;
4+
5+
public interface Startable extends Closeable {
6+
void start();
7+
8+
String getConnectionInfo();
9+
10+
// This starts to couple to containers, so we could move it to sub-interface and use that in dev services
11+
String getContainerId();
12+
13+
}

0 commit comments

Comments
 (0)