Skip to content

Commit 544f3df

Browse files
jfallowsclaude
andauthored
Separate engine initialization from startup (#1808)
* feat(engine): split Engine.start() into init() + start() with beforeStart hook Introduce Engine.init() to start workers and boss without attaching bindings; it is idempotent. Engine.start() now calls init() implicitly before reading config and attaching bindings, so existing callers (CLI start command, embedded users) are unaffected. EngineRule gains a beforeStart(Runnable) builder (default no-op). The rule now calls engine.init(), then runs the beforeStart hook and engine.start() on a separate thread, allowing tests to block binding startup against an external initialization signal (e.g. k3po awaitPrepared) before bindings attach. Closes #1807 https://claude.ai/code/session_01XcNeWBUJAA87NSf9R4KdVT * refactor(engine): only spawn EngineRule start thread when beforeStart is set Keep beforeStart null by default and branch on it: when no hook is set, EngineRule starts the engine synchronously on the test thread exactly as before, with no background thread. The init() + background start() path is used only when a beforeStart hook is provided, preserving existing behavior precisely for the common case. https://claude.ai/code/session_01XcNeWBUJAA87NSf9R4KdVT * refactor(engine): drop redundant beforeStart null assignment in EngineRule Address review feedback: remove the explicit null initialization since null is the implicit default for the reference field. https://claude.ai/code/session_01XcNeWBUJAA87NSf9R4KdVT --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7527c66 commit 544f3df

3 files changed

Lines changed: 115 additions & 5 deletions

File tree

runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/Engine.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ public final class Engine implements Collector, AutoCloseable
105105
private final RouterConfig routerConfig;
106106

107107
private final EventWriter eventWriter;
108+
private final AtomicBoolean initialized;
108109
private final AtomicBoolean closed;
109110

110111
private FileSystem fileSystem = null;
@@ -271,6 +272,7 @@ public final class Engine implements Collector, AutoCloseable
271272
this.readonly = readonly;
272273
this.manager = manager;
273274
this.diagnostics = diagnostics;
275+
this.initialized = new AtomicBoolean(false);
274276
this.closed = new AtomicBoolean(false);
275277
}
276278

@@ -290,14 +292,23 @@ private void process(
290292
manager.process(config);
291293
}
292294

293-
public void start() throws Exception
295+
public void init()
294296
{
295-
for (EngineWorker worker : workers)
297+
if (initialized.compareAndSet(false, true))
296298
{
297-
worker.doStart();
299+
for (EngineWorker worker : workers)
300+
{
301+
worker.doStart();
302+
}
303+
304+
boss.doStart();
298305
}
306+
}
299307

300-
boss.doStart();
308+
public void start() throws Exception
309+
{
310+
// start workers and boss if init() has not already been called; idempotent
311+
init();
301312

302313
// ignore the config file in read-only mode; no config will be read so no namespaces, bindings, etc. will be attached
303314
if (!readonly)

runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/internal/EngineTest.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,53 @@ public void shouldConfigureEmpty()
7474
}
7575
}
7676

77+
@Test
78+
public void shouldInitThenStart()
79+
{
80+
EngineConfiguration config = new EngineConfiguration(properties);
81+
List<Throwable> errors = new LinkedList<>();
82+
try (Engine engine = Engine.builder()
83+
.config(config)
84+
.errorHandler(errors::add)
85+
.build())
86+
{
87+
engine.init();
88+
engine.start();
89+
}
90+
catch (Throwable ex)
91+
{
92+
errors.add(ex);
93+
}
94+
finally
95+
{
96+
assertThat(errors, empty());
97+
}
98+
}
99+
100+
@Test
101+
public void shouldInitIdempotent()
102+
{
103+
EngineConfiguration config = new EngineConfiguration(properties);
104+
List<Throwable> errors = new LinkedList<>();
105+
try (Engine engine = Engine.builder()
106+
.config(config)
107+
.errorHandler(errors::add)
108+
.build())
109+
{
110+
engine.init();
111+
engine.init();
112+
engine.start();
113+
}
114+
catch (Throwable ex)
115+
{
116+
errors.add(ex);
117+
}
118+
finally
119+
{
120+
assertThat(errors, empty());
121+
}
122+
}
123+
77124
@Test
78125
public void shouldConfigure()
79126
{

runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/EngineRule.java

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ public final class EngineRule implements TestRule
9393
private Predicate<String> exceptions;
9494
private boolean interruptible;
9595
private boolean clean;
96+
private Runnable beforeStart;
9697

9798
public EngineRule()
9899
{
@@ -180,6 +181,13 @@ public EngineRule clean()
180181
return this;
181182
}
182183

184+
public EngineRule beforeStart(
185+
Runnable beforeStart)
186+
{
187+
this.beforeStart = requireNonNull(beforeStart);
188+
return this;
189+
}
190+
183191
public void close()
184192
{
185193
try
@@ -391,9 +399,49 @@ public void evaluate() throws Throwable
391399
.errorHandler(errorHandler)
392400
.build();
393401

402+
// when no beforeStart hook is set, preserve existing behavior exactly: start (and implicitly
403+
// init) the engine synchronously on the test thread before the test body runs.
404+
// when a hook is set, init the engine eagerly and run the hook followed by start() on a
405+
// separate thread so the hook can block (e.g. waiting for an external runtime to be ready)
406+
// without stalling the test body, while bindings attach once the hook returns.
407+
final Thread starter;
408+
if (beforeStart == null)
409+
{
410+
starter = null;
411+
}
412+
else
413+
{
414+
engine.init();
415+
starter = new Thread(() ->
416+
{
417+
try
418+
{
419+
beforeStart.run();
420+
engine.start();
421+
}
422+
catch (Throwable t)
423+
{
424+
errors.add(t);
425+
426+
if (interruptible)
427+
{
428+
baseThread.interrupt();
429+
}
430+
}
431+
});
432+
starter.setName("engine-rule-before-start");
433+
}
434+
394435
try
395436
{
396-
engine.start();
437+
if (starter != null)
438+
{
439+
starter.start();
440+
}
441+
else
442+
{
443+
engine.start();
444+
}
397445

398446
base.evaluate();
399447
}
@@ -405,6 +453,10 @@ public void evaluate() throws Throwable
405453
{
406454
try
407455
{
456+
if (starter != null)
457+
{
458+
starter.join();
459+
}
408460
engine.close();
409461
}
410462
catch (Throwable t)

0 commit comments

Comments
 (0)