fix(alerting): ClickHouseSearchIndex bean registered as concrete type (hotfix: production crashloop) #141

Merged
hsiegeln merged 1 commits from fix/alerting-searchindex-bean-type into main 2026-04-20 10:24:50 +02:00
Owner

Summary

Hotfix — production crashlooping after #140 merge. One-line return-type change in StorageBeanConfig.

Root cause

StorageBeanConfig.clickHouseSearchIndex(...) declared return type SearchIndex (interface). Spring registers beans by their declared type, not their runtime instance class. ExchangeMatchEvaluator (Plan 02) autowires the concrete ClickHouseSearchIndex because countExecutionsForAlerting lives only on the concrete class, not the interface. Spring can't match the concrete autowire against an interface-typed bean, so the app fails to start:

Parameter 0 of constructor in com.cameleer.server.app.alerting.eval.ExchangeMatchEvaluator
required a bean of type 'com.cameleer.server.app.search.ClickHouseSearchIndex' that could not be found.

ClickHouseLogStore's bean is already declared with the concrete return type (StorageBeanConfig.java:171), which is why LogPatternEvaluator autowires fine.

Fix

Change the return type from SearchIndex to ClickHouseSearchIndex. Spring still auto-exposes it under the SearchIndex interface for SearchIndexer and any other interface-typed consumers — widening the declared type doesn't break existing autowires.

Why CI didn't catch this

All alerting ITs inherit from AbstractPostgresIT, which has:

@MockBean(name = "clickHouseSearchIndex") protected ClickHouseSearchIndex clickHouseSearchIndex;

@MockBean replaces the bean with a mock of the declared type (ClickHouseSearchIndex). Every test ran against a mock whose declared type IS the concrete class, so ExchangeMatchEvaluator's autowire succeeded against the mock. The real production bean graph was never exercised.

Follow-up (not in this PR — a separate branch)

  1. Remove @MockBean(name = "clickHouseSearchIndex") and @MockBean(name = "clickHouseLogStore") from AbstractPostgresIT. Mocks belong on the specific ITs that actually need them, not on the base class.
  2. Add a SpringContextSmokeIT that loads the full @SpringBootTest context with no mocks and asserts void contextLoads() {}. This is the minimum regression test that would have caught this bug pre-merge. Budget: ~15 minutes.
  3. Audit all Plan 02 @MockBean(name=...) workarounds for similar masked bugs.

This follow-up is broader than a hotfix — I'll queue it for Plan 03 or a dedicated test-hygiene branch.

Test plan

  • Merge + deploy to staging.
  • Confirm application starts without the ExchangeMatchEvaluator autowire error.
  • Smoke-test the alerting REST API: curl POST /api/v1/environments/{envSlug}/alerts/rules with an EXCHANGE_MATCH rule; wait a tick; confirm no exceptions in logs.
  • Verify SearchIndexer still functions (ingestion path — untouched logically, just type-narrowed bean declaration).
## Summary **Hotfix — production crashlooping after #140 merge.** One-line return-type change in `StorageBeanConfig`. ## Root cause `StorageBeanConfig.clickHouseSearchIndex(...)` declared return type `SearchIndex` (interface). Spring registers beans by their **declared** type, not their runtime instance class. `ExchangeMatchEvaluator` (Plan 02) autowires the **concrete** `ClickHouseSearchIndex` because `countExecutionsForAlerting` lives only on the concrete class, not the interface. Spring can't match the concrete autowire against an interface-typed bean, so the app fails to start: ``` Parameter 0 of constructor in com.cameleer.server.app.alerting.eval.ExchangeMatchEvaluator required a bean of type 'com.cameleer.server.app.search.ClickHouseSearchIndex' that could not be found. ``` `ClickHouseLogStore`'s bean is already declared with the concrete return type (`StorageBeanConfig.java:171`), which is why `LogPatternEvaluator` autowires fine. ## Fix Change the return type from `SearchIndex` to `ClickHouseSearchIndex`. Spring still auto-exposes it under the `SearchIndex` interface for `SearchIndexer` and any other interface-typed consumers — widening the declared type doesn't break existing autowires. ## Why CI didn't catch this All alerting ITs inherit from `AbstractPostgresIT`, which has: ```java @MockBean(name = "clickHouseSearchIndex") protected ClickHouseSearchIndex clickHouseSearchIndex; ``` `@MockBean` **replaces** the bean with a mock of the declared type (`ClickHouseSearchIndex`). Every test ran against a mock whose declared type IS the concrete class, so `ExchangeMatchEvaluator`'s autowire succeeded against the mock. The real production bean graph was never exercised. ## Follow-up (not in this PR — a separate branch) 1. Remove `@MockBean(name = "clickHouseSearchIndex")` and `@MockBean(name = "clickHouseLogStore")` from `AbstractPostgresIT`. Mocks belong on the specific ITs that actually need them, not on the base class. 2. Add a `SpringContextSmokeIT` that loads the full `@SpringBootTest` context with **no mocks** and asserts `void contextLoads() {}`. This is the minimum regression test that would have caught this bug pre-merge. Budget: ~15 minutes. 3. Audit all Plan 02 `@MockBean(name=...)` workarounds for similar masked bugs. This follow-up is broader than a hotfix — I'll queue it for Plan 03 or a dedicated test-hygiene branch. ## Test plan - [ ] Merge + deploy to staging. - [ ] Confirm application starts without the `ExchangeMatchEvaluator` autowire error. - [ ] Smoke-test the alerting REST API: `curl POST /api/v1/environments/{envSlug}/alerts/rules` with an `EXCHANGE_MATCH` rule; wait a tick; confirm no exceptions in logs. - [ ] Verify `SearchIndexer` still functions (ingestion path — untouched logically, just type-narrowed bean declaration).
claude added 1 commit 2026-04-20 09:12:09 +02:00
fix(alerting): declare ClickHouseSearchIndex bean as concrete type
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 3m6s
CI / cleanup-branch (pull_request) Has been skipped
CI / build (pull_request) Successful in 4m5s
CI / docker (pull_request) Has been skipped
CI / deploy (pull_request) Has been skipped
CI / deploy-feature (pull_request) Has been skipped
CI / docker (push) Successful in 1m37s
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Successful in 39s
c9c93ac565
Production crashlooped on startup: ExchangeMatchEvaluator autowires the
concrete ClickHouseSearchIndex (for countExecutionsForAlerting, which
lives only on the concrete class, not the SearchIndex interface), but
StorageBeanConfig declared the bean with interface return type SearchIndex.
Spring matches autowire candidates by declared bean type, not by runtime
instance class, so the concrete-typed autowire failed with:

  Parameter 0 of constructor in ExchangeMatchEvaluator required a bean
  of type 'ClickHouseSearchIndex' that could not be found.

ClickHouseLogStore's bean is already declared with the concrete return
type (line 171), which is why LogPatternEvaluator autowires fine.

All alerting ITs passed pre-merge because AbstractPostgresIT replaces the
clickHouseSearchIndex bean with @MockBean(name=...) whose declared type
IS the concrete ClickHouseSearchIndex. The mock masked the prod bug.

Follow-up: remove @MockBean(name="clickHouseSearchIndex") from
AbstractPostgresIT so the real bean graph is exercised by alerting ITs
(and add a SpringContextSmokeIT that loads the context with no mocks).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hsiegeln merged commit 5373ac6541 into main 2026-04-20 10:24:50 +02:00
Sign in to join this conversation.