22
33import java .io .Closeable ;
44import java .io .IOException ;
5+ import java .util .Collection ;
56import java .util .Collections ;
67import java .util .HashMap ;
78import java .util .Map ;
9+ import java .util .function .Supplier ;
10+
11+ import org .jboss .logging .Logger ;
812
913import 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 */
2024public 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}
0 commit comments