105 lines
6.2 KiB
Markdown
105 lines
6.2 KiB
Markdown
# Task 3 — the ten-row activation matrix
|
||
|
||
Every row below names where its evidence is, and nothing is ticked from reasoning about the code.
|
||
Rows 1–7 and 10 are runtime lanes from one Compose matrix run, `20260819T025416Z-200172`; rows 8 and 9
|
||
needed work this wave and are described in full.
|
||
|
||
The per-lane `activation.json` is the application's own answer about which switches are on, not the
|
||
flags the harness passed in — which is the point of reading it rather than the lane definition.
|
||
|
||
| # | Matrix | Evidence | Resolved switches (from `activation.json`) |
|
||
| --- | --- | --- | --- |
|
||
| 1 | five off | `off-local`, `off-dev`, `off-prod` lanes + `FiveAdapterOffInventoryTest` | `local` / `dev` / `prod`, **none on** |
|
||
| 2 | JPA only | `local-jpa` lane | `local`, `persistence-jpa` |
|
||
| 3 | Mongo only | `local-mongo` lane | `local`, `persistence-mongo` |
|
||
| 4 | Messaging only | `local-messaging` lane | `local`, `messaging` |
|
||
| 5 | Notification + JPA, `INGEST_ONLY` | `local-notification-ingest` + `local-notification-handoff` | `local`, `persistence-jpa`+`notification.platform`, `APP_NOTIFICATION_PLATFORM_MODE: INGEST_ONLY` |
|
||
| 6 | Notification + JPA, `SERVING` | `local-notification-serving` | same switches, `MODE: SERVING` |
|
||
| 7 | GraphQL only | `local-graphql` lane | `local`, `graphql` |
|
||
| 8 | relay on, dependency missing | **this wave** — see below | n/a: the deployment is refused |
|
||
| 9 | JPA + Mongo | `all-adapters` lane + `PortResolutionContractTest` | `local`, both persistence switches on |
|
||
| 10 | five on | `all-adapters` lane | `local`, `graphql`,`messaging`,`persistence-jpa`,`persistence-mongo`,`notification.platform` |
|
||
|
||
Row 5's exactly-once claim is not inferred from the lane passing; the smoke client says it:
|
||
|
||
```
|
||
notification-smoke: b0e083cb-06c5-4dde-b454-4c4e26f65edc delivered exactly once and stayed that way
|
||
```
|
||
|
||
## Row 8 — a defect, found by running the row
|
||
|
||
The plan is explicit that Row 8 must be checked for the *name*, not merely for a failure. Checking it
|
||
that way found that the name was not what an operator got.
|
||
|
||
`ca-skeleton.outbox.enabled=true` with the JPA switch off is a dependency error this repository
|
||
names, and `CapabilityDependencyValidator` produces the sentence that names it. But the outbox is
|
||
also a registered relational consumer, so `DataSourceRequirement` reports that a pool is required,
|
||
`JpaOffAutoConfigurationImportFilter` therefore keeps Boot's relational auto-configurations in the
|
||
candidate set, and Hibernate and Flyway were instantiated during refresh — ahead of the
|
||
`InitializingBean` that carried the check. What actually came out was:
|
||
|
||
```
|
||
Unable to obtain connection from database: Connection to localhost:5433 refused.
|
||
```
|
||
|
||
Both components were right on their own. A pool does have consumers besides JPA, and the outbox does
|
||
need the JPA switch. The disagreement was only ever visible in **which one spoke first**, and no test
|
||
could see it: the existing `CapabilityDependencyValidatorTest` calls the validator's static method
|
||
against a `MockEnvironment`, which proves the rule computes the right sentence and nothing about
|
||
whether anything runs it in time.
|
||
|
||
**Fix.** The check moved to the environment stage
|
||
(`CapabilityDependencyEnvironmentValidator`, an `EnvironmentPostProcessor` at
|
||
`LOWEST_PRECEDENCE` alongside the master-switch and profile validators), where every property
|
||
source is resolved and nothing has been instantiated. The `InitializingBean` stays: a context built
|
||
without `spring.factories` — an `ApplicationContextRunner`, a slice test — never reaches the
|
||
post-processor, and the rule should not be optional there.
|
||
|
||
**Evidence, from the built jar rather than from a test harness** — see
|
||
[task3-row8-dependency-error.log](task3-row8-dependency-error.log):
|
||
|
||
```
|
||
### outbox on, JPA off exit code: 1
|
||
This deployment enables capabilities whose dependencies are off:
|
||
- ca-skeleton.outbox.enabled=true needs relational persistence to store rows;
|
||
set ca-skeleton.persistence-jpa.enabled=true or turn the outbox off.
|
||
- ca-skeleton.outbox.enabled=true needs somewhere to publish;
|
||
set app.messaging.enabled=true or turn the outbox off.
|
||
|
||
### relay on, outbox off exit code: 1
|
||
This deployment enables capabilities whose dependencies are off:
|
||
- ca-skeleton.outbox.relay-enabled=true only starts the scheduler for a capability that is off;
|
||
set ca-skeleton.outbox.enabled=true or turn the relay off.
|
||
```
|
||
|
||
`DependencyErrorStartupContractTest` pins this at the composition-root level, including a case that
|
||
boots all-off successfully — without it, every other assertion in that class would also be satisfied
|
||
by a validator that refuses everything.
|
||
|
||
## Row 9 — the positive half runs; the conflict half is pinned as a rule
|
||
|
||
`all-adapters` starts PostgreSQL and MongoDB and the application together, and the application
|
||
reports both persistence switches on. The two adapters implement disjoint ports, so there is no
|
||
ambiguity to resolve and no `@Primary` involved.
|
||
|
||
That absence is what needs pinning, because manufacturing a conflict would test Spring's
|
||
`NoUniqueBeanDefinitionException` rather than this repository. What can silently change is the
|
||
property the row depends on: that no port is resolved by preferring one bean over another. A
|
||
`@Primary` added later to settle an ambiguity would convert a startup rejection into a silent pick,
|
||
and every existing test would stay green, because the composition would still boot.
|
||
|
||
`PortResolutionContractTest` states the rule directly — a `@Primary` on a port implementation is
|
||
permitted only when a condition decides which one is active, which makes it a selector rather than a
|
||
tiebreak. The whole repository has three `@Primary` beans:
|
||
|
||
| bean | type | guard |
|
||
| --- | --- | --- |
|
||
| `inProcessDistributedLock` | `DistributedLockPort` | `multi-instance-enabled` false or absent |
|
||
| `distributedLockProvider` | `DistributedLockPort` | `multi-instance-enabled` true |
|
||
| `applicationTaskExecutor` | `TaskExecutor` | none — Boot's own contract wants a primary executor, and it is not a port |
|
||
|
||
The two lock beans cannot coexist, so neither is being preferred; one of them simply is not there.
|
||
|
||
The rule was falsified before being trusted: removing the `@ConditionalOnProperty` from
|
||
`distributedLockProvider` fails the test, and restoring it passes.
|