Skip to content

Commit a131a80

Browse files
authored
phase a/fix bootstrap listener (#47)
* fix(serve): invoke GraphBootstrapper directly instead of via dead ApplicationReadyEvent listener GraphBootstrapper's H2->Neo4j fallback was registered for @eventlistener(ApplicationReadyEvent.class), but that event never fires during a normal serve run because ServeCommand.call() blocks in Thread.join() as a CommandLineRunner. Consequence: if someone shipped a bundle with only the H2 analysis cache (no enriched Neo4j graph) and ran `code-iq serve`, Neo4j would stay empty forever and graph-traversal queries would return nothing. Not observed in our pipeline today only because `enrich` always runs before `serve`. Fix: - Drop @eventlistener from bootstrapNeo4jFromCache() and its associated imports (ApplicationReadyEvent, @eventlistener). - ServeCommand.call() now calls graphBootstrapper.bootstrapNeo4jFromCache() explicitly before the Neo4j status report, so the advertised node/edge counts reflect post-bootstrap state. Injection is Optional since the bean only exists in the "serving" profile (matches the existing @Profile + @ConditionalOnBean guards). - Class javadoc updated to document the new invocation model and the reason the event-listener path did not work. The method is idempotent (guards on count>0 and on missing H2 file), so moving the invocation is safe even in scenarios where Neo4j is already populated by `enrich`. Tests: existing GraphBootstrapperTest still passes unchanged (it calls the method directly). Integration verification: # full pipeline to populate both H2 + Neo4j ./scripts/baseline/run-pipeline.sh spring-petclinic # wipe ONLY Neo4j; keep H2 rm -rf .seeds/spring-petclinic/.code-iq/graph # re-run serve java -jar target/code-iq-*-cli.jar serve .seeds/spring-petclinic --port 18080 & # wait for /actuator/health=200, then: curl http://127.0.0.1:18080/api/stats Before this fix: Neo4j empty, stats=0. After this fix: log shows "Bootstrapped Neo4j with 634 nodes and 604 edges from H2 cache", /api/stats returns 677 nodes / 604 edges / 65 files. * docs(baseline): mark GraphBootstrapper dead-listener gap RESOLVED
1 parent 3beafd9 commit a131a80

3 files changed

Lines changed: 24 additions & 4 deletions

File tree

docs/superpowers/baselines/2026-04-17/BASELINE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,9 @@ Ordered by severity. Each item cites the raw artifact it was derived from.
239239
- **`GraphHealthIndicator` reports `OUT_OF_SERVICE` (503) even when the graph is loaded.** Discovered during the pipeline smoke-test fix. `/actuator/health` body: `{"groups":["liveness","readiness"],"status":"OUT_OF_SERVICE"}`. The server is fully functional (`/api/stats` returns real data) but the health indicator makes `/actuator/health` unusable as a readiness probe for orchestrators (K8s, Compose, CI). Fix in `src/main/java/io/github/randomcodespace/iq/health/GraphHealthIndicator.java`. Low for baseline use; High when we start Dockerizing or targeting K8s.
240240
- **RESOLVED (2026-04-17, branch `phase-a/fix-graph-health`)**: Root cause was *not* in `GraphHealthIndicator` (which correctly returns UP when nodes>0). It was in `ServeCommand`: the CLI blocks on `Thread.currentThread().join()` inside Spring Boot's `CommandLineRunner.run()`, which prevents `ApplicationReadyEvent` from ever firing. Without that event, Spring's default readiness publisher never flips `ReadinessState` from `REFUSING_TRAFFIC` (503 `OUT_OF_SERVICE`) to `ACCEPTING_TRAFFIC` (200 `UP`). Fix: `ServeCommand` now explicitly publishes `AvailabilityChangeEvent` for `LivenessState.CORRECT` + `ReadinessState.ACCEPTING_TRAFFIC` before blocking, via a new `markReady()` method (unit-tested). Verified end-to-end: `health_http` is now 200 on both seeds (petclinic ready 13s, express ready 14s; status "UP"). Follow-up filed: `GraphBootstrapper`'s `@EventListener(ApplicationReadyEvent.class)` is effectively dead code for the same reason — only noticed because enrich always runs before serve in our pipeline, so the bootstrap fallback never actually needs to fire.
241241

242+
- **`GraphBootstrapper` dead listener** (Low severity, follow-up to the health fix). The H2→Neo4j bootstrap path was triggered via `@EventListener(ApplicationReadyEvent.class)`, which never fires while `ServeCommand` blocks as a `CommandLineRunner`.
243+
- **RESOLVED (2026-04-17, branch `phase-a/fix-bootstrap-listener`)**: dropped the `@EventListener` annotation and instead invoke `GraphBootstrapper.bootstrapNeo4jFromCache()` explicitly from `ServeCommand.call()` before the status report. The method is idempotent (guards on Neo4j count>0 and on missing H2 cache file). Verified end-to-end: after running a full pipeline then wiping only `.code-iq/graph/` (Neo4j) while keeping `.code-iq/cache/` (H2), `serve` logs `Bootstrapped Neo4j with 634 nodes and 604 edges from H2 cache` and `/api/stats` returns real data (nodes=677, edges=604, files=65). Existing `GraphBootstrapperTest` cases still pass unchanged (they always called the method directly).
244+
242245
- **SpotBugs: 8 HIGH-priority findings (priority=1) + 1,484 at priority=2.** Total 1,492. HIGH findings must be triaged individually (read `raw/spotbugs.xml`). Noise-dominant rules (`NM_METHOD_NAMING_CONVENTION`=730, `SF_SWITCH_NO_DEFAULT`=448) should be filtered via a SpotBugs exclude file so real signal surfaces; real-concern patterns that deserve review now: `NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE` (26), `BC_UNCONFIRMED_CAST` (55), `UL_UNRELEASED_LOCK_EXCEPTION_PATH` (1), `WMI_WRONG_MAP_ITERATOR` (2), `ES_COMPARING_STRINGS_WITH_EQ` (2), `MT_CORRECTNESS` category (1).
243246
- Raw: `raw/spotbugs.xml`, `raw/spotbugs-summary.json`.
244247
- **RESOLVED (2026-04-17, branch `phase-a/fixups-spotbugs`)**: Added `spotbugs-exclude.xml` covering ANTLR-generated parsers and global noise rules (`NM_METHOD_NAMING_CONVENTION`, `SF_SWITCH_NO_DEFAULT`, `EI_EXPOSE_REP`/`EI_EXPOSE_REP2`, `MS_PKGPROTECT`/`MS_FINAL_PKGPROTECT`), wired via `pom.xml`. Fixed all 8 priority-1 findings in codeiq code (UTF-8 in `Analyzer.getGitHead`, narrowed catch in `IndexCommand`, dead-store removed in `PluginsCommand`, `.equals()` in `AntlrParserFactory` + `CSharpPreprocessorParserBase`, try-finally unlock in `AnalysisCache.removeFile`, merged duplicate branches in `CodeIqApplication`, removed dead `BundleCommand.writeEntry` overload, `entrySet()` iteration in `PluginsCommand` + `GitLabCiDetector`, narrowed `VersionCommand` catch). **Final: 1,492 → 38 (-97.5%); priority-1: 8 → 0.** Remaining 38 are priority-2 STYLE/BAD_PRACTICE; no CORRECTNESS/MT_CORRECTNESS/SECURITY left. Next-pass candidates: 26 `NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE`. Post-triage summary: `raw/spotbugs-summary-after-triage.json`.

src/main/java/io/github/randomcodespace/iq/cli/ServeCommand.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.github.randomcodespace.iq.cli;
22

33
import io.github.randomcodespace.iq.config.CodeIqConfig;
4+
import io.github.randomcodespace.iq.config.GraphBootstrapper;
45
import io.github.randomcodespace.iq.graph.GraphStore;
56
import org.slf4j.Logger;
67
import org.slf4j.LoggerFactory;
@@ -63,6 +64,10 @@ public class ServeCommand implements Callable<Integer> {
6364
@Autowired
6465
private ApplicationEventPublisher events;
6566

67+
// Optional: only present in the "serving" profile (same conditions as the bean).
68+
@Autowired(required = false)
69+
private GraphBootstrapper graphBootstrapper;
70+
6671
@Override
6772
public Integer call() {
6873
Path root = path.toAbsolutePath().normalize();
@@ -72,6 +77,15 @@ public Integer call() {
7277
}
7378
NumberFormat nf = NumberFormat.getIntegerInstance(Locale.US);
7479

80+
// Bootstrap Neo4j from the H2 analysis cache if Neo4j is empty. This is
81+
// a no-op when enrich has already run (guarded internally by a count>0
82+
// check) and when the H2 cache file is missing. Must happen before the
83+
// status report below so the advertised node/edge counts are truthful.
84+
// See GraphBootstrapper javadoc for why this is not an @EventListener.
85+
if (graphBootstrapper != null) {
86+
graphBootstrapper.bootstrapNeo4jFromCache();
87+
}
88+
7589
// Report Neo4j graph status
7690
if (graphStore != null) {
7791
try {

src/main/java/io/github/randomcodespace/iq/config/GraphBootstrapper.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@
77
import org.slf4j.Logger;
88
import org.slf4j.LoggerFactory;
99
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
10-
import org.springframework.boot.context.event.ApplicationReadyEvent;
1110
import org.springframework.context.annotation.Profile;
12-
import org.springframework.context.event.EventListener;
1311
import org.springframework.stereotype.Component;
1412

1513
import java.nio.file.Files;
@@ -25,7 +23,13 @@
2523
* {@code serve} (which needs Neo4j for graph traversal queries like shortest path,
2624
* cycles, impact trace, ego graph, and neighbors).
2725
* <p>
28-
* Runs after the Spring context is fully ready, so Neo4j and GraphStore are available.
26+
* Invoked explicitly by {@link io.github.randomcodespace.iq.cli.ServeCommand}
27+
* before the server reports "Server started", so the advertised node/edge counts
28+
* reflect the bootstrapped state. Previously triggered via
29+
* {@code @EventListener(ApplicationReadyEvent.class)}, but that event never fires
30+
* because {@code ServeCommand.call()} blocks (as a {@code CommandLineRunner}) and
31+
* Spring's ready-event publication waits for runners to return.
32+
* <p>
2933
* Only active in the "serving" profile when GraphStore is present.
3034
*/
3135
@Component
@@ -43,7 +47,6 @@ public GraphBootstrapper(GraphStore graphStore, CodeIqConfig config) {
4347
this.config = config;
4448
}
4549

46-
@EventListener(ApplicationReadyEvent.class)
4750
public void bootstrapNeo4jFromCache() {
4851
// Skip bootstrap in read-only mode (no writes allowed)
4952
if (config.isReadOnly()) {

0 commit comments

Comments
 (0)