Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions .github/workflows/maven-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
description: Run a release to maven central
default: false
push:
branches:
- main
- master
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- main
- master


permissions:
Expand All @@ -16,11 +24,26 @@
pull-requests: write

jobs:
GuicedInjection:
verify:
if: ${{ github.event_name == 'pull_request' && github.event.action != 'closed' }}
uses: GuicedEE/Workflows/.github/workflows/projects.yml@master

Check warning

Code scanning / CodeQL

Unpinned tag for a non-immutable Action or reusable workflow Medium

Job
Job: verify
in 'Guiced Inject' uses reusable workflow 'GuicedEE/Workflows/.github/workflows/projects.yml' with ref 'master', not a pinned commit hash
with:
baseDir: ''
name: 'Guiced Injection'
sonarProjectName: 'GuicedEE_GuicedInjection'
sonarOrganization: 'guicedee'
secrets: inherit
skipDeploy: 'true'
secrets: inherit
deploy:
if: >-
github.event_name == 'push' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true)
uses: GuicedEE/Workflows/.github/workflows/projects.yml@master
with:
baseDir: ''
name: 'Guiced Injection'
sonarProjectName: 'GuicedEE_GuicedInjection'
sonarOrganization: 'guicedee'
publishToCentral: ${{inputs.centralRelease}}
secrets: inherit
5 changes: 4 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<groupId>com.guicedee</groupId>
<artifactId>inject</artifactId>
<packaging>jar</packaging>
<version>2.2.3</version>
<version>2.2.3-SNAPSHOT</version>
<name>Guiced Injector</name>
<description>GuicedEE Inject is an open-source Guice integration library that discovers binders/modules across JARs via SPI and classpath scanning, bootstraps logging, job services, and JRT URL handling, and keeps optional adapters (e.g., Vert.x) isolated. Requires Java 25 LTS.</description>
<url>https://guicedee.com</url>
Expand Down Expand Up @@ -40,6 +40,9 @@
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.guicedee</groupId><artifactId>client</artifactId><version>2.2.3-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.guicedee</groupId>
<artifactId>fasterxml-bom</artifactId>
Expand Down
101 changes: 101 additions & 0 deletions src/main/java/com/guicedee/guicedinjection/GuiceApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.guicedee.guicedinjection;

import com.guicedee.client.Environment;
import com.guicedee.client.IGuiceContext;
import org.apache.logging.log4j.LogManager;

import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;

/** Process entry boundary. Returns after startup; failed startup cleans up and exits unsuccessfully. */
public final class GuiceApplication {
private GuiceApplication() {}

/**
* Runs preparation and injection within GUICEDEE_STARTUP_TIMEOUT_SECONDS (default 300).
* Failure has GUICEDEE_FAILED_STARTUP_CLEANUP_SECONDS (default 45) to stop the process,
* including cleanup and JVM shutdown hooks. This method is for main entry points only.
*/
public static void run(Runnable preparation) {
Objects.requireNonNull(preparation);
var context = new AtomicReference<IGuiceContext>();
var phase = new AtomicReference<>("deadline configuration");
Thread startup = null;
int cleanupSeconds = 45;
boolean interrupted = false;
try {
cleanupSeconds = seconds("GUICEDEE_FAILED_STARTUP_CLEANUP_SECONDS", 45, 120);
int startupSeconds = seconds("GUICEDEE_STARTUP_TIMEOUT_SECONDS", 300, 1800);
var ready = new CompletableFuture<Void>();
// Preserve normal non-daemon inheritance for resources created by startup hooks.
startup = Thread.ofPlatform().daemon(false).name("guicedee-application-startup").start(() -> {
try {
phase.set("context discovery");
var active = IGuiceContext.instance();
if (active instanceof GuiceContext guice) guice.manageProcessLifecycle();
context.set(active);
phase.set("application preparation");
preparation.run();
phase.set("injector initialization");
active.inject();
phase.set("asynchronous startup");
active.getLoadingFinished().onComplete(result -> {
if (result.succeeded()) ready.complete(null);
else ready.completeExceptionally(result.cause());
});
} catch (Throwable failed) {
ready.completeExceptionally(failed);
}
});
ready.get(startupSeconds, TimeUnit.SECONDS);
return;
} catch (InterruptedException cancelled) {
interrupted = true;
phase.set("interrupted startup");
} catch (TimeoutException expired) {
phase.set("startup deadline exceeded");
} catch (Throwable failed) {
// Retain the fixed phase without repeating configuration values or raw causes.
}

// A stuck cleanup hook or JVM shutdown hook must not leave a failed process serving traffic.
final int cleanupBudget = cleanupSeconds;
Thread.ofPlatform().daemon().name("guicedee-failed-startup-deadline").start(() -> {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(cleanupBudget);
while (System.nanoTime() < deadline) {
try {
TimeUnit.NANOSECONDS.sleep(Math.max(1, deadline - System.nanoTime()));
} catch (InterruptedException ignored) {
// Preserve the absolute failure deadline.
}
}
Runtime.getRuntime().halt(1);
});
LogManager.getLogger(GuiceApplication.class).error("Application startup failed during {}; shutting down", phase.get());
if (startup != null) startup.interrupt();
try {
var active = context.get();
if (active != null) active.destroy();
} catch (Throwable failedCleanup) {
LogManager.getLogger(GuiceApplication.class).error("Application startup cleanup failed");
} finally {
if (interrupted) Thread.currentThread().interrupt();
System.exit(1);
}
}

private static int seconds(String name, int fallback, int maximum) {
try {
String value = Environment.getSystemPropertyOrEnvironment(name, Integer.toString(fallback));
if (!value.matches("[1-9][0-9]{0,3}")) throw new IllegalArgumentException();
int seconds = Integer.parseInt(value);
if (seconds > maximum) throw new IllegalArgumentException();
return seconds;
} catch (Exception invalid) {
throw new IllegalArgumentException("Invalid application startup deadline");
}
}
}
143 changes: 91 additions & 52 deletions src/main/java/com/guicedee/guicedinjection/GuiceContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ private static org.apache.logging.log4j.core.Layout<?> buildConsoleLayout(Consol
/**
* The physical injector for the JVM container
*/
private Injector injector;
private volatile Injector injector;
/**
* The actual scanner
*/
Expand All @@ -451,6 +451,15 @@ private static org.apache.logging.log4j.core.Layout<?> buildConsoleLayout(Consol
private static boolean configured;

private final CompletableFuture<Void> loadingFinished = new CompletableFuture<>();
private final java.util.concurrent.atomic.AtomicBoolean shutdownStarted = new java.util.concurrent.atomic.AtomicBoolean();
private final CompletableFuture<Void> shutdownFinished = new CompletableFuture<>();
private volatile Thread shutdownOwner;
private volatile boolean processLifecycle;

/** Opts a process launcher into terminal, exactly-once shutdown. Embedded callers retain their lifecycle. */
void manageProcessLifecycle() {
processLifecycle = true;
}

/**
* Creates a new Guice context. Not necessary
Expand All @@ -459,13 +468,18 @@ private GuiceContext() {

}

/** Returns the already-created injector without starting application services. */
@Override
public Optional<Injector> existingInjector() {
return Optional.ofNullable(injector);
}

/**
* Reference the Injector Directly
*
* @return The global Guice Injector Object, Never Null, Instantiates the Injector if not configured
* Returns the global injector, creating it and starting lifecycle work when needed.
* Await {@link #getLoadingFinished()} to observe asynchronous startup success or failure.
*/

public Injector inject() {
if (processLifecycle && shutdownStarted.get() && injector == null) throw new IllegalStateException("Guice context is stopped");
if (GuiceContext.buildingInjector) {
log.error("💥 The injector is being called recursively during build. Place such actions in a IGuicePostStartup or use the IGuicePreStartup Service Loader.");
new IllegalStateException("The injector is being called recursively during build. Place such actions in a IGuicePostStartup or use the IGuicePreStartup Service Loader.").printStackTrace();
Expand Down Expand Up @@ -521,7 +535,10 @@ public Injector inject() {
GuiceContext.instance()
.loadPreDestroyServices();
}).subscribe().with(a -> {
log.trace("Subcription for post startups completed - " + a);
log.trace("Subscription for post startups completed - " + a);
}, failure -> {
loadingFinished.completeExceptionally(failure);
log.error("Post-startup initialization failed", failure);
});
Runtime
.getRuntime()
Expand All @@ -535,6 +552,7 @@ public void run() {
} catch (Throwable e) {
GuiceContext.buildingInjector = false;
log.error("💥 Critical failure during dependency injection system initialization: {}", e.getMessage(), e);
loadingFinished.completeExceptionally(e);
throw new RuntimeException("Unable to boot Guice Injector", e);
}
}
Expand All @@ -547,54 +565,62 @@ public void run() {
*/
@SuppressWarnings("unused")
public void destroy() {
log.info("🛑 Starting Guice Context shutdown and resource cleanup");
Stopwatch shutdownStopwatch = Stopwatch.createStarted();

if (processLifecycle && !shutdownStarted.compareAndSet(false, true)) {
if (shutdownOwner != Thread.currentThread()) shutdownFinished.join();
return;
}
shutdownOwner = Thread.currentThread();
if (processLifecycle) loadingFinished.completeExceptionally(new IllegalStateException("Guice context is stopping"));
try {
Set<IGuicePreDestroy> destroyers = loadPreDestroyServices();
log.debug("🗑️ Executing {} pre-destroy services for cleanup", destroyers.size());
log.info("🛑 Starting Guice Context shutdown and resource cleanup");
Stopwatch shutdownStopwatch = Stopwatch.createStarted();

int successCount = 0;
int failureCount = 0;

for (IGuicePreDestroy destroyer : destroyers) {
String destroyerName = destroyer.getClass().getCanonicalName();
log.debug("🗑️ Running pre-destroy service: {}", destroyerName);

try {
destroyer.onDestroy();
successCount++;
log.debug("✅ Successfully executed pre-destroy service: {}", destroyerName);
} catch (Throwable T) {
failureCount++;
log.error("❌ Failed to run destroyer '{}': {}", destroyerName, T.getMessage(), T);
try {
Set<IGuicePreDestroy> destroyers = loadPreDestroyServices();
log.debug("🗑️ Executing {} pre-destroy services for cleanup", destroyers.size());

int successCount = 0;
int failureCount = 0;

for (IGuicePreDestroy destroyer : destroyers) {
String destroyerName = destroyer.getClass().getCanonicalName();
log.debug("🗑️ Running pre-destroy service: {}", destroyerName);

try {
destroyer.onDestroy();
successCount++;
log.debug("✅ Successfully executed pre-destroy service: {}", destroyerName);
} catch (Throwable T) {
failureCount++;
log.error("❌ Failed to run destroyer '{}': {}", destroyerName, T.getMessage(), T);
}
}
}

log.info("📊 Pre-destroy services execution completed - Success: {}, Failed: {}",
successCount, failureCount);
log.info("📊 Pre-destroy services execution completed - Success: {}, Failed: {}",
successCount, failureCount);

} catch (Throwable T) {
log.error("💥 Failed to run destroyers: {}", T.getMessage(), T);
}
} catch (Throwable T) {
log.error("💥 Failed to run destroyers: {}", T.getMessage(), T);
}

log.debug("🧹 Cleaning up scanner resources");
if (GuiceContext.instance().scanResult != null) {
GuiceContext.instance().scanResult.close();
log.debug("✅ Scan result resources released");
}
log.debug("🧹 Cleaning up scanner resources");
if (GuiceContext.instance().scanResult != null) {
GuiceContext.instance().scanResult.close();
log.debug("✅ Scan result resources released");
}

// Clear all references
GuiceContext.instance().scanResult = null;
GuiceContext.instance().scanner = null;
GuiceContext.instance().injector = null;
GuiceContext.configured = false;
GuiceContext.config.reset();
IGuiceContext.getAllLoadedServices().clear();

shutdownStopwatch.stop();
log.info("🎉 Guice Context shutdown completed in {}ms",
shutdownStopwatch.elapsed(TimeUnit.MILLISECONDS));
// Clear all references
GuiceContext.instance().scanResult = null;
GuiceContext.instance().scanner = null;
GuiceContext.instance().injector = null;
GuiceContext.configured = false;
GuiceContext.config.reset();
IGuiceContext.getAllLoadedServices().clear();

shutdownStopwatch.stop();
log.info("🎉 Guice Context shutdown completed in {}ms",
shutdownStopwatch.elapsed(TimeUnit.MILLISECONDS));
} finally { shutdownFinished.complete(null); }
}

/**
Expand Down Expand Up @@ -1230,6 +1256,7 @@ private Uni<Boolean> loadPostStartups() {
return Multi.createFrom().iterable(groupedStartups.entrySet())
.onItem()
.transformToUniAndConcatenate(entry -> {
if (shutdownStarted.get()) return Uni.createFrom().failure(new IllegalStateException("Guice context is stopping"));
int sortOrder = entry.getKey();
List<IGuicePostStartup<?>> group = entry.getValue();
// group.sort(Comparator.comparing(IGuicePostStartup::sortOrder));
Expand All @@ -1243,12 +1270,14 @@ private Uni<Boolean> loadPostStartups() {
if (postLoadResults != null) {
for (Uni<Boolean> postLoadResult : postLoadResults) {
Uni<Boolean> onContext = Uni.createFrom().<Boolean>emitter(em ->
vertx.runOnContext(v ->
vertx.runOnContext(v -> {
try {
postLoadResult
.invoke(a -> log.trace("✅ Completed postload : " + startup.getClass().getCanonicalName()))
.onItem().transform(a -> true)
.subscribe().with(em::complete, em::fail)
)
.subscribe().with(em::complete, em::fail);
} catch (Throwable failure) { em.fail(failure); }
})
);
startupsInGroup.add(onContext);
}
Expand All @@ -1265,9 +1294,13 @@ private Uni<Boolean> loadPostStartups() {
})
.collect().asList()
.replaceWith(true)
.onItem().invoke(ignored -> {
if (shutdownStarted.get()) throw new IllegalStateException("Guice context is stopping");
loadingFinished.complete(null);
})
.onFailure().invoke(loadingFinished::completeExceptionally)
.eventually(() -> {
totalStopwatch.stop();
loadingFinished.complete(null);
log.info("🎉 Post-startup initialization setup completed in {}ms",
totalStopwatch.elapsed(TimeUnit.MILLISECONDS));
});
Expand Down Expand Up @@ -1337,7 +1370,13 @@ public Set<IGuicePreStartup> loadPreStartupServices() {
* @return the set of pre-destroy services
*/
public Set<IGuicePreDestroy> loadPreDestroyServices() {
return new LinkedHashSet<>(getLoader(IGuicePreDestroy.class, true, ServiceLoader.load(IGuicePreDestroy.class)));
// IDefaultService.compareTo is not a valid equality comparator for equal priorities.
// Use an explicit total ordering and retain every distinct provider.
Set<IGuicePreDestroy> hooks = getLoader(IGuicePreDestroy.class, true, ServiceLoader.load(IGuicePreDestroy.class));
return hooks.stream()
.sorted(Comparator.comparingInt((IGuicePreDestroy hook) -> hook.shutdownSortOrder())
.thenComparing(hook -> hook.getClass().getName()))
.collect(Collectors.toCollection(LinkedHashSet::new));
}

/**
Expand Down
Loading
Loading