Engine extensions
Add global engine and scenario lifecycle contributions without importing scoped service APIs.
Use an EngineExtension for behavior that belongs to the engine or to every scenario it runs, such
as diagnostics, observation, or application-owned resources. Its contract is in
me.whereareiam.anvil:api. A capability, platform provider, or cache implementation uses its own
scoped extension point.
Observe scenario outcomes
This complete extension reports when a ready scenario is attached and when it finishes. Put it in
src/main/java/com/example/diagnostics/ScenarioLogExtension.java. It depends on api, Lombok, and
JetBrains annotations; the application supplies its message sink.
package com.example.diagnostics;
import lombok.RequiredArgsConstructor;
import me.whereareiam.anvil.api.engine.EngineExtension;
import me.whereareiam.anvil.api.engine.EngineRegistration;
import org.jetbrains.annotations.NotNull;
import java.util.function.Consumer;
/**
* Observes each ready scenario and its eventual lifecycle outcome.
*/
@RequiredArgsConstructor
public final class ScenarioLogExtension implements EngineExtension {
private final @NotNull Consumer<String> messages;
@Override
public void install(@NotNull EngineRegistration registration) {
registration.scenarios(scenario -> {
String name = scenario.definition().getName();
messages.accept("Started " + name);
return successful -> messages.accept((successful ? "Completed " : "Failed ") + name);
});
}
}The returned ScenarioAttachment belongs to one scenario. The engine installs it before the setup
hook and finalizes it before the underlying scenario resources. If a later attachment or setup hook
fails, already-installed attachments receive an unsuccessful outcome.
Install the extension
Use the embedding classpath, including the platform
providers and protocol libraries your scenario needs. This fragment assumes supplied EngineOptions options
and AnvilScenario scenario:
import com.example.diagnostics.ScenarioLogExtension;
import me.whereareiam.anvil.launcher.AnvilLauncher;
try (var engine = AnvilLauncher.builder()
.options(options)
.extension(new ScenarioLogExtension(System.out::println))
.build();
var context = engine.start(scenario)) {
System.out.println("Ready processes: " + context.processes().all().size());
}For a normal run, the sink receives Started <name> followed by Completed <name>. A failed setup
produces Failed <name>. context.close() means normal caller completion; an application that
catches a failed journey should call context.finish(false). Prior lifecycle failures still
prevent successful finalization.
The builder can be consumed once. Extensions install during build(), before any scenario starts.
Use explicit .extension(...) registration for application contributions; no service file is needed
for this example.
Assign resource ownership
registration.own(resource) transfers a shared AutoCloseable to the engine. Shared resources close
in reverse registration order after all scenarios, including when a later extension fails to install.
An attachment owns only resources acquired for its scenario. Release untransferred acquisitions
inside an attach call that fails before returning its attachment.
The logging example borrows its message sink and does not close System.out. If your extension
opens a file or subscribes to an external observer, assign that resource to the engine or return a
scenario attachment that closes it at the appropriate lifetime.
Supply a custom scenario assembly
Application extensions add lifecycle behavior; they do not select the engine's scenario factory.
A custom assembly supplies its ScenarioFactory directly to the core engine builder constructor:
import me.whereareiam.anvil.engine.AnvilEngineBuilder;
// factory and options are supplied by your assembly.
try (var engine = new AnvilEngineBuilder(factory).options(options).build()) {
// Prepare or start scenarios through engine.
}This requires the engine artifact alongside api. The factory is borrowed. If it depends on
shared resources that should close with the engine, transfer them through an extension using
registration.own(resource); release any resources that have not been transferred if assembly fails.
ScenarioFactory.create(scenario, observer) acquires the complete topology and returns an owned ScenarioContext
before any process generation starts. Implement the context's start() to bring remaining
processes to readiness; the engine then attaches global extensions and runs setup once. Both
contracts are in me.whereareiam.anvil.api.scenario.
The launcher supplies the default scenario factory. Applications building a different assembly bind their scoped services behind the same lifecycle boundary. The engine's full-start convenience prepares a context and completes its startup; individual controls use that context's process group.
A failed acquisition releases resources before returning. A prepared owner must release all its
resources through finish(successful), including inputs for processes that never started. Honor
the supplied observer before each generation launches and expose that managed handle through later
process lookup. Execution assigns an opaque RunningProcess.executionId() UUID; consumers use
that identity when separating replacement output. See individual process controls
for the consumer lifecycle.
Test registration failures, setup failures, repeated closure, attachment order, and the final outcome. See Lifecycle and ownership for the framework's ordering guarantees.