docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
43bccd08a8
commit
b2963105a8
@@ -0,0 +1,7 @@
|
||||
command: git rev-parse HEAD && git status --short --branch
|
||||
cwd: /shared/codebase/clean-architecture-backend-template
|
||||
timestamp: 2026-08-29T00:04:07+00:00
|
||||
sourceRevision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
--- stdout ---
|
||||
a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
## main...origin/main
|
||||
@@ -0,0 +1,50 @@
|
||||
command: python3 module inventory from src/config/architecture/modules.json
|
||||
cwd: /shared/codebase/clean-architecture-backend-template
|
||||
timestamp: 2026-08-29T00:04:07+00:00
|
||||
sourceRevision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
--- stdout ---
|
||||
registry_modules= 44
|
||||
domain-core|:domain-core|prod=8|prod_loc=107|test=0|test_loc=0|deps=|runtime=app-bootstrap,sample-portfolio
|
||||
shared-contract|:shared-contract|prod=56|prod_loc=2839|test=23|test_loc=2706|deps=|runtime=app-bootstrap,sample-portfolio
|
||||
application-core|:application-core|prod=886|prod_loc=35798|test=136|test_loc=19751|deps=domain-core,shared-contract|runtime=app-bootstrap,sample-portfolio
|
||||
adapter-outbound-support|:adapter:outbound:support|prod=5|prod_loc=107|test=1|test_loc=89|deps=domain-core,application-core,shared-contract|runtime=app-bootstrap
|
||||
adapter-outbound-persistence-jpa|:adapter:outbound:persistence-jpa|prod=460|prod_loc=44594|test=141|test_loc=12581|deps=domain-core,application-core,shared-contract|runtime=app-bootstrap,sample-portfolio
|
||||
adapter-outbound-persistence-mongo|:adapter:outbound:persistence-mongo|prod=353|prod_loc=23401|test=139|test_loc=15416|deps=|runtime=app-bootstrap
|
||||
adapter-outbound-identifier|:adapter:outbound:identifier|prod=5|prod_loc=134|test=2|test_loc=151|deps=domain-core,application-core|runtime=app-bootstrap,sample-portfolio
|
||||
adapter-outbound-fileserver|:adapter:outbound:fileserver|prod=79|prod_loc=12723|test=37|test_loc=12043|deps=application-core,shared-contract|runtime=app-bootstrap
|
||||
adapter-outbound-objectstorage|:adapter:outbound:objectstorage|prod=154|prod_loc=14966|test=49|test_loc=6778|deps=application-core,shared-contract|runtime=sample-portfolio
|
||||
adapter-outbound-cache-redis|:adapter:outbound:cache-redis|prod=316|prod_loc=33690|test=70|test_loc=15245|deps=domain-core,application-core,shared-contract,adapter-outbound-support|runtime=app-bootstrap
|
||||
adapter-outbound-httpclient|:adapter:outbound:httpclient|prod=270|prod_loc=15887|test=97|test_loc=8803|deps=domain-core,application-core,shared-contract,adapter-outbound-support|runtime=app-bootstrap
|
||||
adapter-outbound-messaging|:adapter:outbound:messaging|prod=35|prod_loc=4350|test=19|test_loc=3670|deps=domain-core,application-core,shared-contract,adapter-outbound-support|runtime=app-bootstrap
|
||||
adapter-outbound-notification|:adapter:outbound:notification|prod=172|prod_loc=14749|test=61|test_loc=10037|deps=domain-core,application-core,shared-contract,adapter-outbound-support|runtime=app-bootstrap
|
||||
adapter-inbound-web|:adapter:inbound:web|prod=428|prod_loc=29488|test=202|test_loc=23500|deps=domain-core,application-core,shared-contract|runtime=app-bootstrap,sample-portfolio
|
||||
adapter-inbound-grpc|:adapter:inbound:grpc|prod=9|prod_loc=650|test=6|test_loc=782|deps=domain-core,application-core,shared-contract|runtime=
|
||||
adapter-inbound-graphql|:adapter:inbound:graphql|prod=409|prod_loc=26477|test=117|test_loc=14745|deps=domain-core,application-core,shared-contract|runtime=app-bootstrap
|
||||
adapter-inbound-websocket|:adapter:inbound:websocket|prod=174|prod_loc=13426|test=74|test_loc=10336|deps=domain-core,application-core,shared-contract|runtime=
|
||||
app-bootstrap|:app-bootstrap|prod=160|prod_loc=15096|test=282|test_loc=32724|deps=domain-core,application-core,adapter-outbound-persistence-jpa,adapter-outbound-support,adapter-outbound-messaging,adapter-outbound-cache-redis,adapter-outbound-notification,adapter-outbound-fileserver,adapter-outbound-httpclient,adapter-outbound-identifier,adapter-inbound-web,shared-contract,adapter-outbound-persistence-mongo,adapter-inbound-graphql,messaging-spring-boot-starter|runtime=app-bootstrap
|
||||
sample-portfolio|:sample-portfolio|prod=210|prod_loc=10429|test=63|test_loc=7020|deps=domain-core,application-core,adapter-outbound-persistence-jpa,adapter-outbound-identifier,adapter-outbound-objectstorage,adapter-inbound-web,shared-contract|runtime=sample-portfolio
|
||||
messaging-core-api|:messaging:messaging-core-api|prod=86|prod_loc=3952|test=8|test_loc=934|deps=|runtime=app-bootstrap
|
||||
messaging-schema-api|:messaging:messaging-schema-api|prod=11|prod_loc=635|test=3|test_loc=263|deps=messaging-core-api|runtime=app-bootstrap
|
||||
messaging-schema-json|:messaging:messaging-schema-json|prod=2|prod_loc=234|test=3|test_loc=373|deps=messaging-core-api,messaging-schema-api|runtime=app-bootstrap
|
||||
messaging-schema-avro|:messaging:messaging-schema-avro|prod=3|prod_loc=356|test=4|test_loc=463|deps=messaging-core-api,messaging-schema-api|runtime=
|
||||
messaging-schema-protobuf|:messaging:messaging-schema-protobuf|prod=3|prod_loc=209|test=2|test_loc=278|deps=messaging-core-api,messaging-schema-api|runtime=
|
||||
messaging-cloudevents|:messaging:messaging-cloudevents|prod=4|prod_loc=243|test=1|test_loc=162|deps=messaging-core-api,messaging-schema-api|runtime=app-bootstrap
|
||||
messaging-policy|:messaging:messaging-policy|prod=27|prod_loc=1744|test=4|test_loc=1236|deps=messaging-core-api,messaging-schema-api|runtime=app-bootstrap
|
||||
messaging-transport-spi|:messaging:messaging-transport-spi|prod=14|prod_loc=783|test=4|test_loc=586|deps=messaging-core-api,messaging-schema-api,messaging-policy|runtime=app-bootstrap
|
||||
messaging-runtime-core|:messaging:messaging-runtime-core|prod=7|prod_loc=804|test=4|test_loc=866|deps=messaging-core-api,messaging-schema-api,messaging-policy,messaging-transport-spi,messaging-security,messaging-observability|runtime=app-bootstrap
|
||||
messaging-observability|:messaging:messaging-observability|prod=10|prod_loc=847|test=6|test_loc=827|deps=messaging-core-api|runtime=app-bootstrap
|
||||
messaging-security|:messaging:messaging-security|prod=13|prod_loc=959|test=3|test_loc=475|deps=messaging-core-api|runtime=app-bootstrap
|
||||
messaging-kafka|:messaging:messaging-kafka|prod=36|prod_loc=3711|test=24|test_loc=4087|deps=messaging-core-api,messaging-schema-api,messaging-policy,messaging-transport-spi,messaging-observability,messaging-security,messaging-admin-api|runtime=app-bootstrap
|
||||
messaging-kafka-share-experimental|:messaging:messaging-kafka-share-experimental|prod=5|prod_loc=200|test=1|test_loc=112|deps=messaging-core-api,messaging-policy,messaging-transport-spi,messaging-kafka|runtime=
|
||||
messaging-rabbit|:messaging:messaging-rabbit|prod=22|prod_loc=2624|test=10|test_loc=1727|deps=messaging-core-api,messaging-schema-api,messaging-policy,messaging-transport-spi,messaging-observability,messaging-security,messaging-admin-api|runtime=app-bootstrap
|
||||
messaging-reliability-api|:messaging:messaging-reliability-api|prod=14|prod_loc=822|test=0|test_loc=0|deps=messaging-core-api|runtime=app-bootstrap
|
||||
messaging-outbox-jdbc-postgresql|:messaging:messaging-outbox-jdbc-postgresql|prod=19|prod_loc=2525|test=8|test_loc=2140|deps=messaging-core-api,messaging-reliability-api,messaging-policy,messaging-observability,messaging-admin-api|runtime=app-bootstrap
|
||||
messaging-inbox-jdbc-postgresql|:messaging:messaging-inbox-jdbc-postgresql|prod=8|prod_loc=576|test=4|test_loc=607|deps=messaging-core-api,messaging-reliability-api|runtime=app-bootstrap
|
||||
messaging-claim-check|:messaging:messaging-claim-check|prod=7|prod_loc=424|test=3|test_loc=333|deps=messaging-core-api,messaging-reliability-api|runtime=app-bootstrap
|
||||
messaging-admin-api|:messaging:messaging-admin-api|prod=26|prod_loc=1619|test=1|test_loc=147|deps=messaging-core-api,messaging-policy|runtime=app-bootstrap
|
||||
messaging-admin-runtime|:messaging:messaging-admin-runtime|prod=13|prod_loc=1263|test=6|test_loc=1051|deps=messaging-core-api,messaging-policy,messaging-admin-api,messaging-transport-spi,messaging-security,messaging-observability|runtime=app-bootstrap
|
||||
messaging-pulsar-experimental|:messaging:messaging-pulsar-experimental|prod=9|prod_loc=676|test=2|test_loc=414|deps=messaging-core-api,messaging-schema-api,messaging-policy,messaging-transport-spi,messaging-observability,messaging-security,messaging-admin-api|runtime=
|
||||
messaging-nats-experimental|:messaging:messaging-nats-experimental|prod=8|prod_loc=768|test=2|test_loc=460|deps=messaging-core-api,messaging-schema-api,messaging-policy,messaging-transport-spi,messaging-observability,messaging-security,messaging-admin-api|runtime=
|
||||
messaging-spring-cloud-stream-bridge|:messaging:messaging-spring-cloud-stream-bridge|prod=7|prod_loc=516|test=2|test_loc=295|deps=messaging-core-api,messaging-policy,messaging-transport-spi|runtime=
|
||||
messaging-spring-boot-starter|:messaging:messaging-spring-boot-starter|prod=29|prod_loc=3590|test=12|test_loc=2384|deps=messaging-admin-api,messaging-admin-runtime,messaging-claim-check,messaging-cloudevents,messaging-core-api,messaging-inbox-jdbc-postgresql,messaging-kafka,messaging-observability,messaging-outbox-jdbc-postgresql,messaging-policy,messaging-rabbit,messaging-reliability-api,messaging-runtime-core,messaging-schema-api,messaging-schema-json,messaging-security,messaging-transport-spi|runtime=app-bootstrap
|
||||
messaging-testkit|:messaging:messaging-testkit|prod=15|prod_loc=1246|test=6|test_loc=828|deps=messaging-core-api,messaging-schema-api,messaging-policy,messaging-transport-spi|runtime=
|
||||
@@ -0,0 +1,29 @@
|
||||
command: ./gradlew :domain-core:test --console=plain --no-daemon
|
||||
cwd: /shared/codebase/clean-architecture-backend-template/src
|
||||
timestamp: 2026-08-29T00:04:07+00:00
|
||||
sourceRevision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
--- output ---
|
||||
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
|
||||
Daemon will be stopped at the end of the build
|
||||
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||
> Task :build-logic:compileJava UP-TO-DATE
|
||||
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||
> Task :build-logic:processResources UP-TO-DATE
|
||||
> Task :build-logic:classes UP-TO-DATE
|
||||
> Task :build-logic:jar UP-TO-DATE
|
||||
> Task :domain-core:compileJava UP-TO-DATE
|
||||
> Task :domain-core:processResources NO-SOURCE
|
||||
> Task :domain-core:classes UP-TO-DATE
|
||||
> Task :domain-core:compileTestJava NO-SOURCE
|
||||
> Task :domain-core:processTestResources NO-SOURCE
|
||||
> Task :domain-core:testClasses UP-TO-DATE
|
||||
> Task :domain-core:test NO-SOURCE
|
||||
|
||||
BUILD SUCCESSFUL in 7s
|
||||
9 actionable tasks: 9 up-to-date
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
exitCode: 0
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
command: ./gradlew :app-bootstrap:test --tests dev.caskeleton.bootstrap.architecture.CleanArchitectureTest --console=plain --no-daemon
|
||||
cwd: /shared/codebase/clean-architecture-backend-template/src
|
||||
timestamp: 2026-08-29T00:04:25+00:00
|
||||
sourceRevision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
--- output ---
|
||||
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
|
||||
Daemon will be stopped at the end of the build
|
||||
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||
> Task :build-logic:compileJava UP-TO-DATE
|
||||
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||
> Task :build-logic:processResources UP-TO-DATE
|
||||
> Task :build-logic:classes UP-TO-DATE
|
||||
> Task :build-logic:jar UP-TO-DATE
|
||||
> Task :shared-contract:compileJava UP-TO-DATE
|
||||
> Task :shared-contract:processResources UP-TO-DATE
|
||||
> Task :shared-contract:classes UP-TO-DATE
|
||||
> Task :shared-contract:jar UP-TO-DATE
|
||||
> Task :application-core:compileJava UP-TO-DATE
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes UP-TO-DATE
|
||||
> Task :application-core:jar UP-TO-DATE
|
||||
> Task :domain-core:compileJava UP-TO-DATE
|
||||
> Task :domain-core:processResources NO-SOURCE
|
||||
> Task :domain-core:classes UP-TO-DATE
|
||||
> Task :domain-core:jar UP-TO-DATE
|
||||
> Task :messaging:messaging-core-api:compileJava UP-TO-DATE
|
||||
> Task :messaging:messaging-observability:compileJava UP-TO-DATE
|
||||
> Task :messaging:messaging-schema-api:compileJava UP-TO-DATE
|
||||
> Task :messaging:messaging-policy:compileJava UP-TO-DATE
|
||||
> Task :messaging:messaging-reliability-api:compileJava UP-TO-DATE
|
||||
> Task :messaging:messaging-security:compileJava UP-TO-DATE
|
||||
> Task :messaging:messaging-admin-api:compileJava UP-TO-DATE
|
||||
> Task :messaging:messaging-transport-spi:compileJava UP-TO-DATE
|
||||
> Task :messaging:messaging-admin-runtime:compileJava UP-TO-DATE
|
||||
> Task :messaging:messaging-claim-check:compileJava UP-TO-DATE
|
||||
> Task :messaging:messaging-cloudevents:compileJava UP-TO-DATE
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:compileJava UP-TO-DATE
|
||||
> Task :messaging:messaging-kafka:compileJava UP-TO-DATE
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:compileJava UP-TO-DATE
|
||||
> Task :messaging:messaging-rabbit:compileJava UP-TO-DATE
|
||||
> Task :messaging:messaging-runtime-core:compileJava UP-TO-DATE
|
||||
> Task :messaging:messaging-schema-json:compileJava UP-TO-DATE
|
||||
> Task :messaging:messaging-spring-boot-starter:compileJava UP-TO-DATE
|
||||
> Task :adapter:inbound:graphql:compileJava UP-TO-DATE
|
||||
> Task :adapter:inbound:graphql:processResources UP-TO-DATE
|
||||
> Task :adapter:inbound:graphql:classes UP-TO-DATE
|
||||
> Task :adapter:inbound:graphql:jar UP-TO-DATE
|
||||
> Task :adapter:inbound:web:compileJava UP-TO-DATE
|
||||
> Task :adapter:inbound:web:processResources UP-TO-DATE
|
||||
> Task :adapter:inbound:web:classes UP-TO-DATE
|
||||
> Task :adapter:inbound:web:jar UP-TO-DATE
|
||||
> Task :adapter:outbound:support:compileJava UP-TO-DATE
|
||||
> Task :adapter:outbound:support:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:support:classes UP-TO-DATE
|
||||
> Task :adapter:outbound:support:jar UP-TO-DATE
|
||||
> Task :adapter:outbound:cache-redis:compileJava UP-TO-DATE
|
||||
> Task :adapter:outbound:cache-redis:processResources UP-TO-DATE
|
||||
> Task :adapter:outbound:cache-redis:classes UP-TO-DATE
|
||||
> Task :adapter:outbound:cache-redis:jar UP-TO-DATE
|
||||
> Task :adapter:outbound:fileserver:compileJava UP-TO-DATE
|
||||
> Task :adapter:outbound:fileserver:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:fileserver:classes UP-TO-DATE
|
||||
> Task :adapter:outbound:fileserver:jar UP-TO-DATE
|
||||
> Task :adapter:outbound:httpclient:compileJava UP-TO-DATE
|
||||
> Task :adapter:outbound:httpclient:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:httpclient:classes UP-TO-DATE
|
||||
> Task :adapter:outbound:httpclient:jar UP-TO-DATE
|
||||
> Task :adapter:outbound:identifier:compileJava UP-TO-DATE
|
||||
> Task :adapter:outbound:identifier:compileGroovy NO-SOURCE
|
||||
> Task :adapter:outbound:identifier:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:identifier:classes UP-TO-DATE
|
||||
> Task :adapter:outbound:identifier:jar UP-TO-DATE
|
||||
> Task :adapter:outbound:messaging:compileJava UP-TO-DATE
|
||||
> Task :adapter:outbound:messaging:processResources UP-TO-DATE
|
||||
> Task :adapter:outbound:messaging:classes UP-TO-DATE
|
||||
> Task :adapter:outbound:messaging:jar UP-TO-DATE
|
||||
> Task :adapter:outbound:notification:compileJava UP-TO-DATE
|
||||
> Task :adapter:outbound:notification:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:notification:classes UP-TO-DATE
|
||||
> Task :adapter:outbound:notification:jar UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:jar UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-mongo:compileJava UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-mongo:processResources UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-mongo:classes UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-mongo:jar UP-TO-DATE
|
||||
> Task :app-bootstrap:compileJava
|
||||
> Task :app-bootstrap:processResources
|
||||
> Task :app-bootstrap:classes
|
||||
> Task :sample-portfolio:compileJava
|
||||
> Task :sample-portfolio:processResources
|
||||
> Task :sample-portfolio:classes
|
||||
> Task :sample-portfolio:jar
|
||||
> Task :adapter:inbound:web:compileTestkitJava
|
||||
> Task :adapter:inbound:web:processTestkitResources
|
||||
> Task :adapter:inbound:web:testkitClasses
|
||||
> Task :adapter:inbound:web:testkitJar
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses
|
||||
> Task :adapter:outbound:persistence-jpa:testkitJar
|
||||
> Task :app-bootstrap:compileTestJava
|
||||
> Task :app-bootstrap:runtimeClasspathManifest
|
||||
> Task :app-bootstrap:processTestResources
|
||||
> Task :app-bootstrap:testClasses
|
||||
> Task :messaging:messaging-admin-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-admin-api:classes UP-TO-DATE
|
||||
> Task :messaging:messaging-admin-api:jar
|
||||
> Task :messaging:messaging-admin-runtime:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-admin-runtime:classes UP-TO-DATE
|
||||
> Task :messaging:messaging-admin-runtime:jar
|
||||
> Task :messaging:messaging-claim-check:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-claim-check:classes UP-TO-DATE
|
||||
> Task :messaging:messaging-claim-check:jar
|
||||
> Task :messaging:messaging-cloudevents:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-cloudevents:classes UP-TO-DATE
|
||||
> Task :messaging:messaging-cloudevents:jar
|
||||
> Task :messaging:messaging-core-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-core-api:classes UP-TO-DATE
|
||||
> Task :messaging:messaging-core-api:jar
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:processResources
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:classes
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:jar
|
||||
> Task :messaging:messaging-kafka:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-kafka:classes UP-TO-DATE
|
||||
> Task :messaging:messaging-kafka:jar
|
||||
> Task :messaging:messaging-observability:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-observability:classes UP-TO-DATE
|
||||
> Task :messaging:messaging-observability:jar
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:processResources
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:classes
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:jar
|
||||
> Task :messaging:messaging-policy:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-policy:classes UP-TO-DATE
|
||||
> Task :messaging:messaging-policy:jar
|
||||
> Task :messaging:messaging-rabbit:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-rabbit:classes UP-TO-DATE
|
||||
> Task :messaging:messaging-rabbit:jar
|
||||
> Task :messaging:messaging-reliability-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-reliability-api:classes UP-TO-DATE
|
||||
> Task :messaging:messaging-reliability-api:jar
|
||||
> Task :messaging:messaging-runtime-core:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-runtime-core:classes UP-TO-DATE
|
||||
> Task :messaging:messaging-runtime-core:jar
|
||||
> Task :messaging:messaging-schema-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-schema-api:classes UP-TO-DATE
|
||||
> Task :messaging:messaging-schema-api:jar
|
||||
> Task :messaging:messaging-schema-json:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-schema-json:classes UP-TO-DATE
|
||||
> Task :messaging:messaging-schema-json:jar
|
||||
> Task :messaging:messaging-security:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-security:classes UP-TO-DATE
|
||||
> Task :messaging:messaging-security:jar
|
||||
> Task :messaging:messaging-spring-boot-starter:processResources
|
||||
> Task :messaging:messaging-spring-boot-starter:classes
|
||||
> Task :messaging:messaging-spring-boot-starter:jar
|
||||
> Task :messaging:messaging-transport-spi:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-transport-spi:classes UP-TO-DATE
|
||||
> Task :messaging:messaging-transport-spi:jar
|
||||
> Task :adapter:outbound:objectstorage:compileJava
|
||||
> Task :adapter:outbound:objectstorage:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:objectstorage:classes
|
||||
> Task :adapter:outbound:objectstorage:jar
|
||||
> Task :app-bootstrap:test
|
||||
|
||||
BUILD SUCCESSFUL in 1m 9s
|
||||
98 actionable tasks: 37 executed, 61 up-to-date
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
exitCode: 0
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
$ ./gradlew :shared-contract:test --console=plain
|
||||
> Task :shared-contract:compileTestJava
|
||||
> Task :shared-contract:processTestResources NO-SOURCE
|
||||
> Task :shared-contract:testClasses
|
||||
> Task :shared-contract:test
|
||||
|
||||
BUILD SUCCESSFUL in 12s
|
||||
12 actionable tasks: 2 executed, 10 up-to-date
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
$ ./gradlew :shared-contract:edgeRateLimitContractTest --console=plain
|
||||
> Task :shared-contract:compileEdgeRateLimitContractTestJava
|
||||
> Task :shared-contract:processEdgeRateLimitContractTestResources NO-SOURCE
|
||||
> Task :shared-contract:edgeRateLimitContractTestClasses
|
||||
> Task :shared-contract:edgeRateLimitContractTest
|
||||
|
||||
BUILD SUCCESSFUL in 2s
|
||||
12 actionable tasks: 2 executed, 10 up-to-date
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
> Task :shared-contract:compileJava
|
||||
> Task :shared-contract:processResources
|
||||
> Task :shared-contract:classes
|
||||
> Task :shared-contract:jar
|
||||
> Task :application-core:compileJava
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes
|
||||
> Task :application-core:compileTestJava
|
||||
> Task :application-core:processTestResources NO-SOURCE
|
||||
> Task :application-core:testClasses
|
||||
> Task :application-core:test
|
||||
|
||||
BUILD SUCCESSFUL in 25s
|
||||
14 actionable tasks: 14 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
> Task :prepareMessagingContractEvidence
|
||||
> Task :shared-contract:compileJava
|
||||
> Task :shared-contract:processResources
|
||||
> Task :shared-contract:classes
|
||||
> Task :shared-contract:jar
|
||||
> Task :application-core:compileJava
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes
|
||||
> Task :application-core:compileTestJava
|
||||
> Task :application-core:processTestResources NO-SOURCE
|
||||
> Task :application-core:testClasses
|
||||
> Task :application-core:messagingApplicationContractQualificationTestRequiredClasses
|
||||
> Task :application-core:messagingApplicationContractQualificationTest
|
||||
|
||||
> Task :application-core:messagingApplicationContractQualificationTestEvidence
|
||||
messagingApplicationContractQualificationTest: 15 tests, 0 skipped
|
||||
|
||||
BUILD SUCCESSFUL in 18s
|
||||
17 actionable tasks: 17 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
Command: ./gradlew :application-core:messagingApplicationContractQualificationTest --rerun-tasks
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template/src
|
||||
Executed at: 2026-08-29T06:39:57Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Exit code: 1
|
||||
Stdout/stderr excerpt:
|
||||
> Task :prepareMessagingContractEvidence FAILED
|
||||
Execution failed for task ':prepareMessagingContractEvidence'.
|
||||
> -PmessagingSourceDigest=sha256:<64-lowercase-hex> is required for Messaging evidence.
|
||||
|
||||
Observation boundary: this capture records the Gradle evidence-preparation failure before the messaging qualification test lane could execute. It does not establish whether the qualification tests themselves pass or fail. The later exact-digest execution is captured separately in 010-application-core-messaging-qualification.txt.
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
> Task :shared-contract:compileJava
|
||||
> Task :shared-contract:processResources
|
||||
> Task :shared-contract:classes
|
||||
> Task :shared-contract:jar
|
||||
> Task :application-core:compileJava
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes
|
||||
> Task :application-core:jar
|
||||
> Task :domain-core:compileJava
|
||||
> Task :domain-core:processResources NO-SOURCE
|
||||
> Task :domain-core:classes
|
||||
> Task :domain-core:jar
|
||||
> Task :messaging:messaging-core-api:compileJava
|
||||
> Task :messaging:messaging-observability:compileJava
|
||||
> Task :messaging:messaging-schema-api:compileJava
|
||||
> Task :messaging:messaging-policy:compileJava
|
||||
> Task :messaging:messaging-reliability-api:compileJava
|
||||
> Task :messaging:messaging-security:compileJava
|
||||
> Task :messaging:messaging-admin-api:compileJava
|
||||
> Task :messaging:messaging-transport-spi:compileJava
|
||||
> Task :messaging:messaging-admin-runtime:compileJava
|
||||
> Task :messaging:messaging-claim-check:compileJava
|
||||
> Task :messaging:messaging-cloudevents:compileJava
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:compileJava
|
||||
> Task :messaging:messaging-kafka:compileJava
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:compileJava
|
||||
> Task :messaging:messaging-rabbit:compileJava
|
||||
> Task :messaging:messaging-runtime-core:compileJava
|
||||
> Task :messaging:messaging-schema-json:compileJava
|
||||
> Task :messaging:messaging-spring-boot-starter:compileJava
|
||||
> Task :adapter:inbound:graphql:compileJava
|
||||
> Task :adapter:inbound:graphql:processResources
|
||||
> Task :adapter:inbound:graphql:classes
|
||||
> Task :adapter:inbound:graphql:jar
|
||||
> Task :adapter:inbound:web:compileJava
|
||||
> Task :adapter:inbound:web:processResources
|
||||
> Task :adapter:inbound:web:classes
|
||||
> Task :adapter:inbound:web:jar
|
||||
> Task :adapter:outbound:support:compileJava
|
||||
> Task :adapter:outbound:support:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:support:classes
|
||||
> Task :adapter:outbound:support:jar
|
||||
> Task :adapter:outbound:cache-redis:compileJava
|
||||
> Task :adapter:outbound:cache-redis:processResources
|
||||
> Task :adapter:outbound:cache-redis:classes
|
||||
> Task :adapter:outbound:cache-redis:jar
|
||||
> Task :adapter:outbound:fileserver:compileJava
|
||||
> Task :adapter:outbound:fileserver:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:fileserver:classes
|
||||
> Task :adapter:outbound:fileserver:jar
|
||||
> Task :adapter:outbound:httpclient:compileJava
|
||||
> Task :adapter:outbound:httpclient:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:httpclient:classes
|
||||
> Task :adapter:outbound:httpclient:jar
|
||||
> Task :adapter:outbound:identifier:compileJava
|
||||
> Task :adapter:outbound:identifier:compileGroovy NO-SOURCE
|
||||
> Task :adapter:outbound:identifier:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:identifier:classes
|
||||
> Task :adapter:outbound:identifier:jar
|
||||
> Task :adapter:outbound:messaging:compileJava
|
||||
> Task :adapter:outbound:messaging:processResources
|
||||
> Task :adapter:outbound:messaging:classes
|
||||
> Task :adapter:outbound:messaging:jar
|
||||
> Task :adapter:outbound:notification:compileJava
|
||||
> Task :adapter:outbound:notification:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:notification:classes
|
||||
> Task :adapter:outbound:notification:jar
|
||||
> Task :adapter:outbound:persistence-jpa:compileJava
|
||||
> Task :adapter:outbound:persistence-jpa:processResources
|
||||
> Task :adapter:outbound:persistence-jpa:classes
|
||||
> Task :adapter:outbound:persistence-jpa:jar
|
||||
> Task :adapter:outbound:persistence-mongo:compileJava
|
||||
> Task :adapter:outbound:persistence-mongo:processResources
|
||||
> Task :adapter:outbound:persistence-mongo:classes
|
||||
> Task :adapter:outbound:persistence-mongo:jar
|
||||
> Task :app-bootstrap:compileJava
|
||||
> Task :app-bootstrap:processResources
|
||||
> Task :app-bootstrap:classes
|
||||
> Task :sample-portfolio:compileJava
|
||||
> Task :sample-portfolio:processResources
|
||||
> Task :sample-portfolio:classes
|
||||
> Task :sample-portfolio:jar
|
||||
> Task :adapter:inbound:web:compileTestkitJava
|
||||
> Task :adapter:inbound:web:processTestkitResources
|
||||
> Task :adapter:inbound:web:testkitClasses
|
||||
> Task :adapter:inbound:web:testkitJar
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses
|
||||
> Task :adapter:outbound:persistence-jpa:testkitJar
|
||||
> Task :app-bootstrap:compileTestJava
|
||||
> Task :app-bootstrap:runtimeClasspathManifest
|
||||
> Task :app-bootstrap:processTestResources
|
||||
> Task :app-bootstrap:testClasses
|
||||
> Task :messaging:messaging-admin-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-admin-api:classes
|
||||
> Task :messaging:messaging-admin-api:jar
|
||||
> Task :messaging:messaging-admin-runtime:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-admin-runtime:classes
|
||||
> Task :messaging:messaging-admin-runtime:jar
|
||||
> Task :messaging:messaging-claim-check:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-claim-check:classes
|
||||
> Task :messaging:messaging-claim-check:jar
|
||||
> Task :messaging:messaging-cloudevents:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-cloudevents:classes
|
||||
> Task :messaging:messaging-cloudevents:jar
|
||||
> Task :messaging:messaging-core-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-core-api:classes
|
||||
> Task :messaging:messaging-core-api:jar
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:processResources
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:classes
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:jar
|
||||
> Task :messaging:messaging-kafka:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-kafka:classes
|
||||
> Task :messaging:messaging-kafka:jar
|
||||
> Task :messaging:messaging-observability:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-observability:classes
|
||||
> Task :messaging:messaging-observability:jar
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:processResources
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:classes
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:jar
|
||||
> Task :messaging:messaging-policy:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-policy:classes
|
||||
> Task :messaging:messaging-policy:jar
|
||||
> Task :messaging:messaging-rabbit:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-rabbit:classes
|
||||
> Task :messaging:messaging-rabbit:jar
|
||||
> Task :messaging:messaging-reliability-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-reliability-api:classes
|
||||
> Task :messaging:messaging-reliability-api:jar
|
||||
> Task :messaging:messaging-runtime-core:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-runtime-core:classes
|
||||
> Task :messaging:messaging-runtime-core:jar
|
||||
> Task :messaging:messaging-schema-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-schema-api:classes
|
||||
> Task :messaging:messaging-schema-api:jar
|
||||
> Task :messaging:messaging-schema-json:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-schema-json:classes
|
||||
> Task :messaging:messaging-schema-json:jar
|
||||
> Task :messaging:messaging-security:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-security:classes
|
||||
> Task :messaging:messaging-security:jar
|
||||
> Task :messaging:messaging-spring-boot-starter:processResources
|
||||
> Task :messaging:messaging-spring-boot-starter:classes
|
||||
> Task :messaging:messaging-spring-boot-starter:jar
|
||||
> Task :messaging:messaging-transport-spi:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-transport-spi:classes
|
||||
> Task :messaging:messaging-transport-spi:jar
|
||||
> Task :adapter:outbound:objectstorage:compileJava
|
||||
> Task :adapter:outbound:objectstorage:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:objectstorage:classes
|
||||
> Task :adapter:outbound:objectstorage:jar
|
||||
> Task :app-bootstrap:test
|
||||
|
||||
BUILD SUCCESSFUL in 1m 45s
|
||||
98 actionable tasks: 98 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||
> Task :build-logic:compileJava UP-TO-DATE
|
||||
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||
> Task :build-logic:processResources UP-TO-DATE
|
||||
> Task :build-logic:classes UP-TO-DATE
|
||||
> Task :build-logic:jar UP-TO-DATE
|
||||
> Task :verifyCleanArchitectureDependencies
|
||||
|
||||
BUILD SUCCESSFUL in 1s
|
||||
9 actionable tasks: 1 executed, 8 up-to-date
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
# Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
# Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
# Captured execution time: 2026-08-29T06:43:52Z
|
||||
python3 - <<'PY' > /shared/document-detail/clean-architecture-backend-template/evidence/raw/013-application-core-reachability.txt
|
||||
from pathlib import Path
|
||||
root=Path('src')
|
||||
app=Path('src/application-core/src/main/java')
|
||||
packages=['cache','fileexport','filepublication','fileserver','idempotency','inbox','lease','lock','messaging','notification','objectstorage','observability','operation','outbound','outbox','realtime','security','storage','transaction','usecase']
|
||||
files=[p for p in root.rglob('src/main/java/**/*.java') if app not in p.parents]
|
||||
for pkg in packages:
|
||||
needle=f'dev.caskeleton.application.{pkg}'
|
||||
refs=[]
|
||||
for f in files:
|
||||
text=f.read_text(errors='ignore')
|
||||
if needle in text:
|
||||
refs.append(str(f))
|
||||
print(f'{pkg}\texternalProductionFiles={len(refs)}')
|
||||
for r in refs:
|
||||
print(f' {r}')
|
||||
PY
|
||||
+582
@@ -0,0 +1,582 @@
|
||||
cache externalProductionFiles=2
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/redis/RedisCapabilityConfig.java
|
||||
src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/cache/RedisCacheRegionAdapter.java
|
||||
fileexport externalProductionFiles=2
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FilesystemCsvExportAdapter.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileExportConfig.java
|
||||
filepublication externalProductionFiles=16
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/PrivateFileManifest.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/R2PublishedReferenceCodec.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalPersistentRecoveryVerifier.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FilePublishRequestFingerprint.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FilePublicationProvider.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileserverControlRecordCodec.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileserverBindingCompiler.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileserverR2Config.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalFilePublicationAdapter.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalFilePublicationPolicy.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/StreamingCsvEncoder.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileExportConfig.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FilePublicationCanonicalDigests.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalPersistentPublicationProvider.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/CompiledFileDestination.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/RoutingFilePublicationAdapter.java
|
||||
fileserver externalProductionFiles=91
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverStartupConfiguration.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverStartupValidator.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverStartupCheck.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverStorageConfiguration.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/MicrometerFileserverMetrics.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverServiceConfiguration.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverCleanupWorker.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverSecurityConfiguration.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverCleanupConfiguration.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverPlatformAutoConfiguration.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/http/MvcConditionalRequestFactory.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/http/MvcDownloadResponseWriter.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/reactive/FileserverRouterFactory.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/reactive/PartEventUploadReader.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/reactive/ReactiveDownloadResponseWriter.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/reactive/ReactiveUploadApplicationService.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/reactive/FileserverReactiveConfiguration.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/reactive/FileUploadHandler.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/reactive/FileDownloadHandler.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/config/BlockingTransferExecutor.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/config/FileserverWebProperties.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/draft12/Draft12UploadController.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/security/FileserverRequestContextFactory.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/problem/FileserverProblemFactory.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/problem/FileserverProblemTitles.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/problem/FileserverProblemHeaders.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/problem/FileserverExceptionHandler.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/tus/TusChecksumVerifier.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/tus/TusController.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/tus/TusRequestParser.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/lifecycle/FileLifecycleController.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/nginx/NginxDownloadStrategy.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/nginx/DefaultNginxInternalUriMapper.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/nginx/NginxInternalUriMapper.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/admin/FileserverAdminController.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/controller/FileDownloadController.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/controller/FileUploadController.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/mapper/RawUploadRequestMapper.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/dto/BatchUploadItemResult.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/dto/UploadedFileResponse.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/dto/BatchUploadItemProblem.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/FilesystemFailureClassifier.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalUploadStorageGateway.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/MetadataPointerContentPublisher.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalStorageFailures.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalUploadContentGateway.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/PublishResult.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalUploadHandle.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalZeroCopyDownloadGateway.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalAppendEngine.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalCleanupContentGateway.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/AtomicMoveContentPublisher.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/PhysicalPathResolver.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalOrphanScanAdapter.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/SafeFileChannelFactory.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/DefaultPhysicalPathResolver.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalStorageUsageProbe.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalStorageProbeResult.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalDownloadContentGateway.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalCopyContentGateway.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalBlockingContentStore.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalReconciliationContentProbe.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalStorageProperties.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalStorageHealthAdapter.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/ContentPublisher.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/ContentPublishVerification.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/security/UnenforcedFileAccessPolicy.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/security/RoleBasedFileAccessPolicy.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/FilenamePolicyVerifier.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/LengthVerifier.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/MediaTypeVerifier.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/VerificationCoordinator.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/Sha256Verifier.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/VerificationVerdictPrecedence.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/ScriptableContentPolicy.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/VerificationContentReader.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/LocalVerificationContentReader.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/VerificationPolicyCombiner.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/audit/StructuredFileserverAuditAdapter.java
|
||||
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/audit/StructuredAdminAuditAdapter.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaRecoveryQueue.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaContentReferenceLedger.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaStagingUploadLocator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaQuotaReclaimGateway.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaQuotaCommitGateway.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaFileQuotaService.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaFileMetadataStore.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaCleanupQueue.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaUploadSessionStore.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/FileEntityMapper.java
|
||||
src/adapter/outbound/identifier/src/main/java/dev/caskeleton/adapter/outbound/identifier/RandomUploadIdentifierFactory.java
|
||||
idempotency externalProductionFiles=30
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/redis/RedisCapabilityConfig.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/idempotency/PostgreSqlIdempotencyProviderConfig.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/idempotency/IdempotencyProviderSelectionConfig.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/idempotency/IdempotencyConfig.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/objectstorage/PosterImageObjectStorageConfig.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/idempotency/SampleIdempotencyConfig.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/PublishPosterImageUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImagePublicationFingerprint.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImagePublicationReservationCodec.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImageSanitizedIdempotencyContextFactory.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImageUploadIntent.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/outbound/persistence/mapper/PosterImageUploadIntentPersistenceMapper.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/error/GlobalExceptionHandler.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/idempotency/JsonIdempotentResponseCodec.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/idempotency/SemanticRequestFingerprintFactory.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/idempotency/IdempotencyKeySupport.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/idempotency/WebIdempotencyGate.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/idempotency/IdempotencyResponsePlan.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/idempotency/IdempotencyAdmission.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/RoutingObjectMutationAdapter.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/StagedObjectPublicationKernel.java
|
||||
src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/idempotency/RedisIdempotencyStoreAdapter.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/idempotency/IdempotencyStoreAdapter.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/operation/DurableOperationStoreAdapter.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/idempotency/mapper/IdempotencyRecordEntityMapper.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyClaimGateway.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyRecordRow.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyRowMapper.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyTransitionGateway.java
|
||||
inbox externalProductionFiles=1
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/inbox/PostgreSqlSameStoreInboxAdapter.java
|
||||
lease externalProductionFiles=2
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/redis/RedisCapabilityConfig.java
|
||||
src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/lease/RedisDistributedLeaseAdapter.java
|
||||
lock externalProductionFiles=4
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/lock/DistributedLockConfig.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/lock/MeteredDistributedLockPort.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/lock/DistributedLockPersistenceConfig.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/lock/LockRegistryDistributedLockAdapter.java
|
||||
messaging externalProductionFiles=14
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/event/WorkLogReservedPayload.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/event/WorkLogReservedContractContribution.java
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/config/CompiledMessagingDescriptor.java
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/destination/DestinationBindingSettings.java
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/destination/CompiledPublicationBinding.java
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/destination/PartitionKeyV1.java
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/destination/DestinationBindingCompiler.java
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/envelope/EnvelopeHashV1.java
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/envelope/DeterministicEnvelopeWriter.java
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/envelope/JsonSchemaIntegrationEventEncoder.java
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/envelope/LocalJsonSchemaRegistry.java
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/contract/CompiledIntegrationEventContract.java
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/contract/ContractCatalogDigest.java
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/contract/ContractCatalogCompiler.java
|
||||
notification externalProductionFiles=156
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformRegistriesConfig.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformSecretsConfig.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformProviderConfig.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformDispatchConfig.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformWorkerConfig.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformPolicyConfig.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformObservabilityConfig.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformCodecConfig.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/observation/NotificationObservationConvention.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/observation/MicrometerNotificationMetrics.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/notification/platform/submission/NotificationSubmissionController.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/notification/platform/submission/PublishTemplateHttpResponse.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/notification/platform/submission/SubmitNotificationHttpResponse.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/notification/platform/submission/NotificationVariableCoercion.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/notification/platform/submission/NotificationTemplateController.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/notification/platform/callback/CallbackRequestFactory.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/notification/platform/callback/NotificationCallbackMvcController.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/notification/platform/callback/reactive/NotificationCallbackWebFluxHandler.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/notification/platform/callback/reactive/CallbackWebFluxConfiguration.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/NotificationConfig.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/NotificationRoutesSettings.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifier.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/NotificationProvider.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/FailOpenNotificationProvider.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationReconciliationAdapter.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationAttemptContext.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderAttemptClient.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderRateAdmission.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderReadinessSnapshot.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderAttemptAdapter.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationAdmissionReadinessAdapter.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/provider/InlineNotificationAttemptAdapter.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationTemplateDescriptor.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationPlanAdapter.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationCutoverRouteCatalog.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationBindingCompiler.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationProviderCapabilityDescriptorSource.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationProviderDescriptor.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationRouteDescriptor.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationCanonicalRouteCatalog.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationProviderCapabilityCard.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/template/NotificationTemplateManifest.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/template/NotificationTemplateRenderer.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/template/LocalEmailRenderer.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/template/SlackBlockKitRenderer.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/template/NotificationTemplateCatalog.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderAttemptLimiter.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/NotificationSchedulerWorker.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/RegistryProviderDispatchGateway.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/CredentialValidationException.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderEventReplayWorker.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderRuntimeRegistry.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/RegistryProviderRuntimeControl.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/CapabilityReconciliationGateway.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/LeaseRecoveryService.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/JacksonRoutingPlanCodec.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ReconciliationJobWorker.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderRuntimeRotator.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/LoggingInboxSignalPublisher.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/SingleTenantContext.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ConfiguredProfileCatalog.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderRuntime.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/UuidV7Generator.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/MapTemplateRendererRegistry.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationPlatformAutoConfiguration.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSmtpProviderConfig.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/ProviderType.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirements.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/SmtpProviderRuntimeAssembler.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/AssembledProvider.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationProviderAssembly.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmNotificationPayloadProtection.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtector.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/SettingsSecretMaterialProvider.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmCallbackPayloadProtection.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/HmacProviderRequestIdHasher.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/CredentialGeneration.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/ProviderCredentialManager.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ProviderResults.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/UnconfiguredAttachmentResolver.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/EmailAttachments.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/ThymeleafStringTemplateEngine.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/JacksonTemplateContentCodec.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/JsonSchemaVariableValidator.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/TemplateSlotPolicy.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/Sha256MessageDigestAdapter.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/JacksonNotificationVariablesCodec.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/JacksonInboxContentCodec.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/PlaceholderTemplateEngine.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/ThymeleafNotificationRenderer.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/NotificationTemplateEngine.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/CanonicalNotificationRenderer.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/LoggingNotificationAudit.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/LoggingNotificationMetrics.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/NotificationHealthReporter.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/NotificationServingThresholds.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/reactor/ReactiveNotificationOrchestrator.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/reactor/ReactorNotificationOrchestrator.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ses/SesEventNormalizer.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ses/SesFailureClassifier.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ses/SesSuppressionUpdater.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ses/SesNotificationProviderAdapter.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ses/SesRequestMapper.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ses/SesCallbackAdapter.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ses/SesDeliveryProjector.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/Rfc8291Aes128GcmEncryptor.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/WebPushRequestMapper.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/VapidJwtSigner.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/VapidAuthorizationProvider.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/WebPushNotificationProviderAdapter.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/VapidKeyRegistry.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/WebPushFailureClassifier.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/smtp/SmtpNotificationProviderAdapter.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/smtp/SmtpMimeMessageFactory.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/smtp/SmtpFailureClassifier.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioFailureClassifier.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioRequestMapper.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioDeliveryProjector.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioCallbackAdapter.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioStatusNormalizer.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioSmsProviderAdapter.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioReconciliationCapability.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/fcm/FcmTargetMapper.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/fcm/FcmNotificationProviderAdapter.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/fcm/FcmMessageMapper.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/fcm/FcmFailureClassifier.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/fcm/FcmBatchCoordinator.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/fcm/FcmContactPointUpdater.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/apns/ApnsRequestMapper.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/apns/ApnsProviderProperties.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/apns/ApnsNotificationProviderAdapter.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/apns/ApnsFailureClassifier.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webhook/WebhookNotificationProviderAdapter.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/slack/webhook/SlackClient.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/slack/webhook/SlackWebhookProvider.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/email/google/GoogleEmailProvider.java
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/email/google/GoogleEmailClient.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaDeliveryAttemptStore.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaAdminOperationStore.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaSuppressionStore.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaNotificationRequestStore.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaTemplateRegistry.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaPolicyStores.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaRecipientLeaseStore.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaNotificationSideEffectStore.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/NotificationRecordMapper.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JdbcNotificationServingState.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JdbcReconciliationJobStore.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaRecipientDeliveryStore.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaProviderEventLedger.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/TenantBoundRepositoryGuard.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaContactPointStore.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/configuration/NotificationJpaPersistenceFacade.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/inbox/InboxCommitEventPublisher.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/inbox/InboxOutboxRecordFactory.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/inbox/JpaNotificationInbox.java
|
||||
objectstorage externalProductionFiles=96
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/objectstorage/PosterImageRetirementJob.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/objectstorage/PosterImageObjectStorageConfig.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/event/PosterImageAttachmentPrepared.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/event/PosterImageAttachmentReadyV2.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/AbortPosterImageUploadUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ReconcilePosterImageUploadUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/GetPosterImagePublicationStatusUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/PublishPosterImageUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ReconcilePosterImageRetirementUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImagePublicationPolicy.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImagePublicationFingerprint.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImageOperationIdFactory.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImagePublicationStatus.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImagePublicationReservation.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImageRetirementResult.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImagePublicationReservationCodec.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImageRetirementIntent.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImageUploadIntent.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImageUploadIntentStorePort.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImageRetirementIntentStorePort.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/command/ReconcilePosterImageUploadCommand.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/command/PublishPosterImageCommand.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/command/AbortPosterImageUploadCommand.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/command/RetirePosterImageCommand.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/command/ReconcilePosterImageRetirementCommand.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/query/GetPosterImagePublicationStatusQuery.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/objectstorage/MultipartObjectContentProducer.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/controller/PosterImagePublicationController.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/outbound/identifier/UuidPosterImageOperationIdFactory.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/outbound/persistence/mapper/PosterImageUploadIntentPersistenceMapper.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/outbound/persistence/mapper/PosterImageRetirementIntentPersistenceMapper.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/outbound/persistence/repository/PosterImageRetirementIntentRepositoryAdapter.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/outbound/persistence/repository/PosterImageUploadIntentRepositoryAdapter.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageDestinationSettings.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/RoutingObjectMutationAdapter.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageBindingCompiler.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/CompiledObjectStorageDestination.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/RoutingObjectReadAdapter.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/RoutingObjectScanMaintenanceAdapter.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageLegacyMigrationConfig.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageCapabilityAssembler.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectDirectMultipartGrantRecord.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectReferencePointerRecord.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectMultipartPartRecord.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/CanonicalJsonObjectControlRecordCodec.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectDirectMultipartSessionRecord.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectStagedObjectRecord.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectReferenceRecord.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectPublicationHandoffRecord.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectOperationRecord.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectMultipartSessionRecord.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectManifestRecord.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/provider/ObjectStorageProvider.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/provider/ObjectStorageProviderOperation.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/codec/ObjectHandleCodec.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/codec/ObjectDataKeyCodec.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/codec/ObjectControlKeyCodec.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/codec/ObjectReferenceCodec.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/codec/ObjectRequestFingerprintCodec.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/Ed25519LegacyAdoptionApprovalVerifier.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/LegacyAdoptionApprovalReplayRecord.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/LegacyObjectAdoptionService.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/LegacyObjectInspection.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectTransferSessionRecord.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectMultipartCoordinator.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectMultipartGrantLedger.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectPartAcknowledgementVerifier.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectMultipartProvider.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectGrantProvider.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectPublishedObjectResolver.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectTransferCoordinator.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectMultipartCompletionVerifier.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/ObjectPublicationHandoffService.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/ObjectOperationEpochRecord.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/ObjectOperationKernel.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/ObjectScanVerdictPolicy.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/StagedObjectPublicationKernel.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/ObjectReferencePublicationService.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/ObjectPolicySnapshot.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3DirectMultipartProvider.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3AsyncRequestBodyBridge.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3ObjectEvidenceMapper.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3ChecksumPolicy.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3ConditionalRequestMapper.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3AsyncResponseBodyBridge.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3DirectTransferProvider.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3ObjectOperationResolver.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3DirectCompletionVerifier.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3ManagedObjectProvider.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3ManagedMultipartProvider.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/multipart/MultipartOperationResolver.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/multipart/MultipartPartLedger.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/multipart/MultipartCompletionEvidence.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/filesystem/LocalObjectStreamTransfer.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/filesystem/LocalDevObjectDataStore.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/filesystem/LocalDevObjectStorageProvider.java
|
||||
observability externalProductionFiles=10
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/logging/PseudonymizationConfig.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/logging/SamplePseudonymizationConfig.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/CreateWorkLogUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/event/PosterImagePublicationEventPublisher.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/event/PosterEventPublisher.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/filter/RequestLoggingFilter.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/ratelimit/RateLimitWebConfig.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/ratelimit/VersionedEdgeSubjectPseudonymizer.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/observability/MdcCorrelationIdPortAdapter.java
|
||||
src/adapter/outbound/identifier/src/main/java/dev/caskeleton/adapter/outbound/identifier/HmacUserPrincipalPseudonymizer.java
|
||||
operation externalProductionFiles=4
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/operationasync/OperationAccessPolicy.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/operationasync/OperationQueryService.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/operationasync/OperationResourceFactory.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/operation/DurableOperationStoreAdapter.java
|
||||
outbound externalProductionFiles=11
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/objectstorage/PosterImageRetirementJob.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/command/ReconcilePosterImageUploadCommand.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/command/PublishPosterImageCommand.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/command/AbortPosterImageUploadCommand.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/command/ReconcilePosterImageRetirementCommand.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/controller/PosterImagePublicationController.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/provider/ObjectStorageProvider.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/provider/ObjectStorageProviderOperation.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/filesystem/LocalObjectStreamTransfer.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDeadlineCalculator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionRetryBackoff.java
|
||||
outbox externalProductionFiles=15
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxMetrics.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxRelayScheduler.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/messaging/DisabledMessagingSentinelAutoConfiguration.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/CreateWorkLogUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/event/PosterImagePublicationEventPublisher.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/event/PosterEventPublisher.java
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfig.java
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/outbox/DisabledOutboxMessagePublisher.java
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/outbox/Slf4jOutboxRelayFailureReportAdapter.java
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/outbox/OutboxEnvelopeJson.java
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/outbox/OutboxMessagePublishAdapter.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlImmutableOutboxAppendAdapter.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java
|
||||
realtime externalProductionFiles=12
|
||||
src/adapter/inbound/websocket/src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/resume/PortBackedReplayAvailability.java
|
||||
src/adapter/inbound/websocket/src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/cluster/MessagingFanoutAdapter.java
|
||||
src/adapter/inbound/websocket/src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/cluster/PortBackedExternalSessionIndex.java
|
||||
src/adapter/inbound/websocket/src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/presence/PortBackedPresenceStore.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/advanced/stream/replay/MessagingReplayBridge.java
|
||||
src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/realtime/RedisEphemeralFanoutAdapter.java
|
||||
src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/realtime/RedisConnectionRegistryAdapter.java
|
||||
src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/realtime/RegistrationCodec.java
|
||||
src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/realtime/RealtimeKeys.java
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/realtime/MessagingDurableFanoutAdapter.java
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/realtime/RealtimeFanoutEnvelopeJson.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/liveevent/JpaLiveEventReplayAdapter.java
|
||||
security externalProductionFiles=22
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/CreateWorkLogUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/UpdateWorkLogUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/DeleteWorkLogUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/BatchCreateWorkLogsUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ArchivePosterUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/RetirePosterImageUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/PublishPosterUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/DeletePosterUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/AbortPosterImageUploadUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/UploadPosterImageUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ReconcilePosterImageUploadUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/GetPosterImagePublicationStatusUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/UpdatePosterUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/PublishPosterImageUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ReconcilePosterImageRetirementUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/CreatePosterUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/migration/AdoptLegacyPosterImageUseCase.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/authz/MethodSecurityConfig.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/authz/AuthorizationAdapter.java
|
||||
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/authz/RequiresPermissionAuthorizationManager.java
|
||||
src/adapter/inbound/graphql/src/main/java/dev/caskeleton/adapter/inbound/graphql/security/ApplicationObjectAuthorization.java
|
||||
src/adapter/inbound/graphql/src/main/java/dev/caskeleton/adapter/inbound/graphql/security/GraphQlObjectAuthorizationPort.java
|
||||
storage externalProductionFiles=16
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/objectstorage/PosterImageApiConfig.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/UploadPosterImageUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/LegacyPosterImageAdoptionResult.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/command/AdoptLegacyPosterImageCommand.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/migration/AdoptLegacyPosterImageUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/controller/LegacyPosterImageController.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/mapper/PosterWebMapper.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/S3ObjectStorageAdapter.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/ObjectStorageConfig.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/FilesystemObjectStorageAdapter.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/LegacyObjectAdoptionSettings.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageCapabilityConfig.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageLegacyMigrationConfig.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/Ed25519LegacyAdoptionApprovalVerifier.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/LegacyObjectAdoptionService.java
|
||||
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/LegacyObjectInspector.java
|
||||
transaction externalProductionFiles=35
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformDispatchConfig.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverServiceConfiguration.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/objectstorage/PosterImageApiConfig.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/objectstorage/PosterImageObjectStorageConfig.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/ListWorkLogsUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/CreateWorkLogUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/GetWorkLogUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/ListRecentWorkLogSummariesUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/UpdateWorkLogUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/DeleteWorkLogUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/GetRepoStatsUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/BatchCreateWorkLogsUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ArchivePosterUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/GetPosterUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/RetirePosterImageUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/PublishPosterUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/DeletePosterUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/AbortPosterImageUploadUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/UploadPosterImageUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ReconcilePosterImageUploadUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/GetPosterImagePublicationStatusUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/UpdatePosterUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ListPostersUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/PublishPosterImageUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ReconcilePosterImageRetirementUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/CreatePosterUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/migration/AdoptLegacyPosterImageUseCase.java
|
||||
src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/idempotency/RedisIdempotencyStoreAdapter.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringPolicyTransactionPort.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicy.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDeadlineCalculator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/inbox/PostgreSqlSameStoreInboxAdapter.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java
|
||||
usecase externalProductionFiles=23
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/ListWorkLogsUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/CreateWorkLogUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/GetWorkLogUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/ListRecentWorkLogSummariesUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/UpdateWorkLogUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/DeleteWorkLogUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/GetRepoStatsUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/BatchCreateWorkLogsUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ArchivePosterUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/GetPosterUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/RetirePosterImageUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/PublishPosterUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/DeletePosterUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/AbortPosterImageUploadUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/UploadPosterImageUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ReconcilePosterImageUploadUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/GetPosterImagePublicationStatusUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/UpdatePosterUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ListPostersUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/PublishPosterImageUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ReconcilePosterImageRetirementUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/CreatePosterUseCase.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/migration/AdoptLegacyPosterImageUseCase.java
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Repository: /shared/codebase/clean-architecture-backend-template
|
||||
Gradle cwd: /shared/codebase/clean-architecture-backend-template/src
|
||||
|
||||
009 executed at: 2026-08-29T06:39:25Z
|
||||
009 command: ./gradlew :application-core:test --rerun-tasks
|
||||
009 exit: 0
|
||||
009 raw stdout/stderr: evidence/raw/009-application-core-test.txt
|
||||
|
||||
010a executed at: 2026-08-29T06:39:57Z
|
||||
010a command: ./gradlew :application-core:messagingApplicationContractQualificationTest --rerun-tasks
|
||||
010a exit: 1
|
||||
010a raw excerpt/reproduction record: evidence/raw/010a-messaging-qualification-missing-attestation.txt
|
||||
|
||||
010 source digest method: git archive HEAD | sha256sum
|
||||
010 source digest: sha256:e52b60c97a9496a0b18dc03f9232c295da37de7c0f56e692d000660eef0e370a
|
||||
010 artifact: application-core/build/libs/application-core-0.0.1+a24ece9cf797.jar
|
||||
010 artifact digest: sha256:6fd061854ad9a8631b65f5940f2a0792241d688aa4c11efcf392662109965942
|
||||
010 profile: config/messaging/profile-compatibility.yaml
|
||||
010 profile digest: sha256:7ac987233951a4d0427ac9e731a20df3a1c4aa1c15f30a5ed2b1e6c280b72afd
|
||||
010 executed at: 2026-08-29T06:41:19Z
|
||||
010 command: ./gradlew :application-core:messagingApplicationContractQualificationTest --rerun-tasks -PmessagingSourceDigest=sha256:e52b60c97a9496a0b18dc03f9232c295da37de7c0f56e692d000660eef0e370a -PmessagingArtifactDigest=sha256:6fd061854ad9a8631b65f5940f2a0792241d688aa4c11efcf392662109965942 -PmessagingProfileHash=sha256:7ac987233951a4d0427ac9e731a20df3a1c4aa1c15f30a5ed2b1e6c280b72afd
|
||||
010 exit: 0
|
||||
010 raw stdout/stderr: evidence/raw/010-application-core-messaging-qualification.txt
|
||||
|
||||
011 executed at: 2026-08-29T06:41:43Z
|
||||
011 command: ./gradlew :app-bootstrap:test --tests dev.caskeleton.bootstrap.architecture.CleanArchitectureTest --rerun-tasks
|
||||
011 exit: 0
|
||||
011 raw stdout/stderr: evidence/raw/011-clean-architecture-test.txt
|
||||
|
||||
012 executed at: 2026-08-29T06:43:33Z
|
||||
012 command: ./gradlew verifyCleanArchitectureDependencies
|
||||
012 exit: 0
|
||||
012 raw stdout/stderr: evidence/raw/012-verify-clean-architecture-dependencies.txt
|
||||
|
||||
013 executed at: 2026-08-29T06:43:52Z
|
||||
013 exact reproduction script: evidence/raw/013-application-core-reachability-command.sh
|
||||
013 exit: 0
|
||||
013 raw stdout: evidence/raw/013-application-core-reachability.txt
|
||||
013 observation boundary: scans textual application-package imports/references in production Java source outside application-core; it does not establish runtime execution of every type and does not cover generated/reflection-only registration.
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
Command: python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
root=Path('src/adapter/outbound/support')
|
||||
for p in sorted(x for x in root.rglob('*') if x.is_file() and 'build/' not in str(x)):
|
||||
print(p)
|
||||
print('--- counts ---')
|
||||
for label,base,pattern in [('prod-java',root/'src/main/java','*.java'),('test-java',root/'src/test/java','*.java')]:
|
||||
files=list(base.rglob(pattern)) if base.exists() else []
|
||||
print(label,'files=',len(files),'loc=',sum(len(p.read_text(errors='ignore').splitlines()) for p in files))
|
||||
r=json.load(open('src/config/architecture/modules.json'))['modules']
|
||||
print('--- registry ---')
|
||||
print(json.dumps(next(m for m in r if m['id']=='adapter-outbound-support'),indent=2))
|
||||
PY
|
||||
printf '%s\n' '--- build.gradle ---'; cat src/adapter/outbound/support/build.gradle
|
||||
printf '%s\n' '--- README.md ---'; cat src/adapter/outbound/support/README.md
|
||||
printf '%s\n' '--- CLAUDE.md ---'; cat src/adapter/outbound/support/CLAUDE.md
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T07:19:26Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Enumerates the support leaf source/build/document files and registry entry at the recorded revision; downstream consumers are not measured here.
|
||||
--- stdout/stderr ---
|
||||
src/adapter/outbound/support/CLAUDE.md
|
||||
src/adapter/outbound/support/README.md
|
||||
src/adapter/outbound/support/build.gradle
|
||||
src/adapter/outbound/support/gradle.lockfile
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/package-info.java
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundCorrelation.java
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundSupportConfig.java
|
||||
src/adapter/outbound/support/src/test/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLoggerTest.java
|
||||
--- counts ---
|
||||
prod-java files= 4 loc= 97
|
||||
test-java files= 1 loc= 89
|
||||
--- registry ---
|
||||
{
|
||||
"id": "adapter-outbound-support",
|
||||
"gradle_path": ":adapter:outbound:support",
|
||||
"source_path": "src/adapter/outbound/support",
|
||||
"allowed_dependencies": [
|
||||
"domain-core",
|
||||
"application-core",
|
||||
"shared-contract"
|
||||
],
|
||||
"runtime_memberships": [
|
||||
"app-bootstrap"
|
||||
]
|
||||
}
|
||||
--- build.gradle ---
|
||||
// Shared base for outbound integration adapters: correlation, fail-open dependency
|
||||
// logging, and the @Configuration seam. Depended on by messaging/cache/notification/httpclient.
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-autoconfigure'
|
||||
implementation 'org.slf4j:slf4j-api'
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.encoding = 'UTF-8'
|
||||
}
|
||||
--- README.md ---
|
||||
# adapter:outbound:support — 설계 결정 참조
|
||||
|
||||
아웃바운드 기술 어댑터(`:adapter:outbound:messaging`, `:adapter:outbound:cache-redis`,
|
||||
`:adapter:outbound:notification`, `:adapter:outbound:httpclient`)가 공유하는 베이스 모듈. 패키지
|
||||
루트: `dev.caskeleton.adapter.outbound.support`.
|
||||
|
||||
허용/금지 의존 정책은 `src/build.gradle` 의
|
||||
`allowedProjectDependencies['adapter:outbound:support']` 항목이 SSOT 다(이 모듈은 아직 별도
|
||||
CLAUDE.md 를 두지 않았다). 이 문서는 코드 주석에서 덜어낸 **설계 결정의 근거**를 모아둔 참조용
|
||||
기록이다 — 코드를 읽다 "왜 이렇게 했나"가 궁금할 때 본다.
|
||||
|
||||
## 왜 별도 모듈인가
|
||||
|
||||
옛 `adapter-outbound` 모듈을 기술별(`messaging`/`cache-redis`/`notification`/`httpclient`)로
|
||||
쪼개면서, 네 모듈이 공통으로 필요로 하는 조각 — correlation 조회, fail-open 의존성 로깅,
|
||||
공유 `@Configuration` 등록 지점 — 을 이 모듈로 뽑아냈다. 네 기술 모듈 모두 `implementation
|
||||
project(':adapter:outbound:support')` 로 이 모듈에 의존한다.
|
||||
|
||||
## FailOpenDependencyLogger — WARN, ERROR 아님
|
||||
|
||||
실패를 ERROR 가 아니라 WARN 으로 로깅한다: cache/messaging/notification 선택형 어댑터는
|
||||
fail-open 이라 의존성 실패가 나도 use case 는 성공한 것이다 — 관측은 하되 escalate 하지 않는다.
|
||||
이는 호출자에게 직접 노출되는 hard failure 를 ERROR 로 올리는 `httpclient` 모듈의
|
||||
`OutboundHttpDependencyLogger` 와 명확히 구분된다. 메서드 시그니처가 본문·수신자·페이로드를
|
||||
받지 않아 PII 가 로그에 닿지 않는다.
|
||||
|
||||
## OutboundCorrelation — `"unknown"` 센티넬
|
||||
|
||||
MDC 에 correlation id 가 없으면 `null` 이 아니라 `"unknown"` 문자열을 반환해 로그 라인에 필드가
|
||||
비지 않게 한다. MDC 키는 SSOT(`mdc-keys.yaml`)를 따른다.
|
||||
|
||||
## OutboundSupportConfig — 공유 `@Configuration` seam
|
||||
|
||||
`FailOpenDependencyLogger` 빈을 `@ConditionalOnMissingBean` 으로 무조건 등록하는 단일
|
||||
`@Configuration` 이다. 네 기술 모듈이 각자 로거 빈을 중복 등록하지 않고 이 모듈에만 의존하면
|
||||
와이어링되며, 포크는 같은 타입의 빈을 직접 등록해 오버라이드할 수 있다. 이 모듈 자체는 어떤
|
||||
`@ConditionalOnProperty` 게이팅도 갖지 않는다 — 게이팅은 각 기술 모듈이 자신의 연동 client
|
||||
단위로 소유한다.
|
||||
--- CLAUDE.md ---
|
||||
# adapter:outbound:support — shared outbound support
|
||||
|
||||
## Registered identity
|
||||
|
||||
- Module ID: `adapter-outbound-support`
|
||||
- Gradle path: `:adapter:outbound:support`
|
||||
- Focused test (derived from Gradle path): `./gradlew :adapter:outbound:support:test --console=plain`
|
||||
- Runtime baseline: Java 21; repository framework baseline: Spring Boot 4.0.0.
|
||||
- Registry SSOT: `src/config/architecture/modules.json`.
|
||||
|
||||
Package roots: `dev.caskeleton.adapter.outbound` and
|
||||
`dev.caskeleton.adapter.outbound.support`.
|
||||
|
||||
## Responsibility
|
||||
|
||||
- Own reusable outbound correlation, fail-open dependency logging, and auto-configuration seams.
|
||||
- Provide technical support used by registered messaging/cache/notification/httpclient leaves.
|
||||
- Keep feature-specific clients and routing policies in their owning leaf.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Allowed project dependencies are registry-owned: application, domain, and shared contracts.
|
||||
- No inbound transport, persistence, bootstrap, or sample dependencies.
|
||||
- No business rules or external-system-specific protocol implementation.
|
||||
|
||||
## Tests
|
||||
|
||||
Use focused unit tests for support behavior. Do not start a Spring application context unless the
|
||||
contract being tested is configuration wiring itself.
|
||||
|
||||
Exit code: 0
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
Command: for t in FailOpenDependencyLogger OutboundCorrelation OutboundSupportConfig; do echo "=== $t ==="; git grep -n -w "$t" -- 'src/**' ':!src/adapter/outbound/support/**'; echo "git-grep-exit=$?"; done
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T07:19:26Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Searches tracked repository text outside the support leaf for three support public types; textual search does not cover reflection, generated code, or external consumers.
|
||||
--- stdout/stderr ---
|
||||
=== FailOpenDependencyLogger ===
|
||||
src/adapter/outbound/messaging/CLAUDE.md:71: `FailOpenDependencyLogger`.
|
||||
src/adapter/outbound/messaging/README.md:42:로깅하고 삼켜(→ `:adapter:outbound:support` 의 `FailOpenDependencyLogger`) outbox/retry 로
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfig.java:10:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfig.java:33: FailOpenDependencyLogger dependencyLogger) {
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisher.java:3:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisher.java:19: private final FailOpenDependencyLogger dependencyLogger;
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisher.java:21: public OutboundMessagePublisher(MessageBroker broker, FailOpenDependencyLogger dependencyLogger) {
|
||||
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfigTest.java:11:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfigTest.java:21: .withBean(FailOpenDependencyLogger.class, FailOpenDependencyLogger::new)
|
||||
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:9:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:28: private FailOpenDependencyLogger dependencyLogger;
|
||||
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:37: dependencyLogger = new FailOpenDependencyLogger(logbackLogger);
|
||||
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:138: new OutboundMessagePublisher(broker, new FailOpenDependencyLogger(throwingLogger));
|
||||
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/outbox/OutboxMessagePublishAdapterTest.java:11:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/outbox/OutboxMessagePublishAdapterTest.java:143: (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(FailOpenDependencyLogger.class);
|
||||
src/adapter/outbound/notification/README.md:51:`:adapter:outbound:support` 의 `FailOpenDependencyLogger` 로 WARN 로깅). 라우팅은
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/NotificationConfig.java:6:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/NotificationConfig.java:33: FailOpenDependencyLogger failOpenDependencyLogger) {
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/FailOpenNotificationProvider.java:3:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/FailOpenNotificationProvider.java:17: private final FailOpenDependencyLogger dependencyLogger;
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/FailOpenNotificationProvider.java:20: NotificationProvider delegate, FailOpenDependencyLogger dependencyLogger) {
|
||||
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:10:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:47: private FailOpenDependencyLogger dependencyLogger;
|
||||
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:56: dependencyLogger = new FailOpenDependencyLogger(logbackLogger);
|
||||
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java:10:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java:55: private FailOpenDependencyLogger dependencyLogger;
|
||||
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java:64: dependencyLogger = new FailOpenDependencyLogger(logbackLogger);
|
||||
src/app-bootstrap/README.md:532: "로거가 애초에 본문/페이로드를 받지 않도록 설계한 것"(`FailOpenDependencyLogger`)이고, 이 정규식은
|
||||
git-grep-exit=0
|
||||
=== OutboundCorrelation ===
|
||||
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:10:import dev.caskeleton.adapter.outbound.support.OutboundCorrelation;
|
||||
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:96: MDC.put(OutboundCorrelation.MDC_KEY, "corr-msg-1");
|
||||
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:11:import dev.caskeleton.adapter.outbound.support.OutboundCorrelation;
|
||||
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:99: MDC.put(OutboundCorrelation.MDC_KEY, "corr-email-1");
|
||||
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:131: MDC.put(OutboundCorrelation.MDC_KEY, "corr-slack-1");
|
||||
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java:11:import dev.caskeleton.adapter.outbound.support.OutboundCorrelation;
|
||||
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java:155: MDC.put(OutboundCorrelation.MDC_KEY, "corr-1");
|
||||
src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:1358: // :adapter:outbound:support is the shared base (OutboundCorrelation, fail-open
|
||||
git-grep-exit=0
|
||||
=== OutboundSupportConfig ===
|
||||
src/app-bootstrap/src/test/java/dev/caskeleton/adapter/outbound/OptionalAdapterBeanGatingTest.java:27:import dev.caskeleton.adapter.outbound.support.OutboundSupportConfig;
|
||||
src/app-bootstrap/src/test/java/dev/caskeleton/adapter/outbound/OptionalAdapterBeanGatingTest.java:66: OutboundSupportConfig.class,
|
||||
git-grep-exit=0
|
||||
|
||||
Exit code: 0
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
Command: printf '%s\n' '--- support config annotations ---'; grep -n -E '@Configuration|@Bean|@Conditional' src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundSupportConfig.java; printf '%s\n' '--- app component scan/exclusions ---'; sed -n '35,125p' src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/CaSkeletonApplication.java; printf '%s\n' '--- registry support membership/consumers ---'; python3 - <<'PY'
|
||||
import json
|
||||
mods=json.load(open('src/config/architecture/modules.json'))['modules']
|
||||
for m in mods:
|
||||
if m['id']=='adapter-outbound-support' or 'adapter-outbound-support' in m.get('allowed_dependencies',[]):
|
||||
print(m['id'],m['gradle_path'],'runtime=',m.get('runtime_memberships'),'allows=',m.get('allowed_dependencies'))
|
||||
PY
|
||||
printf '%s\n' '--- explicit support config references ---'; git grep -n -w 'OutboundSupportConfig' -- 'src/**' ':!src/adapter/outbound/support/**'; echo "git-grep-exit=$?"
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T07:19:26Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Shows support configuration annotations, composition-root component-scan ownership, optional-adapter config references, and registry runtime membership; it does not boot the application.
|
||||
--- stdout/stderr ---
|
||||
--- support config annotations ---
|
||||
10: * {@code @ConditionalOnMissingBean} lets a fork override it.
|
||||
12:@Configuration
|
||||
15: @Bean
|
||||
16: @ConditionalOnMissingBean
|
||||
--- app component scan/exclusions ---
|
||||
* component scanner discover each child configuration on its own and assemble the capability
|
||||
* whatever that switch said. Registering them as auto-configuration instead makes "off" a
|
||||
* structural fact rather than a condition every future bean has to remember to repeat.
|
||||
*/
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan(
|
||||
basePackages = {
|
||||
"dev.caskeleton.bootstrap",
|
||||
"dev.caskeleton.adapter",
|
||||
"dev.caskeleton.application",
|
||||
"dev.caskeleton.domain",
|
||||
"dev.caskeleton.shared"
|
||||
},
|
||||
excludeFilters = {
|
||||
@ComponentScan.Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
|
||||
@ComponentScan.Filter(
|
||||
type = FilterType.CUSTOM,
|
||||
classes = AutoConfigurationExcludeFilter.class),
|
||||
@ComponentScan.Filter(
|
||||
type = FilterType.REGEX,
|
||||
pattern = CaSkeletonApplication.AUTO_CONFIGURED_PACKAGES)
|
||||
})
|
||||
@ConfigurationPropertiesScan(
|
||||
basePackages = {
|
||||
"dev.caskeleton.bootstrap.async",
|
||||
"dev.caskeleton.bootstrap.autoconfigure.fileserver",
|
||||
"dev.caskeleton.bootstrap.autoconfigure.httpclient",
|
||||
"dev.caskeleton.bootstrap.concurrency",
|
||||
"dev.caskeleton.bootstrap.idempotency",
|
||||
"dev.caskeleton.bootstrap.outbox",
|
||||
"dev.caskeleton.bootstrap.redis",
|
||||
"dev.caskeleton.bootstrap.runtime",
|
||||
"dev.caskeleton.bootstrap.security",
|
||||
"dev.caskeleton.bootstrap.settings",
|
||||
"dev.caskeleton.bootstrap.tracing",
|
||||
"dev.caskeleton.adapter.inbound.grpc",
|
||||
"dev.caskeleton.adapter.inbound.web",
|
||||
"dev.caskeleton.adapter.inbound.websocket",
|
||||
"dev.caskeleton.adapter.outbound.cache.redis",
|
||||
"dev.caskeleton.adapter.outbound.fileserver",
|
||||
"dev.caskeleton.adapter.outbound.objectstorage",
|
||||
"dev.caskeleton.application",
|
||||
"dev.caskeleton.domain",
|
||||
"dev.caskeleton.shared"
|
||||
})
|
||||
public class CaSkeletonApplication {
|
||||
|
||||
/**
|
||||
* Packages owned by an auto-configuration entry rather than by the component scan.
|
||||
*
|
||||
* <p>A package prefix rather than a class list: a new configuration added to an optional
|
||||
* capability must not become active merely because nobody remembered to exclude it.
|
||||
*
|
||||
* <p>The Fileserver admin package is here for a different reason: those routes belong to the
|
||||
* management context, and a component scan that also found them would publish the management
|
||||
* plane on the public connector — the exposure the separate context exists to remove.
|
||||
*
|
||||
* <p>The web platform's error, budget and operation packages are here for a third reason. Their
|
||||
* advices and controllers need beans that only exist when the corresponding platform
|
||||
* auto-configuration is active, and a component scan finds them regardless — so an all-off or
|
||||
* partially configured deployment failed to start on an unsatisfied dependency rather than simply
|
||||
* not installing the control. Ownership by auto-configuration is what ties a control's presence
|
||||
* to its dependency's.
|
||||
*/
|
||||
static final String AUTO_CONFIGURED_PACKAGES =
|
||||
"dev\\.caskeleton\\.bootstrap\\.autoconfigure\\..*"
|
||||
+ "|dev\\.caskeleton\\.bootstrap\\.notification\\..*"
|
||||
+ "|dev\\.caskeleton\\.adapter\\.inbound\\.web\\.fileserver\\.admin\\..*"
|
||||
+ "|dev\\.caskeleton\\.adapter\\.outbound\\.mongo\\..*"
|
||||
+ "|dev\\.caskeleton\\.adapter\\.outbound\\.messaging\\..*"
|
||||
+ "|dev\\.caskeleton\\.adapter\\.outbound\\.notification\\..*"
|
||||
+ "|dev\\.caskeleton\\.adapter\\.outbound\\.persistence\\..*"
|
||||
+ "|dev\\.caskeleton\\.adapter\\.inbound\\.graphql\\..*"
|
||||
+ "|dev\\.caskeleton\\.adapter\\.inbound\\.web\\.mvc\\.error\\..*"
|
||||
+ "|dev\\.caskeleton\\.adapter\\.inbound\\.web\\.mvc\\.budget\\..*"
|
||||
+ "|dev\\.caskeleton\\.adapter\\.inbound\\.web\\.mvc\\.operation\\..*"
|
||||
+ "|dev\\.caskeleton\\.adapter\\.inbound\\.web\\.webflux\\.error\\..*"
|
||||
+ "|dev\\.caskeleton\\.adapter\\.inbound\\.web\\.webflux\\.operation\\..*"
|
||||
+ "|dev\\.caskeleton\\.messaging\\..*";
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(CaSkeletonApplication.class, args);
|
||||
}
|
||||
}
|
||||
--- registry support membership/consumers ---
|
||||
adapter-outbound-support :adapter:outbound:support runtime= ['app-bootstrap'] allows= ['domain-core', 'application-core', 'shared-contract']
|
||||
adapter-outbound-cache-redis :adapter:outbound:cache-redis runtime= ['app-bootstrap'] allows= ['domain-core', 'application-core', 'shared-contract', 'adapter-outbound-support']
|
||||
adapter-outbound-httpclient :adapter:outbound:httpclient runtime= ['app-bootstrap'] allows= ['domain-core', 'application-core', 'shared-contract', 'adapter-outbound-support']
|
||||
adapter-outbound-messaging :adapter:outbound:messaging runtime= ['app-bootstrap'] allows= ['domain-core', 'application-core', 'shared-contract', 'adapter-outbound-support']
|
||||
adapter-outbound-notification :adapter:outbound:notification runtime= ['app-bootstrap'] allows= ['domain-core', 'application-core', 'shared-contract', 'adapter-outbound-support']
|
||||
app-bootstrap :app-bootstrap runtime= ['app-bootstrap'] allows= ['domain-core', 'application-core', 'adapter-outbound-persistence-jpa', 'adapter-outbound-support', 'adapter-outbound-messaging', 'adapter-outbound-cache-redis', 'adapter-outbound-notification', 'adapter-outbound-fileserver', 'adapter-outbound-httpclient', 'adapter-outbound-identifier', 'adapter-inbound-web', 'shared-contract', 'adapter-outbound-persistence-mongo', 'adapter-inbound-graphql', 'messaging-spring-boot-starter']
|
||||
--- explicit support config references ---
|
||||
src/app-bootstrap/src/test/java/dev/caskeleton/adapter/outbound/OptionalAdapterBeanGatingTest.java:27:import dev.caskeleton.adapter.outbound.support.OutboundSupportConfig;
|
||||
src/app-bootstrap/src/test/java/dev/caskeleton/adapter/outbound/OptionalAdapterBeanGatingTest.java:66: OutboundSupportConfig.class,
|
||||
git-grep-exit=0
|
||||
|
||||
Exit code: 0
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
Command: printf '%s\n' '--- support imports in production ---'; git grep -n 'dev\.caskeleton\.adapter\.outbound\.support' -- 'src/**/src/main/java/**/*.java' ':!src/adapter/outbound/support/**'; echo "support-import-exit=$?"; printf '%s\n' '--- dependency logger names ---'; git grep -n -E 'FailOpenDependencyLogger|OutboundHttpDependencyLogger|DependencyLogger|dependency_name=' -- 'src/adapter/outbound/**' ':!src/adapter/outbound/support/build/**'; echo "logger-search-exit=$?"; printf '%s\n' '--- correlation implementations/usages ---'; git grep -n -E 'OutboundCorrelation|MDC\.get\(|correlation_id' -- 'src/adapter/outbound/**/src/main/java/**/*.java'; echo "correlation-search-exit=$?"
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T07:19:26Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Searches current outbound production sources and docs for dependency-logger/correlation/fail-open mechanisms and direct support imports; matches require semantic comparison before being called duplicates.
|
||||
--- stdout/stderr ---
|
||||
--- support imports in production ---
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfig.java:10:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisher.java:3:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/NotificationConfig.java:6:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/FailOpenNotificationProvider.java:3:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
support-import-exit=0
|
||||
--- dependency logger names ---
|
||||
src/adapter/outbound/messaging/CLAUDE.md:71: `FailOpenDependencyLogger`.
|
||||
src/adapter/outbound/messaging/README.md:42:로깅하고 삼켜(→ `:adapter:outbound:support` 의 `FailOpenDependencyLogger`) outbox/retry 로
|
||||
src/adapter/outbound/messaging/README.md:68:`dependency_name=disabled`를 쓴다. 이 adapter는 confirmed FAILED/DEAD report 하나를 SLF4J 2 fluent
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfig.java:10:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfig.java:33: FailOpenDependencyLogger dependencyLogger) {
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisher.java:3:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisher.java:19: private final FailOpenDependencyLogger dependencyLogger;
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisher.java:21: public OutboundMessagePublisher(MessageBroker broker, FailOpenDependencyLogger dependencyLogger) {
|
||||
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfigTest.java:11:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfigTest.java:21: .withBean(FailOpenDependencyLogger.class, FailOpenDependencyLogger::new)
|
||||
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:9:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:28: private FailOpenDependencyLogger dependencyLogger;
|
||||
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:37: dependencyLogger = new FailOpenDependencyLogger(logbackLogger);
|
||||
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:106: .contains("dependency_name=\"kafka\"")
|
||||
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:138: new OutboundMessagePublisher(broker, new FailOpenDependencyLogger(throwingLogger));
|
||||
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/outbox/OutboxMessagePublishAdapterTest.java:11:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/outbox/OutboxMessagePublishAdapterTest.java:143: (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(FailOpenDependencyLogger.class);
|
||||
src/adapter/outbound/notification/README.md:51:`:adapter:outbound:support` 의 `FailOpenDependencyLogger` 로 WARN 로깅). 라우팅은
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/NotificationConfig.java:6:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/NotificationConfig.java:33: FailOpenDependencyLogger failOpenDependencyLogger) {
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/NotificationConfig.java:36: .map(p -> new FailOpenNotificationProvider(p, failOpenDependencyLogger))
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/FailOpenNotificationProvider.java:3:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/FailOpenNotificationProvider.java:17: private final FailOpenDependencyLogger dependencyLogger;
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/FailOpenNotificationProvider.java:20: NotificationProvider delegate, FailOpenDependencyLogger dependencyLogger) {
|
||||
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:10:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:47: private FailOpenDependencyLogger dependencyLogger;
|
||||
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:56: dependencyLogger = new FailOpenDependencyLogger(logbackLogger);
|
||||
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:113: .contains("dependency_name=\"google-email\"")
|
||||
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:145: .contains("dependency_name=\"slack-webhook\"")
|
||||
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java:10:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java:55: private FailOpenDependencyLogger dependencyLogger;
|
||||
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java:64: dependencyLogger = new FailOpenDependencyLogger(logbackLogger);
|
||||
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java:169: .contains("dependency_name=\"google-email\"")
|
||||
src/adapter/outbound/support/README.md:19:## FailOpenDependencyLogger — WARN, ERROR 아님
|
||||
src/adapter/outbound/support/README.md:24:`OutboundHttpDependencyLogger` 와 명확히 구분된다. 메서드 시그니처가 본문·수신자·페이로드를
|
||||
src/adapter/outbound/support/README.md:34:`FailOpenDependencyLogger` 빈을 `@ConditionalOnMissingBean` 으로 무조건 등록하는 단일
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java:13:public final class FailOpenDependencyLogger {
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java:17: public FailOpenDependencyLogger() {
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java:18: this(LoggerFactory.getLogger(FailOpenDependencyLogger.class));
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java:22: public FailOpenDependencyLogger(Logger log) {
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java:28: "dependency_name=\"{}\" dependency_type=\"{}\" operation=\"{}\" "
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java:40: "dependency_name=\"{}\" dependency_type=\"{}\" operation=\"{}\" "
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundSupportConfig.java:8: * Unconditionally registers the shared log/error contract bean ({@link FailOpenDependencyLogger})
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundSupportConfig.java:17: public FailOpenDependencyLogger failOpenDependencyLogger() {
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundSupportConfig.java:18: return new FailOpenDependencyLogger();
|
||||
src/adapter/outbound/support/src/test/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLoggerTest.java:14:class FailOpenDependencyLoggerTest {
|
||||
src/adapter/outbound/support/src/test/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLoggerTest.java:18: private FailOpenDependencyLogger dependencyLogger;
|
||||
src/adapter/outbound/support/src/test/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLoggerTest.java:28: dependencyLogger = new FailOpenDependencyLogger(logbackLogger);
|
||||
src/adapter/outbound/support/src/test/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLoggerTest.java:48: .contains("dependency_name=\"kafka\"")
|
||||
src/adapter/outbound/support/src/test/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLoggerTest.java:84: .contains("dependency_name=\"slack\"")
|
||||
logger-search-exit=0
|
||||
--- correlation implementations/usages ---
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/outbox/Slf4jOutboxRelayFailureReportAdapter.java:52: .addKeyValue("correlation_id", report.correlationId())
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/NotificationRequestEntity.java:73: @Column(name = "correlation_id", length = 160)
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/NotificationRequestJpaRepository.java:53: + " schedule_at, not_before, expires_at, request_status, correlation_id,"
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/entity/OutboxEventEntity.java:49: @Column(name = "correlation_id", nullable = false, length = 64, updatable = false)
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlImmutableOutboxAppendAdapter.java:103: correlation_id,
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlImmutableOutboxAppendAdapter.java:128: event.correlation_id,
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlImmutableOutboxAppendAdapter.java:286: resultSet.getString("correlation_id"),
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java:148: event.correlation_id,
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java:413: resultSet.getString("correlation_id"),
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java:29: + "outcome=\"SUCCESS\" correlation_id=\"{}\"",
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java:33: OutboundCorrelation.current());
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java:41: + "outcome=\"FAILURE\" correlation_id=\"{}\" error=\"{}: {}\"",
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java:45: OutboundCorrelation.current(),
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundCorrelation.java:6: * Reads the {@code correlation_id} from the SLF4J MDC for outbound adapter logging. The MDC key
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundCorrelation.java:11:public final class OutboundCorrelation {
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundCorrelation.java:13: /** MDC key SSOT (docs/registries/mdc-keys.yaml — key: correlation_id). */
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundCorrelation.java:14: public static final String MDC_KEY = "correlation_id";
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundCorrelation.java:19: private OutboundCorrelation() {}
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundCorrelation.java:23: String value = MDC.get(MDC_KEY);
|
||||
correlation-search-exit=0
|
||||
|
||||
Exit code: 0
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
Command: printf '%s\n' '--- support README named claims ---'; grep -n -E 'allowedProjectDependencies|CLAUDE|OutboundHttpDependencyLogger|project\(' src/adapter/outbound/support/README.md; printf '%s\n' '--- current policy authority ---'; git grep -n -E 'modules\.json|verifyCleanArchitectureDependencies' -- AGENTS.md src/adapter/outbound/support/CLAUDE.md src/build.gradle | head -180; printf '%s\n' '--- current exact http logger references ---'; git grep -n -w 'OutboundHttpDependencyLogger' -- 'src/**'; echo "http-logger-current-exit=$?"; printf '%s\n' '--- symbol history ---'; git log --all --name-status --format='COMMIT %H %s' -S'OutboundHttpDependencyLogger' -- src | sed -n '1,180p'
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T07:19:26Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Compares named support README claims with current policy documents/source and records history for OutboundHttpDependencyLogger; this is textual/document history evidence, not a general documentation audit.
|
||||
--- stdout/stderr ---
|
||||
--- support README named claims ---
|
||||
8:`allowedProjectDependencies['adapter:outbound:support']` 항목이 SSOT 다(이 모듈은 아직 별도
|
||||
9:CLAUDE.md 를 두지 않았다). 이 문서는 코드 주석에서 덜어낸 **설계 결정의 근거**를 모아둔 참조용
|
||||
17:project(':adapter:outbound:support')` 로 이 모듈에 의존한다.
|
||||
24:`OutboundHttpDependencyLogger` 와 명확히 구분된다. 메서드 시그니처가 본문·수신자·페이로드를
|
||||
--- current policy authority ---
|
||||
AGENTS.md:21:5. 프로젝트 의존성이 `src/config/architecture/modules.json` 또는 Gradle 의존성 검증을 위반한다.
|
||||
AGENTS.md:52:- `src/config/architecture/modules.json`: 등록된 모든 leaf의 ID, repository-relative 소스 경로,
|
||||
AGENTS.md:57:- `src/build.gradle`: 같은 registry를 읽는 `verifyCleanArchitectureDependencies`와 그 밖의
|
||||
AGENTS.md:107:`src/config/architecture/modules.json`이 SSOT다. focused test는 소유 leaf의 `gradle_path`에서
|
||||
AGENTS.md:136:개별 edge는 `src/config/architecture/modules.json`의 `allowed_dependencies`가 유일한 목록이다.
|
||||
AGENTS.md:179:./gradlew check # check 가 verifyCleanArchitectureDependencies + verifyEnvKeys 2종을 전이 실행한다 (src/build.gradle)
|
||||
AGENTS.md:180:./gradlew verifyCleanArchitectureDependencies
|
||||
AGENTS.md:185:소유 leaf의 정확한 Gradle path는 `src/config/architecture/modules.json`에서 읽고 focused test
|
||||
src/adapter/outbound/support/CLAUDE.md:9:- Registry SSOT: `src/config/architecture/modules.json`.
|
||||
src/build.gradle:574: dependsOn rootProject.tasks.named('verifyCleanArchitectureDependencies')
|
||||
src/build.gradle:1101: dependsOn 'verifyCleanArchitectureDependencies'
|
||||
src/build.gradle:1310: File moduleRegistryFile = new File(rootProject.projectDir, 'config/architecture/modules.json')
|
||||
src/build.gradle:1401:tasks.register('verifyCleanArchitectureDependencies') {
|
||||
src/build.gradle:1405: File moduleRegistryFile = new File(rootProject.projectDir, 'config/architecture/modules.json')
|
||||
src/build.gradle:1440: "Gradle modules ${missingFromPolicy} are not covered by verifyCleanArchitectureDependencies. " +
|
||||
src/build.gradle:2152: File moduleRegistryFile = new File(rootProject.projectDir, 'config/architecture/modules.json')
|
||||
--- current exact http logger references ---
|
||||
src/adapter/outbound/support/README.md:24:`OutboundHttpDependencyLogger` 와 명확히 구분된다. 메서드 시그니처가 본문·수신자·페이로드를
|
||||
http-logger-current-exit=0
|
||||
--- symbol history ---
|
||||
COMMIT 5f10b791d35a02a4842814b90a0e0362dcef3982 chore: record pre-existing uncommitted repository state
|
||||
|
||||
M src/adapter/outbound/httpclient/README.md
|
||||
D src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpCallObserver.java
|
||||
D src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClient.java
|
||||
D src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClientConfig.java
|
||||
D src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/diagnostics/OutboundHttpDependencyLogger.java
|
||||
D src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpCallObserverTest.java
|
||||
D src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClientDeadlineTest.java
|
||||
D src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClientSafetyRegressionTest.java
|
||||
D src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClientTest.java
|
||||
D src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/diagnostics/OutboundHttpDependencyLoggerTest.java
|
||||
M src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java
|
||||
COMMIT 567422f2e5ff0557e76b233fe5174812d63b515e feat: add production capability foundations
|
||||
|
||||
M src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java
|
||||
COMMIT b3add0162df8d4a0a11e749e514901defe0a62a3 feat: redis, fileserver, httpclient 런타임 시점 구현 추가
|
||||
|
||||
A src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClientDeadlineTest.java
|
||||
A src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClientSafetyRegressionTest.java
|
||||
COMMIT 821fe00c323b5335980f271c7ee47b92ac2168f2 init: 클린 아키텍처 백엔드
|
||||
|
||||
A src/adapter/outbound/httpclient/README.md
|
||||
A src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpCallObserver.java
|
||||
A src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClient.java
|
||||
A src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClientConfig.java
|
||||
A src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/diagnostics/OutboundHttpDependencyLogger.java
|
||||
A src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpCallObserverTest.java
|
||||
A src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClientTest.java
|
||||
A src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/diagnostics/OutboundHttpDependencyLoggerTest.java
|
||||
A src/adapter/outbound/support/README.md
|
||||
|
||||
Exit code: 0
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
Command: for m in cache-redis httpclient messaging notification; do echo "=== $m ==="; grep -n "adapter:outbound:support" "src/adapter/outbound/$m/build.gradle"; echo "build-grep-exit=$?"; git grep -n 'dev\.caskeleton\.adapter\.outbound\.support' -- "src/adapter/outbound/$m/src/main/java/**/*.java"; echo "source-grep-exit=$?"; done; printf '%s\n' '--- support resources ---'; find src/adapter/outbound/support/src -type f | sort
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T07:19:26Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Compares four outbound leaves that declare a Gradle dependency on support with current production Java textual imports of support types; resource/reflection-only usage would require separate evidence.
|
||||
--- stdout/stderr ---
|
||||
=== cache-redis ===
|
||||
15: implementation project(':adapter:outbound:support')
|
||||
build-grep-exit=0
|
||||
source-grep-exit=1
|
||||
=== httpclient ===
|
||||
14: implementation project(':adapter:outbound:support')
|
||||
build-grep-exit=0
|
||||
source-grep-exit=1
|
||||
=== messaging ===
|
||||
5: implementation project(':adapter:outbound:support')
|
||||
build-grep-exit=0
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfig.java:10:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisher.java:3:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
source-grep-exit=0
|
||||
=== notification ===
|
||||
4: implementation project(':adapter:outbound:support')
|
||||
build-grep-exit=0
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/NotificationConfig.java:6:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/FailOpenNotificationProvider.java:3:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
source-grep-exit=0
|
||||
--- support resources ---
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/package-info.java
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundCorrelation.java
|
||||
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundSupportConfig.java
|
||||
src/adapter/outbound/support/src/test/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLoggerTest.java
|
||||
|
||||
Exit code: 0
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
Command: CP='adapter/outbound/support/build/classes/java/main:/root/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/2.0.18/78a9e7a37cd6360e0b818e86341b24123d28d4df/slf4j-api-2.0.18.jar:/root/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-classic/1.5.38/bc5873cb4a7f502ce18ce2977d98ad26b9f96444/logback-classic-1.5.38.jar:/root/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-core/1.5.38/7b1d133b29bf42ff268cf3f71d1308b80480fb8d/logback-core-1.5.38.jar'; java --class-path "$CP" /shared/document-detail/clean-architecture-backend-template/evidence/raw/021a-support-logger-pii-probe.java
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template/src
|
||||
Executed at: 2026-08-29T07:19:45Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Runs the current compiled FailOpenDependencyLogger with an exception message containing explicit test PII markers; it demonstrates rendered logger input/output for this controlled probe, not whether a specific external provider emits such messages in production.
|
||||
--- stdout/stderr ---
|
||||
07:19:46.022 [main] WARN probe.support.pii -- dependency_name="google-email" dependency_type="notification" operation="send" outcome="FAILURE" correlation_id="unknown" error="RuntimeException: provider rejected recipient secret@gmail.com body=secret-body-content"
|
||||
dependency_name="google-email" dependency_type="notification" operation="send" outcome="FAILURE" correlation_id="unknown" error="RuntimeException: provider rejected recipient secret@gmail.com body=secret-body-content"
|
||||
|
||||
Exit code: 0
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.read.ListAppender;
|
||||
import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class SupportLoggerLeakProbe {
|
||||
public static void main(String[] args) {
|
||||
var logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("probe.support.pii");
|
||||
var appender = new ListAppender<ILoggingEvent>();
|
||||
appender.start();
|
||||
logger.addAppender(appender);
|
||||
logger.setLevel(Level.DEBUG);
|
||||
try {
|
||||
new FailOpenDependencyLogger(logger).logFailure(
|
||||
"google-email", "notification", "send",
|
||||
new RuntimeException("provider rejected recipient secret@gmail.com body=secret-body-content"));
|
||||
System.out.println(appender.list.get(0).getFormattedMessage());
|
||||
} finally {
|
||||
logger.detachAppender(appender);
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
Command: CP='adapter/outbound/notification/build/classes/java/main:adapter/outbound/support/build/classes/java/main:application-core/build/classes/java/main:shared-contract/build/classes/java/main:/root/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/2.0.18/78a9e7a37cd6360e0b818e86341b24123d28d4df/slf4j-api-2.0.18.jar'; java --class-path "$CP" /shared/document-detail/clean-architecture-backend-template/evidence/raw/022a-notification-logger-failure-probe.java
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template/src
|
||||
Executed at: 2026-08-29T07:19:46Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Runs the current compiled FailOpenNotificationProvider with a deliberately throwing SLF4J Logger proxy to observe success/failure logging control flow; it is a focused local probe, not a real logging-backend outage test.
|
||||
--- stdout/stderr ---
|
||||
SLF4J(W): No SLF4J providers were found.
|
||||
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
|
||||
SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.
|
||||
SUCCESS_PATH sends=1 debugCalls=1 warnCalls=1
|
||||
FAILURE_PATH propagated=IllegalStateException:logger-warn-failed warnCalls=1
|
||||
|
||||
Exit code: 0
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import dev.caskeleton.adapter.outbound.notification.core.FailOpenNotificationProvider;
|
||||
import dev.caskeleton.adapter.outbound.notification.core.NotificationProvider;
|
||||
import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
|
||||
import dev.caskeleton.application.notification.Channel;
|
||||
import dev.caskeleton.application.notification.Notification;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
public class NotificationLoggerFailureProbe {
|
||||
private static Logger loggerThatThrowsOn(String methodToThrow, AtomicInteger debug, AtomicInteger warn) {
|
||||
return (Logger) Proxy.newProxyInstance(
|
||||
NotificationLoggerFailureProbe.class.getClassLoader(),
|
||||
new Class<?>[] {Logger.class},
|
||||
(proxy, method, args) -> {
|
||||
if (method.getName().equals("debug")) debug.incrementAndGet();
|
||||
if (method.getName().equals("warn")) warn.incrementAndGet();
|
||||
if (method.getName().equals(methodToThrow)) {
|
||||
throw new IllegalStateException("logger-" + methodToThrow + "-failed");
|
||||
}
|
||||
if (method.getReturnType() == boolean.class) return true;
|
||||
if (method.getReturnType() == String.class) return "probe.notification";
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Notification n = new Notification("secret@gmail.com", "subject", "body");
|
||||
|
||||
AtomicInteger sends = new AtomicInteger();
|
||||
AtomicInteger debug = new AtomicInteger();
|
||||
AtomicInteger warn = new AtomicInteger();
|
||||
NotificationProvider successful = new NotificationProvider() {
|
||||
public Channel channel() { return Channel.EMAIL; }
|
||||
public String providerId() { return "google-email"; }
|
||||
public void send(Notification ignored) { sends.incrementAndGet(); }
|
||||
};
|
||||
var successPath = new FailOpenNotificationProvider(
|
||||
successful, new FailOpenDependencyLogger(loggerThatThrowsOn("debug", debug, warn)));
|
||||
successPath.send(n);
|
||||
System.out.printf("SUCCESS_PATH sends=%d debugCalls=%d warnCalls=%d%n", sends.get(), debug.get(), warn.get());
|
||||
|
||||
AtomicInteger failDebug = new AtomicInteger();
|
||||
AtomicInteger failWarn = new AtomicInteger();
|
||||
NotificationProvider failing = new NotificationProvider() {
|
||||
public Channel channel() { return Channel.EMAIL; }
|
||||
public String providerId() { return "google-email"; }
|
||||
public void send(Notification ignored) { throw new RuntimeException("provider-failed"); }
|
||||
};
|
||||
var failurePath = new FailOpenNotificationProvider(
|
||||
failing, new FailOpenDependencyLogger(loggerThatThrowsOn("warn", failDebug, failWarn)));
|
||||
try {
|
||||
failurePath.send(n);
|
||||
System.out.println("FAILURE_PATH propagated=none");
|
||||
} catch (RuntimeException ex) {
|
||||
System.out.printf("FAILURE_PATH propagated=%s:%s warnCalls=%d%n",
|
||||
ex.getClass().getSimpleName(), ex.getMessage(), failWarn.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
Command: ./gradlew :adapter:outbound:support:test --rerun-tasks --console=plain
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template/src
|
||||
Executed at: 2026-08-29T07:19:56Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Fresh focused unit-test execution for the support leaf; it does not exercise downstream adapter/provider runtime behavior.
|
||||
--- stdout/stderr ---
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
> Task :adapter:outbound:support:compileJava
|
||||
> Task :adapter:outbound:support:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:support:classes
|
||||
> Task :adapter:outbound:support:compileTestJava
|
||||
> Task :adapter:outbound:support:processTestResources NO-SOURCE
|
||||
> Task :adapter:outbound:support:testClasses
|
||||
> Task :adapter:outbound:support:test
|
||||
|
||||
BUILD SUCCESSFUL in 7s
|
||||
11 actionable tasks: 11 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
Exit code: 0
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
Command: ./gradlew :adapter:outbound:support:dependencies --configuration compileClasspath --console=plain
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template/src
|
||||
Executed at: 2026-08-29T07:20:03Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Gradle compileClasspath resolution for support at this revision; it reports declared/resolved compile dependencies, not runtime consumers.
|
||||
--- stdout/stderr ---
|
||||
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||
> Task :build-logic:compileJava UP-TO-DATE
|
||||
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||
> Task :build-logic:processResources UP-TO-DATE
|
||||
> Task :build-logic:classes UP-TO-DATE
|
||||
> Task :build-logic:jar UP-TO-DATE
|
||||
|
||||
> Task :adapter:outbound:support:dependencies
|
||||
|
||||
------------------------------------------------------------
|
||||
Project ':adapter:outbound:support'
|
||||
------------------------------------------------------------
|
||||
|
||||
compileClasspath - Compile classpath for source set 'main'.
|
||||
+--- org.springframework.boot:spring-boot-autoconfigure -> 4.0.8
|
||||
| \--- org.springframework.boot:spring-boot:4.0.8
|
||||
| +--- org.springframework:spring-core:7.0.9
|
||||
| | +--- commons-logging:commons-logging:1.3.5 -> 1.3.6
|
||||
| | \--- org.jspecify:jspecify:1.0.0 -> 1.0.1
|
||||
| \--- org.springframework:spring-context:7.0.9
|
||||
| +--- org.springframework:spring-aop:7.0.9
|
||||
| | +--- org.springframework:spring-beans:7.0.9
|
||||
| | | \--- org.springframework:spring-core:7.0.9 (*)
|
||||
| | \--- org.springframework:spring-core:7.0.9 (*)
|
||||
| +--- org.springframework:spring-beans:7.0.9 (*)
|
||||
| +--- org.springframework:spring-core:7.0.9 (*)
|
||||
| +--- org.springframework:spring-expression:7.0.9
|
||||
| | \--- org.springframework:spring-core:7.0.9 (*)
|
||||
| \--- io.micrometer:micrometer-observation:1.16.7
|
||||
| +--- org.jspecify:jspecify:1.0.1
|
||||
| \--- io.micrometer:micrometer-commons:1.16.7
|
||||
| \--- org.jspecify:jspecify:1.0.1
|
||||
+--- org.slf4j:slf4j-api -> 2.0.18
|
||||
+--- org.slf4j:slf4j-api:{strictly 2.0.18} -> 2.0.18 (c)
|
||||
+--- org.springframework.boot:spring-boot-autoconfigure:{strictly 4.0.8} -> 4.0.8 (c)
|
||||
+--- org.springframework.boot:spring-boot:{strictly 4.0.8} -> 4.0.8 (c)
|
||||
+--- org.springframework:spring-core:{strictly 7.0.9} -> 7.0.9 (c)
|
||||
+--- org.springframework:spring-context:{strictly 7.0.9} -> 7.0.9 (c)
|
||||
+--- commons-logging:commons-logging:{strictly 1.3.6} -> 1.3.6 (c)
|
||||
+--- org.jspecify:jspecify:{strictly 1.0.1} -> 1.0.1 (c)
|
||||
+--- org.springframework:spring-aop:{strictly 7.0.9} -> 7.0.9 (c)
|
||||
+--- org.springframework:spring-beans:{strictly 7.0.9} -> 7.0.9 (c)
|
||||
+--- org.springframework:spring-expression:{strictly 7.0.9} -> 7.0.9 (c)
|
||||
+--- io.micrometer:micrometer-observation:{strictly 1.16.7} -> 1.16.7 (c)
|
||||
\--- io.micrometer:micrometer-commons:{strictly 1.16.7} -> 1.16.7 (c)
|
||||
|
||||
(c) - A dependency constraint, not a dependency. The dependency affected by the constraint occurs elsewhere in the tree.
|
||||
(*) - Indicates repeated occurrences of a transitive dependency subtree. Gradle expands transitive dependency subtrees only once per project; repeat occurrences only display the root of the subtree, followed by this annotation.
|
||||
|
||||
A web-based, searchable dependency report is available by adding the --scan option.
|
||||
|
||||
BUILD SUCCESSFUL in 918ms
|
||||
9 actionable tasks: 1 executed, 8 up-to-date
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
Exit code: 0
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
Command: ./gradlew verifyCleanArchitectureDependencies --console=plain
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template/src
|
||||
Executed at: 2026-08-29T07:20:04Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Fresh architecture-registry project-edge verification; it verifies registered Gradle dependency policy, not semantic necessity of every allowed edge.
|
||||
--- stdout/stderr ---
|
||||
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||
> Task :build-logic:compileJava UP-TO-DATE
|
||||
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||
> Task :build-logic:processResources UP-TO-DATE
|
||||
> Task :build-logic:classes UP-TO-DATE
|
||||
> Task :build-logic:jar UP-TO-DATE
|
||||
> Task :verifyCleanArchitectureDependencies
|
||||
|
||||
BUILD SUCCESSFUL in 788ms
|
||||
9 actionable tasks: 1 executed, 8 up-to-date
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
Exit code: 0
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
Command: ./gradlew :adapter:outbound:messaging:test --tests dev.caskeleton.adapter.outbound.messaging.core.OutboundMessagePublisherTest --rerun-tasks --console=plain
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template/src
|
||||
Executed at: 2026-08-29T07:20:17Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Fresh execution of the messaging publisher test class that contains the logger-failure-after-send regression; it covers the messaging consumer only.
|
||||
--- stdout/stderr ---
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
> Task :shared-contract:compileJava
|
||||
> Task :shared-contract:processResources
|
||||
> Task :shared-contract:classes
|
||||
> Task :shared-contract:jar
|
||||
> Task :application-core:compileJava
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes
|
||||
> Task :application-core:jar
|
||||
> Task :adapter:outbound:support:compileJava
|
||||
> Task :adapter:outbound:support:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:support:classes
|
||||
> Task :adapter:outbound:support:jar
|
||||
> Task :adapter:outbound:messaging:compileJava
|
||||
> Task :adapter:outbound:messaging:processResources
|
||||
> Task :adapter:outbound:messaging:classes
|
||||
> Task :adapter:outbound:messaging:compileTestJava
|
||||
> Task :adapter:outbound:messaging:processTestResources
|
||||
> Task :adapter:outbound:messaging:testClasses
|
||||
> Task :adapter:outbound:messaging:test
|
||||
|
||||
BUILD SUCCESSFUL in 19s
|
||||
20 actionable tasks: 20 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
Exit code: 0
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
Command: ./gradlew :adapter:outbound:notification:test --tests dev.caskeleton.adapter.outbound.notification.core.NotificationAdapterTest --rerun-tasks --console=plain
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template/src
|
||||
Executed at: 2026-08-29T07:20:37Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Fresh execution of the current notification adapter behavior tests; it covers ordinary Logback behavior but contains no throwing-logger regression.
|
||||
--- stdout/stderr ---
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
> Task :shared-contract:compileJava
|
||||
> Task :shared-contract:processResources
|
||||
> Task :shared-contract:classes
|
||||
> Task :shared-contract:jar
|
||||
> Task :application-core:compileJava
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes
|
||||
> Task :application-core:jar
|
||||
> Task :adapter:outbound:support:compileJava
|
||||
> Task :adapter:outbound:support:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:support:classes
|
||||
> Task :adapter:outbound:support:jar
|
||||
> Task :adapter:outbound:notification:compileJava
|
||||
> Task :adapter:outbound:notification:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:notification:classes
|
||||
> Task :adapter:outbound:notification:compileTestJava
|
||||
> Task :adapter:outbound:notification:processTestResources
|
||||
> Task :adapter:outbound:notification:testClasses
|
||||
> Task :adapter:outbound:notification:test
|
||||
|
||||
BUILD SUCCESSFUL in 20s
|
||||
19 actionable tasks: 19 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
Exit code: 0
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
Command: ./gradlew :app-bootstrap:test --tests dev.caskeleton.adapter.outbound.OptionalAdapterBeanGatingTest --rerun-tasks --console=plain
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template/src
|
||||
Executed at: 2026-08-29T07:20:57Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Fresh composition-level test using OutboundSupportConfig with optional outbound adapter configurations; its runner supplies selected configs explicitly rather than booting the full application.
|
||||
--- stdout/stderr ---
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
> Task :shared-contract:compileJava
|
||||
> Task :shared-contract:processResources
|
||||
> Task :shared-contract:classes
|
||||
> Task :shared-contract:jar
|
||||
> Task :application-core:compileJava
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes
|
||||
> Task :application-core:jar
|
||||
> Task :domain-core:compileJava
|
||||
> Task :domain-core:processResources NO-SOURCE
|
||||
> Task :domain-core:classes
|
||||
> Task :domain-core:jar
|
||||
> Task :messaging:messaging-core-api:compileJava
|
||||
> Task :messaging:messaging-observability:compileJava
|
||||
> Task :messaging:messaging-schema-api:compileJava
|
||||
> Task :messaging:messaging-policy:compileJava
|
||||
> Task :messaging:messaging-reliability-api:compileJava
|
||||
> Task :messaging:messaging-security:compileJava
|
||||
> Task :messaging:messaging-admin-api:compileJava
|
||||
> Task :messaging:messaging-transport-spi:compileJava
|
||||
> Task :messaging:messaging-admin-runtime:compileJava
|
||||
> Task :messaging:messaging-claim-check:compileJava
|
||||
> Task :messaging:messaging-cloudevents:compileJava
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:compileJava
|
||||
> Task :messaging:messaging-kafka:compileJava
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:compileJava
|
||||
> Task :messaging:messaging-rabbit:compileJava
|
||||
> Task :messaging:messaging-runtime-core:compileJava
|
||||
> Task :messaging:messaging-schema-json:compileJava
|
||||
> Task :messaging:messaging-spring-boot-starter:compileJava
|
||||
> Task :adapter:inbound:graphql:compileJava
|
||||
> Task :adapter:inbound:graphql:processResources
|
||||
> Task :adapter:inbound:graphql:classes
|
||||
> Task :adapter:inbound:graphql:jar
|
||||
> Task :adapter:inbound:web:compileJava
|
||||
> Task :adapter:inbound:web:processResources
|
||||
> Task :adapter:inbound:web:classes
|
||||
> Task :adapter:inbound:web:jar
|
||||
> Task :adapter:outbound:support:compileJava
|
||||
> Task :adapter:outbound:support:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:support:classes
|
||||
> Task :adapter:outbound:support:jar
|
||||
> Task :adapter:outbound:cache-redis:compileJava
|
||||
> Task :adapter:outbound:cache-redis:processResources
|
||||
> Task :adapter:outbound:cache-redis:classes
|
||||
> Task :adapter:outbound:cache-redis:jar
|
||||
> Task :adapter:outbound:fileserver:compileJava
|
||||
> Task :adapter:outbound:fileserver:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:fileserver:classes
|
||||
> Task :adapter:outbound:fileserver:jar
|
||||
> Task :adapter:outbound:httpclient:compileJava
|
||||
> Task :adapter:outbound:httpclient:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:httpclient:classes
|
||||
> Task :adapter:outbound:httpclient:jar
|
||||
> Task :adapter:outbound:identifier:compileJava
|
||||
> Task :adapter:outbound:identifier:compileGroovy NO-SOURCE
|
||||
> Task :adapter:outbound:identifier:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:identifier:classes
|
||||
> Task :adapter:outbound:identifier:jar
|
||||
> Task :adapter:outbound:messaging:compileJava
|
||||
> Task :adapter:outbound:messaging:processResources
|
||||
> Task :adapter:outbound:messaging:classes
|
||||
> Task :adapter:outbound:messaging:jar
|
||||
> Task :adapter:outbound:notification:compileJava
|
||||
> Task :adapter:outbound:notification:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:notification:classes
|
||||
> Task :adapter:outbound:notification:jar
|
||||
> Task :adapter:outbound:persistence-jpa:compileJava
|
||||
> Task :adapter:outbound:persistence-jpa:processResources
|
||||
> Task :adapter:outbound:persistence-jpa:classes
|
||||
> Task :adapter:outbound:persistence-jpa:jar
|
||||
> Task :adapter:outbound:persistence-mongo:compileJava
|
||||
> Task :adapter:outbound:persistence-mongo:processResources
|
||||
> Task :adapter:outbound:persistence-mongo:classes
|
||||
> Task :adapter:outbound:persistence-mongo:jar
|
||||
> Task :app-bootstrap:compileJava
|
||||
> Task :app-bootstrap:processResources
|
||||
> Task :app-bootstrap:classes
|
||||
> Task :sample-portfolio:compileJava
|
||||
> Task :sample-portfolio:processResources
|
||||
> Task :sample-portfolio:classes
|
||||
> Task :sample-portfolio:jar
|
||||
> Task :adapter:inbound:web:compileTestkitJava
|
||||
> Task :adapter:inbound:web:processTestkitResources
|
||||
> Task :adapter:inbound:web:testkitClasses
|
||||
> Task :adapter:inbound:web:testkitJar
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses
|
||||
> Task :adapter:outbound:persistence-jpa:testkitJar
|
||||
> Task :app-bootstrap:compileTestJava
|
||||
> Task :app-bootstrap:runtimeClasspathManifest
|
||||
> Task :app-bootstrap:processTestResources
|
||||
> Task :app-bootstrap:testClasses
|
||||
> Task :messaging:messaging-admin-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-admin-api:classes
|
||||
> Task :messaging:messaging-admin-api:jar
|
||||
> Task :messaging:messaging-admin-runtime:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-admin-runtime:classes
|
||||
> Task :messaging:messaging-admin-runtime:jar
|
||||
> Task :messaging:messaging-claim-check:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-claim-check:classes
|
||||
> Task :messaging:messaging-claim-check:jar
|
||||
> Task :messaging:messaging-cloudevents:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-cloudevents:classes
|
||||
> Task :messaging:messaging-cloudevents:jar
|
||||
> Task :messaging:messaging-core-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-core-api:classes
|
||||
> Task :messaging:messaging-core-api:jar
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:processResources
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:classes
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:jar
|
||||
> Task :messaging:messaging-kafka:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-kafka:classes
|
||||
> Task :messaging:messaging-kafka:jar
|
||||
> Task :messaging:messaging-observability:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-observability:classes
|
||||
> Task :messaging:messaging-observability:jar
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:processResources
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:classes
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:jar
|
||||
> Task :messaging:messaging-policy:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-policy:classes
|
||||
> Task :messaging:messaging-policy:jar
|
||||
> Task :messaging:messaging-rabbit:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-rabbit:classes
|
||||
> Task :messaging:messaging-rabbit:jar
|
||||
> Task :messaging:messaging-reliability-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-reliability-api:classes
|
||||
> Task :messaging:messaging-reliability-api:jar
|
||||
> Task :messaging:messaging-runtime-core:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-runtime-core:classes
|
||||
> Task :messaging:messaging-runtime-core:jar
|
||||
> Task :messaging:messaging-schema-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-schema-api:classes
|
||||
> Task :messaging:messaging-schema-api:jar
|
||||
> Task :messaging:messaging-schema-json:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-schema-json:classes
|
||||
> Task :messaging:messaging-schema-json:jar
|
||||
> Task :messaging:messaging-security:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-security:classes
|
||||
> Task :messaging:messaging-security:jar
|
||||
> Task :messaging:messaging-spring-boot-starter:processResources
|
||||
> Task :messaging:messaging-spring-boot-starter:classes
|
||||
> Task :messaging:messaging-spring-boot-starter:jar
|
||||
> Task :messaging:messaging-transport-spi:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-transport-spi:classes
|
||||
> Task :messaging:messaging-transport-spi:jar
|
||||
> Task :adapter:outbound:objectstorage:compileJava
|
||||
> Task :adapter:outbound:objectstorage:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:objectstorage:classes
|
||||
> Task :adapter:outbound:objectstorage:jar
|
||||
> Task :app-bootstrap:test
|
||||
|
||||
BUILD SUCCESSFUL in 1m 32s
|
||||
98 actionable tasks: 98 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
Exit code: 0
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
Command: ./gradlew :app-bootstrap:test --tests dev.caskeleton.bootstrap.architecture.CleanArchitectureTest --rerun-tasks --console=plain
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template/src
|
||||
Executed at: 2026-08-29T07:22:45Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Fresh execution of the project architecture suite containing the outbound-adapter peer rule that permits shared dependencies only into ..adapter.outbound.support..; static ArchUnit analysis does not prove runtime activation.
|
||||
--- stdout/stderr ---
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
> Task :shared-contract:compileJava
|
||||
> Task :shared-contract:processResources
|
||||
> Task :shared-contract:classes
|
||||
> Task :shared-contract:jar
|
||||
> Task :application-core:compileJava
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes
|
||||
> Task :application-core:jar
|
||||
> Task :domain-core:compileJava
|
||||
> Task :domain-core:processResources NO-SOURCE
|
||||
> Task :domain-core:classes
|
||||
> Task :domain-core:jar
|
||||
> Task :messaging:messaging-core-api:compileJava
|
||||
> Task :messaging:messaging-observability:compileJava
|
||||
> Task :messaging:messaging-schema-api:compileJava
|
||||
> Task :messaging:messaging-policy:compileJava
|
||||
> Task :messaging:messaging-reliability-api:compileJava
|
||||
> Task :messaging:messaging-security:compileJava
|
||||
> Task :messaging:messaging-admin-api:compileJava
|
||||
> Task :messaging:messaging-transport-spi:compileJava
|
||||
> Task :messaging:messaging-admin-runtime:compileJava
|
||||
> Task :messaging:messaging-claim-check:compileJava
|
||||
> Task :messaging:messaging-cloudevents:compileJava
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:compileJava
|
||||
> Task :messaging:messaging-kafka:compileJava
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:compileJava
|
||||
> Task :messaging:messaging-rabbit:compileJava
|
||||
> Task :messaging:messaging-runtime-core:compileJava
|
||||
> Task :messaging:messaging-schema-json:compileJava
|
||||
> Task :messaging:messaging-spring-boot-starter:compileJava
|
||||
> Task :adapter:inbound:graphql:compileJava
|
||||
> Task :adapter:inbound:graphql:processResources
|
||||
> Task :adapter:inbound:graphql:classes
|
||||
> Task :adapter:inbound:graphql:jar
|
||||
> Task :adapter:inbound:web:compileJava
|
||||
> Task :adapter:inbound:web:processResources
|
||||
> Task :adapter:inbound:web:classes
|
||||
> Task :adapter:inbound:web:jar
|
||||
> Task :adapter:outbound:support:compileJava
|
||||
> Task :adapter:outbound:support:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:support:classes
|
||||
> Task :adapter:outbound:support:jar
|
||||
> Task :adapter:outbound:cache-redis:compileJava
|
||||
> Task :adapter:outbound:cache-redis:processResources
|
||||
> Task :adapter:outbound:cache-redis:classes
|
||||
> Task :adapter:outbound:cache-redis:jar
|
||||
> Task :adapter:outbound:fileserver:compileJava
|
||||
> Task :adapter:outbound:fileserver:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:fileserver:classes
|
||||
> Task :adapter:outbound:fileserver:jar
|
||||
> Task :adapter:outbound:httpclient:compileJava
|
||||
> Task :adapter:outbound:httpclient:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:httpclient:classes
|
||||
> Task :adapter:outbound:httpclient:jar
|
||||
> Task :adapter:outbound:identifier:compileJava
|
||||
> Task :adapter:outbound:identifier:compileGroovy NO-SOURCE
|
||||
> Task :adapter:outbound:identifier:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:identifier:classes
|
||||
> Task :adapter:outbound:identifier:jar
|
||||
> Task :adapter:outbound:messaging:compileJava
|
||||
> Task :adapter:outbound:messaging:processResources
|
||||
> Task :adapter:outbound:messaging:classes
|
||||
> Task :adapter:outbound:messaging:jar
|
||||
> Task :adapter:outbound:notification:compileJava
|
||||
> Task :adapter:outbound:notification:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:notification:classes
|
||||
> Task :adapter:outbound:notification:jar
|
||||
> Task :adapter:outbound:persistence-jpa:compileJava
|
||||
> Task :adapter:outbound:persistence-jpa:processResources
|
||||
> Task :adapter:outbound:persistence-jpa:classes
|
||||
> Task :adapter:outbound:persistence-jpa:jar
|
||||
> Task :adapter:outbound:persistence-mongo:compileJava
|
||||
> Task :adapter:outbound:persistence-mongo:processResources
|
||||
> Task :adapter:outbound:persistence-mongo:classes
|
||||
> Task :adapter:outbound:persistence-mongo:jar
|
||||
> Task :app-bootstrap:compileJava
|
||||
> Task :app-bootstrap:processResources
|
||||
> Task :app-bootstrap:classes
|
||||
> Task :sample-portfolio:compileJava
|
||||
> Task :sample-portfolio:processResources
|
||||
> Task :sample-portfolio:classes
|
||||
> Task :sample-portfolio:jar
|
||||
> Task :adapter:inbound:web:compileTestkitJava
|
||||
> Task :adapter:inbound:web:processTestkitResources
|
||||
> Task :adapter:inbound:web:testkitClasses
|
||||
> Task :adapter:inbound:web:testkitJar
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses
|
||||
> Task :adapter:outbound:persistence-jpa:testkitJar
|
||||
> Task :app-bootstrap:compileTestJava
|
||||
> Task :app-bootstrap:runtimeClasspathManifest
|
||||
> Task :app-bootstrap:processTestResources
|
||||
> Task :app-bootstrap:testClasses
|
||||
> Task :messaging:messaging-admin-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-admin-api:classes
|
||||
> Task :messaging:messaging-admin-api:jar
|
||||
> Task :messaging:messaging-admin-runtime:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-admin-runtime:classes
|
||||
> Task :messaging:messaging-admin-runtime:jar
|
||||
> Task :messaging:messaging-claim-check:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-claim-check:classes
|
||||
> Task :messaging:messaging-claim-check:jar
|
||||
> Task :messaging:messaging-cloudevents:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-cloudevents:classes
|
||||
> Task :messaging:messaging-cloudevents:jar
|
||||
> Task :messaging:messaging-core-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-core-api:classes
|
||||
> Task :messaging:messaging-core-api:jar
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:processResources
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:classes
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:jar
|
||||
> Task :messaging:messaging-kafka:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-kafka:classes
|
||||
> Task :messaging:messaging-kafka:jar
|
||||
> Task :messaging:messaging-observability:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-observability:classes
|
||||
> Task :messaging:messaging-observability:jar
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:processResources
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:classes
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:jar
|
||||
> Task :messaging:messaging-policy:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-policy:classes
|
||||
> Task :messaging:messaging-policy:jar
|
||||
> Task :messaging:messaging-rabbit:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-rabbit:classes
|
||||
> Task :messaging:messaging-rabbit:jar
|
||||
> Task :messaging:messaging-reliability-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-reliability-api:classes
|
||||
> Task :messaging:messaging-reliability-api:jar
|
||||
> Task :messaging:messaging-runtime-core:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-runtime-core:classes
|
||||
> Task :messaging:messaging-runtime-core:jar
|
||||
> Task :messaging:messaging-schema-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-schema-api:classes
|
||||
> Task :messaging:messaging-schema-api:jar
|
||||
> Task :messaging:messaging-schema-json:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-schema-json:classes
|
||||
> Task :messaging:messaging-schema-json:jar
|
||||
> Task :messaging:messaging-security:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-security:classes
|
||||
> Task :messaging:messaging-security:jar
|
||||
> Task :messaging:messaging-spring-boot-starter:processResources
|
||||
> Task :messaging:messaging-spring-boot-starter:classes
|
||||
> Task :messaging:messaging-spring-boot-starter:jar
|
||||
> Task :messaging:messaging-transport-spi:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-transport-spi:classes
|
||||
> Task :messaging:messaging-transport-spi:jar
|
||||
> Task :adapter:outbound:objectstorage:compileJava
|
||||
> Task :adapter:outbound:objectstorage:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:objectstorage:classes
|
||||
> Task :adapter:outbound:objectstorage:jar
|
||||
> Task :app-bootstrap:test
|
||||
|
||||
BUILD SUCCESSFUL in 1m 45s
|
||||
98 actionable tasks: 98 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
Exit code: 0
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
Command: test -d /home/donghyeon/workspace/ai-tool/llm-wiki-private && find /home/donghyeon/workspace/ai-tool/llm-wiki-private -maxdepth 1 -type f -print
|
||||
Working directory: /
|
||||
Executed at: 2026-08-29T07:27:30Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Checks only whether the repository-mandated local LLM Wiki vault exists in this remote host at the exact configured path; it does not search for alternate vault locations.
|
||||
--- stdout/stderr ---
|
||||
|
||||
Exit code: 1
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
Command: python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import subprocess, collections
|
||||
root='src/adapter/outbound/persistence-jpa'
|
||||
files=subprocess.check_output(['git','ls-files',root],text=True).splitlines()
|
||||
print('tracked_total=',len(files))
|
||||
for sr in sorted((Path(root)/'src').iterdir()):
|
||||
if sr.is_dir():
|
||||
owned=[p for p in sr.rglob('*') if p.is_file() and 'build/' not in str(p)]
|
||||
java=[p for p in owned if p.suffix=='.java']
|
||||
print(sr.name,'files=',len(owned),'java=',len(java),'nonjava=',len(owned)-len(java),'java_loc=',sum(len(p.read_text(errors='ignore').splitlines()) for p in java))
|
||||
print('top_level_files=')
|
||||
for p in sorted(Path(root).iterdir()):
|
||||
if p.is_file(): print(p)
|
||||
PY
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T07:48:05Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Enumerates the tracked persistence-jpa leaf and its source-root/package counts at the recorded revision; the sub-scope labels are deterministic path buckets and do not themselves assert behavioral findings.
|
||||
--- stdout/stderr ---
|
||||
tracked_total= 605
|
||||
jpaPlatformPerformanceTest files= 3 java= 3 nonjava= 0 java_loc= 268
|
||||
main files= 381 java= 350 nonjava= 31 java_loc= 27744
|
||||
postgresqlIntegrationTest files= 75 java= 71 nonjava= 4 java_loc= 13958
|
||||
test files= 101 java= 100 nonjava= 1 java_loc= 9511
|
||||
testkit files= 41 java= 41 nonjava= 0 java_loc= 3070
|
||||
top_level_files=
|
||||
src/adapter/outbound/persistence-jpa/CLAUDE.md
|
||||
src/adapter/outbound/persistence-jpa/README.md
|
||||
src/adapter/outbound/persistence-jpa/build.gradle
|
||||
src/adapter/outbound/persistence-jpa/gradle.lockfile
|
||||
|
||||
Exit code: 0
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
Command: python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import re, subprocess
|
||||
api=Path('src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api')
|
||||
alljava=[Path(p) for p in subprocess.check_output(['git','ls-files'],text=True).splitlines() if '/src/main/java/' in p and p.endswith('.java')]
|
||||
zeros=[]
|
||||
for src in sorted(api.rglob('*.java')):
|
||||
t=src.read_text()
|
||||
m=re.search(r'public\s+(?:final\s+)?(?:record|class|interface|enum)\s+(\w+)',t)
|
||||
if not m: continue
|
||||
name=m.group(1); pkg=re.search(r'package\s+([^;]+);',t).group(1); fqn=pkg+'.'+name
|
||||
refs=[]
|
||||
for p in alljava:
|
||||
if p==src: continue
|
||||
txt=p.read_text(errors='ignore')
|
||||
pp=re.search(r'package\s+([^;]+);',txt)
|
||||
same=bool(pp and pp.group(1)==pkg and re.search(r'\b'+re.escape(name)+r'\b',txt))
|
||||
imported=(f'import {fqn};' in txt) or (fqn in txt) or (f'import {pkg}.*;' in txt and bool(re.search(r'\b'+re.escape(name)+r'\b',txt)))
|
||||
if same or imported: refs.append(str(p))
|
||||
print(f'{fqn}|production_reference_files={len(refs)}')
|
||||
for r in refs: print(' '+r)
|
||||
if not refs: zeros.append(fqn)
|
||||
print('ZERO_REFERENCE_TYPES=',len(zeros))
|
||||
for x in zeros: print('ZERO '+x)
|
||||
PY
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T07:48:05Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Conservatively searches current tracked production Java for references to each public top-level persistence API type, using exact imports/FQNs plus same-package simple-name references. Javadoc/simple-name references in the same package can over-count; reflection, generated code, and external adopters are outside this repository search.
|
||||
--- stdout/stderr ---
|
||||
dev.caskeleton.adapter.outbound.persistence.api.PersistenceOperationName|production_reference_files=21
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/capability/JpaCapability.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/JpaFailureContext.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/VendorFailureTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/JpaTransactionExecutor.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/RetryEventListener.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/FlywayValidationGate.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaRetryObservation.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaTransactionObservation.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/json/JsonDocumentCodec.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/LockWaitObservation.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlLockExceptionTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecord.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/OptimisticConflictTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/PersistenceFailureTranslatorChain.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceContext.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceFrame.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/UnknownOperation.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.capability.CapabilitySupport|production_reference_files=2
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.capability.JpaCapability|production_reference_files=2
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/capability/CapabilitySupport.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.capability.SupportLevel|production_reference_files=3
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/capability/CapabilitySupport.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/capability/JpaCapability.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/experimental/next/CompatibilityLane.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.CheckConstraintViolationException|production_reference_files=1
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.ConnectionUnavailableException|production_reference_files=1
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.ConstraintCode|production_reference_files=4
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ConstraintViolationDetails.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintCatalog.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/ConstraintCatalog.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.ConstraintViolationDetails|production_reference_files=8
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/CheckConstraintViolationException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ConstraintCode.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ForeignKeyViolationException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/NotNullConstraintViolationException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/UniqueConstraintViolationException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintCatalog.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.DataCorruptionException|production_reference_files=2
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/json/JsonDocumentCodec.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.DeadlockDetectedException|production_reference_files=3
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/PessimisticLockTimeoutException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlLockExceptionTranslator.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.FailureCategory|production_reference_files=22
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/CheckConstraintViolationException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ConnectionUnavailableException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/DataCorruptionException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/DeadlockDetectedException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ForeignKeyViolationException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/JpaEntityNotFoundException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/JpaPersistenceException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/NotNullConstraintViolationException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/OptimisticConflictException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/PessimisticLockTimeoutException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/QueryTimeoutException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/SchemaMismatchException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/SerializationFailureException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/TransactionCompletionUnknownException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/TransactionTimeoutException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/UniqueConstraintViolationException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/RetryProfile.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/MicrometerQueryObservation.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlFailureClassifier.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicy.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.ForeignKeyViolationException|production_reference_files=1
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.JpaEntityNotFoundException|production_reference_files=0
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.JpaFailureContext|production_reference_files=24
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/CheckConstraintViolationException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ConnectionUnavailableException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ConstraintViolationDetails.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/DataCorruptionException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/DeadlockDetectedException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ForeignKeyViolationException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/JpaEntityNotFoundException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/JpaPersistenceException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/NotNullConstraintViolationException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/OptimisticConflictException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/PessimisticLockTimeoutException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/QueryTimeoutException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/SchemaMismatchException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/SerializationFailureException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/TransactionCompletionUnknownException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/TransactionTimeoutException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/UniqueConstraintViolationException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/FlywayValidationGate.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/json/JsonDocumentCodec.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlLockExceptionTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/OptimisticConflictTranslator.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.JpaPersistenceException|production_reference_files=27
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/CheckConstraintViolationException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ConnectionUnavailableException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/DataCorruptionException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/DeadlockDetectedException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ForeignKeyViolationException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/JpaEntityNotFoundException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/NotNullConstraintViolationException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/OptimisticConflictException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/PessimisticLockTimeoutException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/QueryTimeoutException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/SchemaMismatchException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/SerializationFailureException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/TransactionCompletionUnknownException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/TransactionTimeoutException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/UniqueConstraintViolationException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/VendorFailureTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/JpaRetryPolicy.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/RetryEventListener.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaRetryObservation.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaTransactionObservation.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/MicrometerQueryObservation.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlLockExceptionTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicy.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/PersistenceFailureTranslatorChain.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.NotNullConstraintViolationException|production_reference_files=1
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.OptimisticConflictException|production_reference_files=3
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/OptimisticConflictTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/PersistenceFailureTranslatorChain.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.PessimisticLockTimeoutException|production_reference_files=2
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlLockExceptionTranslator.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.QueryTimeoutException|production_reference_files=1
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.SchemaMismatchException|production_reference_files=2
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/FlywayValidationGate.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.SerializationFailureException|production_reference_files=1
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.SqlExceptionSqlStateResolver|production_reference_files=3
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlLockExceptionTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.SqlStateResolver|production_reference_files=4
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/SqlExceptionSqlStateResolver.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlLockExceptionTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.TransactionCompletionUnknownException|production_reference_files=6
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ConnectionUnavailableException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/TransactionCompletionEvidence.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecord.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecorder.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicy.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.TransactionTimeoutException|production_reference_files=1
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.UniqueConstraintViolationException|production_reference_files=1
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.VendorFailureTranslator|production_reference_files=5
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlPersistenceConfig.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/PersistenceFailureTranslatorChain.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.CursorCodec|production_reference_files=1
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/SignedJsonCursorCodec.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.CursorPayloadCodec|production_reference_files=1
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/SignedJsonCursorCodec.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.KeysetPageRequest|production_reference_files=1
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaKeysetQuerySupport.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.KeysetSlice|production_reference_files=2
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaKeysetQuerySupport.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/KeysetSliceAssembler.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.NoopQueryObservation|production_reference_files=1
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaObservabilityAutoConfiguration.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.QueryName|production_reference_files=11
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/NoopQueryObservation.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/QueryObservation.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/NamedStatementInspector.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/QueryNameContext.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/bulk/HibernateBulkDmlExecutor.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/MicrometerQueryObservation.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/querydsl/QuerydslJpaSupport.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaKeysetQuerySupport.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaRepositoryFragmentSupport.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaStreamExecutor.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/RegisteredQuery.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.QueryObservation|production_reference_files=5
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/NoopQueryObservation.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/bulk/HibernateBulkDmlExecutor.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/MicrometerQueryObservation.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaStreamExecutor.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaObservabilityAutoConfiguration.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.QueryScope|production_reference_files=5
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/NoopQueryObservation.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/QueryObservation.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/bulk/HibernateBulkDmlExecutor.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/MicrometerQueryObservation.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaStreamExecutor.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.SignedJsonCursorCodec|production_reference_files=1
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/CursorPayloadCodec.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.SortDirection|production_reference_files=2
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/KeysetPageRequest.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/KeysetTerm.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.IsolationLevel|production_reference_files=2
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/TransactionProfile.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDefinitionMapper.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.JitterMode|production_reference_files=2
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/RetryProfile.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/BackoffCalculator.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.JpaRetryPolicy|production_reference_files=4
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/RetryProfile.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicy.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.JpaTransactionExecutor|production_reference_files=1
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.PropagationMode|production_reference_files=2
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/TransactionProfile.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDefinitionMapper.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryDecision|production_reference_files=5
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/JpaRetryPolicy.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/RetryEventListener.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaRetryObservation.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicy.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryDisposition|production_reference_files=2
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/RetryDecision.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryEventListener|production_reference_files=5
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaRetryObservation.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaObservabilityAutoConfiguration.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryProfile|production_reference_files=6
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/TransactionProfile.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/BackoffCalculator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicy.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/RetryBudget.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionAttempt|production_reference_files=5
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/JpaRetryPolicy.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/RetryEventListener.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaRetryObservation.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicy.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionCompletionEvidence|production_reference_files=6
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/TransactionCompletionUnknownException.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecord.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceContext.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceFrame.java
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionProfile|production_reference_files=5
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/JpaTransactionExecutor.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDefinitionMapper.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistry.java
|
||||
ZERO_REFERENCE_TYPES= 1
|
||||
ZERO dev.caskeleton.adapter.outbound.persistence.api.error.JpaEntityNotFoundException
|
||||
|
||||
Exit code: 0
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
Command: printf '%s\n' '--- framework annotations under persistence api ---'; git grep -n -E '@(Component|Configuration|Bean|Conditional|AutoConfiguration|Repository|Entity|Service)' -- 'src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/**/*.java' 'src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/*.java' || true; printf '%s\n' '--- api baseline header/count ---'; sed -n '1,8p' docs/architecture/jpa-api-surface.txt; grep -c 'dev.caskeleton.adapter.outbound.persistence.api' docs/architecture/jpa-api-surface.txt; printf '%s\n' '--- capability enum/composition counts ---'; python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import re
|
||||
p=Path('src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/capability/JpaCapability.java').read_text()
|
||||
inside=p.split('public enum JpaCapability {',1)[1].split('private final String id;',1)[0]
|
||||
print('enum=',len(re.findall(r'^\s*[A-Z][A-Z0-9_]*\("',inside,re.M)))
|
||||
q=Path('src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java').read_text()
|
||||
print('composition=',q.count('CapabilitySupport.'))
|
||||
PY
|
||||
printf '%s\n' '--- capability report/endpoint path ---'; git grep -n -E 'CapabilitySupport|JpaPlatformReport|JpaPlatformEndpoint|@Endpoint\(id = "jpaplatform"\)' -- 'src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/*.java'; printf '%s\n' '--- retry reason accessor uses in persistence/app-bootstrap ---'; git grep -n '\.reason()' -- 'src/adapter/outbound/persistence-jpa/**' 'src/app-bootstrap/**' || true; printf '%s\n' '--- same simple-name mechanisms elsewhere ---'; git grep -n -E 'class RetryDecision|record RetryDecision|interface CursorCodec|enum SortDirection|enum FailureCategory' -- 'src/adapter/outbound/**/src/main/java/**/*.java' | head -220
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T07:48:10Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Collects the API package's framework-annotation absence, current public API baseline/count, capability composition count, same-name mechanisms in other bounded contexts, actuator report wiring, and exact retry-reason uses. Same-name classes in other adapters are not asserted to be semantic duplicates by this evidence.
|
||||
--- stdout/stderr ---
|
||||
--- framework annotations under persistence api ---
|
||||
--- api baseline header/count ---
|
||||
# JPA persistence leaf public API surface — every public top-level type in src/main/java.
|
||||
# A public type in a single-jar leaf is reachable from every adopter's code, so
|
||||
# additions are reviewed rather than discovered. `api` is the intended external
|
||||
# surface; the rest is implementation that has not been moved under an internal
|
||||
# root yet.
|
||||
# Update only after review with:
|
||||
# ./gradlew :adapter:outbound:persistence-jpa:updateJpaApiSurface -PapproveJpaApiSurfaceChange
|
||||
# types: 338
|
||||
49
|
||||
--- capability enum/composition counts ---
|
||||
enum= 16
|
||||
composition= 16
|
||||
--- capability report/endpoint path ---
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:3:import dev.caskeleton.adapter.outbound.persistence.api.capability.CapabilitySupport;
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:54: public List<CapabilitySupport> capabilities() {
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:56: CapabilitySupport.stable(JpaCapability.TRANSACTION_RETRY),
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:57: CapabilitySupport.stable(JpaCapability.COMPLETION_EVIDENCE),
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:58: CapabilitySupport.stable(JpaCapability.KEYSET_PAGINATION),
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:59: CapabilitySupport.stable(JpaCapability.BATCH),
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:60: CapabilitySupport.stable(JpaCapability.SCHEMA_GATE),
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:61: CapabilitySupport.stable(JpaCapability.RUNTIME_ROLE_VERIFICATION),
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:62: CapabilitySupport.stable(JpaCapability.OBSERVABILITY),
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:63: CapabilitySupport.advanced(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:65: CapabilitySupport.advanced(JpaCapability.POSTGRESQL_WORK_CLAIM, "requires a named queue"),
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:66: CapabilitySupport.advanced(JpaCapability.POSTGRESQL_JSONB, "requires a registered path"),
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:67: CapabilitySupport.advanced(JpaCapability.POSTGRESQL_ARRAY_RANGE, "requires a JDBC type"),
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:68: CapabilitySupport.advanced(JpaCapability.BULK_DML, "requires a registered operation"),
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:69: CapabilitySupport.advanced(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:71: CapabilitySupport.advanced(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:73: CapabilitySupport.advanced(JpaCapability.L2_CACHE, "requires an enrolled region catalog"),
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:74: CapabilitySupport.advanced(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:84: public JpaPlatformReport report(DataSource dataSource, String schemaVersion) {
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:100: return JpaPlatformReport.sanitized(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformEndpoint.java:20:@Endpoint(id = "jpaplatform")
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformEndpoint.java:21:public class JpaPlatformEndpoint {
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformEndpoint.java:23: private final Supplier<JpaPlatformReport> reportSupplier;
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformEndpoint.java:25: public JpaPlatformEndpoint(Supplier<JpaPlatformReport> reportSupplier) {
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformEndpoint.java:31: public JpaPlatformReport platform() {
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:3:import dev.caskeleton.adapter.outbound.persistence.api.capability.CapabilitySupport;
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:19:public record JpaPlatformReport(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:26: List<CapabilitySupport> capabilities) {
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:28: public JpaPlatformReport {
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:42: public static JpaPlatformReport sanitized(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:49: List<CapabilitySupport> capabilities) {
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:50: return new JpaPlatformReport(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:122: public Supplier<JpaPlatformReport> jpaPlatformReportSupplier(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:138: public JpaPlatformEndpoint jpaPlatformEndpoint(Supplier<JpaPlatformReport> reportSupplier) {
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:139: return new JpaPlatformEndpoint(reportSupplier);
|
||||
--- retry reason accessor uses in persistence/app-bootstrap ---
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaSuppressionStore.java:51: entry.tenantId().value(), entry.normalizedTargetHmac(), entry.reason().name())
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaSuppressionStore.java:55: entry.reason().name(),
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaSuppressionStore.java:83: entry.reason().name(),
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaSuppressionStore.java:97: SuppressionReason.valueOf(entity.reason()),
|
||||
src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/platform/experimental/ReadAfterWriteRoutingContractTest.java:93: assertThat(decision.reason()).isNotBlank();
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicyTest.java:86: assertThat(decision.reason()).contains("irreversible");
|
||||
--- same simple-name mechanisms elsewhere ---
|
||||
src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/operations/SortDirection.java:9:public enum SortDirection {
|
||||
src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/api/operation/FailureCategory.java:9:public enum FailureCategory {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/FailureCategory.java:11:public enum FailureCategory {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/CursorCodec.java:10:public interface CursorCodec<C> {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/SortDirection.java:9:public enum SortDirection {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/RetryDecision.java:12:public record RetryDecision(RetryDisposition disposition, Duration delay, String reason) {
|
||||
|
||||
Exit code: 0
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
Command: printf '%s\n' '--- baseline API entries ---'; grep 'dev.caskeleton.adapter.outbound.persistence.api' docs/architecture/jpa-api-surface.txt; printf '%s\n' '--- current API declarations ---'; python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import re
|
||||
root=Path('src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api')
|
||||
for p in sorted(root.rglob('*.java')):
|
||||
t=p.read_text(); m=re.search(r'public\s+(?:final\s+)?(?:record|class|interface|enum)\s+(\w+)',t)
|
||||
if m:
|
||||
pkg=re.search(r'package\s+([^;]+);',t).group(1); print(pkg+'.'+m.group(1))
|
||||
PY
|
||||
printf '%s\n' '--- intended external statement ---'; sed -n '1,8p' docs/architecture/jpa-api-surface.txt
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T07:48:10Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Compares the intended external api surface baseline with current public top-level api declarations and records named API/design references; it does not evaluate behavioral correctness of those types.
|
||||
--- stdout/stderr ---
|
||||
--- baseline API entries ---
|
||||
dev.caskeleton.adapter.outbound.persistence.api.PersistenceOperationName
|
||||
dev.caskeleton.adapter.outbound.persistence.api.capability.CapabilitySupport
|
||||
dev.caskeleton.adapter.outbound.persistence.api.capability.JpaCapability
|
||||
dev.caskeleton.adapter.outbound.persistence.api.capability.SupportLevel
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.CheckConstraintViolationException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.ConnectionUnavailableException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.ConstraintCode
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.ConstraintViolationDetails
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.DataCorruptionException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.DeadlockDetectedException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.FailureCategory
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.ForeignKeyViolationException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.JpaEntityNotFoundException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.JpaFailureContext
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.JpaPersistenceException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.NotNullConstraintViolationException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.OptimisticConflictException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.PessimisticLockTimeoutException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.QueryTimeoutException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.SchemaMismatchException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.SerializationFailureException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.SqlExceptionSqlStateResolver
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.SqlStateResolver
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.TransactionCompletionUnknownException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.TransactionTimeoutException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.UniqueConstraintViolationException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.VendorFailureTranslator
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.CursorCodec
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.CursorPayloadCodec
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.KeysetPageRequest
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.KeysetSlice
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.NoopQueryObservation
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.QueryName
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.QueryObservation
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.QueryScope
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.SignedJsonCursorCodec
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.SortDirection
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.IsolationLevel
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.JitterMode
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.JpaRetryPolicy
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.JpaTransactionExecutor
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.PropagationMode
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryDecision
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryDisposition
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryEventListener
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryProfile
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionAttempt
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionCompletionEvidence
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionProfile
|
||||
--- current API declarations ---
|
||||
dev.caskeleton.adapter.outbound.persistence.api.PersistenceOperationName
|
||||
dev.caskeleton.adapter.outbound.persistence.api.capability.CapabilitySupport
|
||||
dev.caskeleton.adapter.outbound.persistence.api.capability.JpaCapability
|
||||
dev.caskeleton.adapter.outbound.persistence.api.capability.SupportLevel
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.CheckConstraintViolationException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.ConnectionUnavailableException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.ConstraintCode
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.ConstraintViolationDetails
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.DataCorruptionException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.DeadlockDetectedException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.FailureCategory
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.ForeignKeyViolationException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.JpaEntityNotFoundException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.JpaFailureContext
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.JpaPersistenceException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.NotNullConstraintViolationException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.OptimisticConflictException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.PessimisticLockTimeoutException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.QueryTimeoutException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.SchemaMismatchException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.SerializationFailureException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.SqlExceptionSqlStateResolver
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.SqlStateResolver
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.TransactionCompletionUnknownException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.TransactionTimeoutException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.UniqueConstraintViolationException
|
||||
dev.caskeleton.adapter.outbound.persistence.api.error.VendorFailureTranslator
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.CursorCodec
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.CursorPayloadCodec
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.KeysetPageRequest
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.KeysetSlice
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.NoopQueryObservation
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.QueryName
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.QueryObservation
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.QueryScope
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.SignedJsonCursorCodec
|
||||
dev.caskeleton.adapter.outbound.persistence.api.query.SortDirection
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.IsolationLevel
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.JitterMode
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.JpaRetryPolicy
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.JpaTransactionExecutor
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.PropagationMode
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryDecision
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryDisposition
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryEventListener
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryProfile
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionAttempt
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionCompletionEvidence
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionProfile
|
||||
--- intended external statement ---
|
||||
# JPA persistence leaf public API surface — every public top-level type in src/main/java.
|
||||
# A public type in a single-jar leaf is reachable from every adopter's code, so
|
||||
# additions are reviewed rather than discovered. `api` is the intended external
|
||||
# surface; the rest is implementation that has not been moved under an internal
|
||||
# root yet.
|
||||
# Update only after review with:
|
||||
# ./gradlew :adapter:outbound:persistence-jpa:updateJpaApiSurface -PapproveJpaApiSurfaceChange
|
||||
# types: 338
|
||||
|
||||
Exit code: 0
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
Command: java --class-path src/adapter/outbound/persistence-jpa/build/classes/java/main /shared/document-detail/clean-architecture-backend-template/evidence/raw/035a-jpa-cursor-boundary-probe.java
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T07:48:27Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Runs the current compiled SignedJsonCursorCodec across payload sizes immediately below/at/above its declared 2048-byte payload bound. It isolates encode/decode self-round-trip behavior and does not exercise HTTP transport or an application-specific JSON mapper.
|
||||
--- stdout/stderr ---
|
||||
size=2045 tokenChars=2774 encode=OK decode=OK decodedBytes=2045
|
||||
size=2046 tokenChars=2775 encode=OK decode=IllegalArgumentException:cursor payload exceeds the maximum size
|
||||
size=2047 tokenChars=2777 encode=OK decode=IllegalArgumentException:cursor payload exceeds the maximum size
|
||||
size=2048 tokenChars=2778 encode=OK decode=IllegalArgumentException:cursor payload exceeds the maximum size
|
||||
size=2049 encode=IllegalStateException:cursor payload is 2049 bytes; the bound is 2048
|
||||
|
||||
Exit code: 0
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import dev.caskeleton.adapter.outbound.persistence.api.query.CursorPayloadCodec;
|
||||
import dev.caskeleton.adapter.outbound.persistence.api.query.SignedJsonCursorCodec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class JpaCursorBoundaryProbe {
|
||||
static SignedJsonCursorCodec<String> codecFor(int payloadBytes) {
|
||||
return new SignedJsonCursorCodec<>(new CursorPayloadCodec<>() {
|
||||
public String toJson(String cursor) { return "x".repeat(payloadBytes); }
|
||||
public String fromJson(String json) { return json; }
|
||||
}, "k".repeat(32).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
for (int size : new int[]{2045, 2046, 2047, 2048, 2049}) {
|
||||
var codec = codecFor(size);
|
||||
try {
|
||||
String token = codec.encode("ignored");
|
||||
try {
|
||||
String decoded = codec.decode(token);
|
||||
System.out.printf("size=%d tokenChars=%d encode=OK decode=OK decodedBytes=%d%n",
|
||||
size, token.length(), decoded.getBytes(StandardCharsets.UTF_8).length);
|
||||
} catch (RuntimeException ex) {
|
||||
System.out.printf("size=%d tokenChars=%d encode=OK decode=%s:%s%n",
|
||||
size, token.length(), ex.getClass().getSimpleName(), ex.getMessage());
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
System.out.printf("size=%d encode=%s:%s%n", size, ex.getClass().getSimpleName(), ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
Command: java --class-path src/adapter/outbound/persistence-jpa/build/classes/java/main /shared/document-detail/clean-architecture-backend-template/evidence/raw/036a-jpa-api-bound-probe.java
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T07:48:27Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Constructs current API value types with deliberately large diagnostic strings and a very large attempt count to record constructor-enforced bounds only. It does not assert that current app-bootstrap composition supplies such values.
|
||||
--- stdout/stderr ---
|
||||
constraintLength=100000
|
||||
retryDecisionReasonLength=100000
|
||||
retryProfileMaxAttempts=2147483647
|
||||
|
||||
Exit code: 0
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import dev.caskeleton.adapter.outbound.persistence.api.capability.*;
|
||||
import dev.caskeleton.adapter.outbound.persistence.api.transaction.*;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
public class JpaApiBoundProbe {
|
||||
public static void main(String[] args) {
|
||||
String large = "x".repeat(100_000);
|
||||
CapabilitySupport support = new CapabilitySupport(JpaCapability.OBSERVABILITY, SupportLevel.ADVANCED, List.of(large));
|
||||
RetryDecision decision = RetryDecision.fail(large);
|
||||
RetryProfile profile = new RetryProfile("retry.profile", Integer.MAX_VALUE, Duration.ZERO, Duration.ZERO, 1.0d, JitterMode.NONE, java.util.Set.of());
|
||||
System.out.println("constraintLength=" + support.constraints().get(0).length());
|
||||
System.out.println("retryDecisionReasonLength=" + decision.reason().length());
|
||||
System.out.println("retryProfileMaxAttempts=" + profile.maxAttempts());
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
Command: ./gradlew :adapter:outbound:persistence-jpa:test --tests dev.caskeleton.adapter.outbound.persistence.api.PersistenceOperationNameTest --tests dev.caskeleton.adapter.outbound.persistence.api.error.JpaFailureContextTest --tests dev.caskeleton.adapter.outbound.persistence.api.error.JpaPersistenceExceptionTest --tests dev.caskeleton.adapter.outbound.persistence.api.query.QueryNameTest --tests dev.caskeleton.adapter.outbound.persistence.api.query.SignedJsonCursorCodecTest --tests dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionProfileTest --rerun-tasks --console=plain
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template/src
|
||||
Executed at: 2026-08-29T07:48:59Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Fresh execution of the six dedicated persistence api test classes. These tests cover their declared fixtures but do not add the external boundary probe used separately for 2046-2048 byte cursor payloads.
|
||||
--- stdout/stderr ---
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
> Task :shared-contract:compileJava
|
||||
> Task :shared-contract:processResources
|
||||
> Task :shared-contract:classes
|
||||
> Task :shared-contract:jar
|
||||
> Task :application-core:compileJava
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes
|
||||
> Task :application-core:jar
|
||||
> Task :adapter:outbound:persistence-jpa:compileJava
|
||||
> Task :adapter:outbound:persistence-jpa:processResources
|
||||
> Task :adapter:outbound:persistence-jpa:classes
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testClasses
|
||||
> Task :adapter:outbound:persistence-jpa:test
|
||||
|
||||
BUILD SUCCESSFUL in 23s
|
||||
18 actionable tasks: 18 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
Exit code: 0
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
Command: ./gradlew :adapter:outbound:persistence-jpa:verifyJpaApiSurface --rerun-tasks --console=plain
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template/src
|
||||
Executed at: 2026-08-29T07:49:23Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Fresh Gradle verification that the current leaf public top-level API matches the committed jpa-api-surface baseline. It verifies names/surface drift, not behavioral correctness or reachability.
|
||||
--- stdout/stderr ---
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
|
||||
> Task :adapter:outbound:persistence-jpa:verifyJpaApiSurface
|
||||
verifyJpaApiSurface: OK — the committed public API surface is unchanged.
|
||||
|
||||
BUILD SUCCESSFUL in 3s
|
||||
9 actionable tasks: 9 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
Exit code: 0
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
Command: ./gradlew :app-bootstrap:test --tests dev.caskeleton.bootstrap.autoconfigure.jpa.JpaPlatformAutoConfigurationTest --rerun-tasks --console=plain
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template/src
|
||||
Executed at: 2026-08-29T07:49:26Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Fresh app-bootstrap test for current JPA platform composition/capability declarations. It exercises the repository's static capability list, not arbitrary third-party CapabilitySupport values.
|
||||
--- stdout/stderr ---
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
> Task :shared-contract:compileJava
|
||||
> Task :shared-contract:processResources
|
||||
> Task :shared-contract:classes
|
||||
> Task :shared-contract:jar
|
||||
> Task :application-core:compileJava
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes
|
||||
> Task :application-core:jar
|
||||
> Task :domain-core:compileJava
|
||||
> Task :domain-core:processResources NO-SOURCE
|
||||
> Task :domain-core:classes
|
||||
> Task :domain-core:jar
|
||||
> Task :messaging:messaging-core-api:compileJava
|
||||
> Task :messaging:messaging-observability:compileJava
|
||||
> Task :messaging:messaging-schema-api:compileJava
|
||||
> Task :messaging:messaging-policy:compileJava
|
||||
> Task :messaging:messaging-reliability-api:compileJava
|
||||
> Task :messaging:messaging-security:compileJava
|
||||
> Task :messaging:messaging-admin-api:compileJava
|
||||
> Task :messaging:messaging-transport-spi:compileJava
|
||||
> Task :messaging:messaging-admin-runtime:compileJava
|
||||
> Task :messaging:messaging-claim-check:compileJava
|
||||
> Task :messaging:messaging-cloudevents:compileJava
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:compileJava
|
||||
> Task :messaging:messaging-kafka:compileJava
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:compileJava
|
||||
> Task :messaging:messaging-rabbit:compileJava
|
||||
> Task :messaging:messaging-runtime-core:compileJava
|
||||
> Task :messaging:messaging-schema-json:compileJava
|
||||
> Task :messaging:messaging-spring-boot-starter:compileJava
|
||||
> Task :adapter:inbound:graphql:compileJava
|
||||
> Task :adapter:inbound:graphql:processResources
|
||||
> Task :adapter:inbound:graphql:classes
|
||||
> Task :adapter:inbound:graphql:jar
|
||||
> Task :adapter:inbound:web:compileJava
|
||||
> Task :adapter:inbound:web:processResources
|
||||
> Task :adapter:inbound:web:classes
|
||||
> Task :adapter:inbound:web:jar
|
||||
> Task :adapter:outbound:support:compileJava
|
||||
> Task :adapter:outbound:support:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:support:classes
|
||||
> Task :adapter:outbound:support:jar
|
||||
> Task :adapter:outbound:cache-redis:compileJava
|
||||
> Task :adapter:outbound:cache-redis:processResources
|
||||
> Task :adapter:outbound:cache-redis:classes
|
||||
> Task :adapter:outbound:cache-redis:jar
|
||||
> Task :adapter:outbound:fileserver:compileJava
|
||||
> Task :adapter:outbound:fileserver:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:fileserver:classes
|
||||
> Task :adapter:outbound:fileserver:jar
|
||||
> Task :adapter:outbound:httpclient:compileJava
|
||||
> Task :adapter:outbound:httpclient:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:httpclient:classes
|
||||
> Task :adapter:outbound:httpclient:jar
|
||||
> Task :adapter:outbound:identifier:compileJava
|
||||
> Task :adapter:outbound:identifier:compileGroovy NO-SOURCE
|
||||
> Task :adapter:outbound:identifier:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:identifier:classes
|
||||
> Task :adapter:outbound:identifier:jar
|
||||
> Task :adapter:outbound:messaging:compileJava
|
||||
> Task :adapter:outbound:messaging:processResources
|
||||
> Task :adapter:outbound:messaging:classes
|
||||
> Task :adapter:outbound:messaging:jar
|
||||
> Task :adapter:outbound:notification:compileJava
|
||||
> Task :adapter:outbound:notification:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:notification:classes
|
||||
> Task :adapter:outbound:notification:jar
|
||||
> Task :adapter:outbound:persistence-jpa:compileJava
|
||||
> Task :adapter:outbound:persistence-jpa:processResources
|
||||
> Task :adapter:outbound:persistence-jpa:classes
|
||||
> Task :adapter:outbound:persistence-jpa:jar
|
||||
> Task :adapter:outbound:persistence-mongo:compileJava
|
||||
> Task :adapter:outbound:persistence-mongo:processResources
|
||||
> Task :adapter:outbound:persistence-mongo:classes
|
||||
> Task :adapter:outbound:persistence-mongo:jar
|
||||
> Task :app-bootstrap:compileJava
|
||||
> Task :app-bootstrap:processResources
|
||||
> Task :app-bootstrap:classes
|
||||
> Task :sample-portfolio:compileJava
|
||||
> Task :sample-portfolio:processResources
|
||||
> Task :sample-portfolio:classes
|
||||
> Task :sample-portfolio:jar
|
||||
> Task :adapter:inbound:web:compileTestkitJava
|
||||
> Task :adapter:inbound:web:processTestkitResources
|
||||
> Task :adapter:inbound:web:testkitClasses
|
||||
> Task :adapter:inbound:web:testkitJar
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses
|
||||
> Task :adapter:outbound:persistence-jpa:testkitJar
|
||||
> Task :app-bootstrap:compileTestJava
|
||||
> Task :app-bootstrap:runtimeClasspathManifest
|
||||
> Task :app-bootstrap:processTestResources
|
||||
> Task :app-bootstrap:testClasses
|
||||
> Task :messaging:messaging-admin-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-admin-api:classes
|
||||
> Task :messaging:messaging-admin-api:jar
|
||||
> Task :messaging:messaging-admin-runtime:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-admin-runtime:classes
|
||||
> Task :messaging:messaging-admin-runtime:jar
|
||||
> Task :messaging:messaging-claim-check:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-claim-check:classes
|
||||
> Task :messaging:messaging-claim-check:jar
|
||||
> Task :messaging:messaging-cloudevents:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-cloudevents:classes
|
||||
> Task :messaging:messaging-cloudevents:jar
|
||||
> Task :messaging:messaging-core-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-core-api:classes
|
||||
> Task :messaging:messaging-core-api:jar
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:processResources
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:classes
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:jar
|
||||
> Task :messaging:messaging-kafka:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-kafka:classes
|
||||
> Task :messaging:messaging-kafka:jar
|
||||
> Task :messaging:messaging-observability:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-observability:classes
|
||||
> Task :messaging:messaging-observability:jar
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:processResources
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:classes
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:jar
|
||||
> Task :messaging:messaging-policy:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-policy:classes
|
||||
> Task :messaging:messaging-policy:jar
|
||||
> Task :messaging:messaging-rabbit:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-rabbit:classes
|
||||
> Task :messaging:messaging-rabbit:jar
|
||||
> Task :messaging:messaging-reliability-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-reliability-api:classes
|
||||
> Task :messaging:messaging-reliability-api:jar
|
||||
> Task :messaging:messaging-runtime-core:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-runtime-core:classes
|
||||
> Task :messaging:messaging-runtime-core:jar
|
||||
> Task :messaging:messaging-schema-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-schema-api:classes
|
||||
> Task :messaging:messaging-schema-api:jar
|
||||
> Task :messaging:messaging-schema-json:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-schema-json:classes
|
||||
> Task :messaging:messaging-schema-json:jar
|
||||
> Task :messaging:messaging-security:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-security:classes
|
||||
> Task :messaging:messaging-security:jar
|
||||
> Task :messaging:messaging-spring-boot-starter:processResources
|
||||
> Task :messaging:messaging-spring-boot-starter:classes
|
||||
> Task :messaging:messaging-spring-boot-starter:jar
|
||||
> Task :messaging:messaging-transport-spi:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-transport-spi:classes
|
||||
> Task :messaging:messaging-transport-spi:jar
|
||||
> Task :adapter:outbound:objectstorage:compileJava
|
||||
> Task :adapter:outbound:objectstorage:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:objectstorage:classes
|
||||
> Task :adapter:outbound:objectstorage:jar
|
||||
> Task :app-bootstrap:test
|
||||
|
||||
BUILD SUCCESSFUL in 1m 31s
|
||||
98 actionable tasks: 98 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
Exit code: 0
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
Command: git history for cursor/capability/entity-not-found API hotspots
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T07:50:26Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Repository history for the three selected API hotspots; commit text/diffs show when code entered or changed but do not establish unstated author intent.
|
||||
--- stdout/stderr ---
|
||||
=== SignedJsonCursorCodec history ===
|
||||
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
|
||||
0e61f86 feat(jpa): implement the JPA relational persistence platform
|
||||
=== decodedLengthOf introducing diff ===
|
||||
COMMIT 2f5d2fc21954286213c1474d19935f571ef896ea feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
|
||||
|
||||
diff --git a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/SignedJsonCursorCodec.java b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/SignedJsonCursorCodec.java
|
||||
index 0263e77..7c306cd 100644
|
||||
--- a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/SignedJsonCursorCodec.java
|
||||
+++ b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/SignedJsonCursorCodec.java
|
||||
@@ -50,13 +50,33 @@ public final class SignedJsonCursorCodec<C> implements CursorCodec<C> {
|
||||
this.key = key.clone();
|
||||
}
|
||||
|
||||
+ /** Longest token this codec will look at, in characters. */
|
||||
+ public static final int MAX_ENCODED_LENGTH = 4096;
|
||||
+
|
||||
+ /** Largest payload this codec will decode, in bytes. */
|
||||
+ public static final int MAX_PAYLOAD_BYTES = 2048;
|
||||
+
|
||||
+ /** HMAC-SHA256 produces exactly this many bytes. */
|
||||
+ private static final int MAC_LENGTH_BYTES = 32;
|
||||
+
|
||||
@Override
|
||||
public String encode(C cursor) {
|
||||
Objects.requireNonNull(cursor, "cursor");
|
||||
- String payload =
|
||||
- encoder.encodeToString(payloadCodec.toJson(cursor).getBytes(StandardCharsets.UTF_8));
|
||||
+ byte[] json = payloadCodec.toJson(cursor).getBytes(StandardCharsets.UTF_8);
|
||||
+ if (json.length > MAX_PAYLOAD_BYTES) {
|
||||
+ // The application's own bug, not a caller's: a cursor this large is a payload the codec
|
||||
+ // cannot hand back, so producing it would mint a token that fails on the next page.
|
||||
+ throw new IllegalStateException(
|
||||
+ "cursor payload is " + json.length + " bytes; the bound is " + MAX_PAYLOAD_BYTES);
|
||||
+ }
|
||||
+ String payload = encoder.encodeToString(json);
|
||||
String signed = VERSION + SEPARATOR + payload;
|
||||
- return signed + SEPARATOR + encoder.encodeToString(mac(signed));
|
||||
+ String token = signed + SEPARATOR + encoder.encodeToString(mac(signed));
|
||||
+ if (token.length() > MAX_ENCODED_LENGTH) {
|
||||
+ throw new IllegalStateException(
|
||||
+ "cursor token is " + token.length() + " characters; the bound is " + MAX_ENCODED_LENGTH);
|
||||
+ }
|
||||
+ return token;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -64,6 +84,13 @@ public final class SignedJsonCursorCodec<C> implements CursorCodec<C> {
|
||||
if (encoded == null || encoded.isBlank()) {
|
||||
throw new IllegalArgumentException("cursor must not be blank");
|
||||
}
|
||||
+ // First line, before any substring, decode or MAC. A paging endpoint is public, and everything
|
||||
+ // below this point allocates in proportion to what the caller sent: repeatedly posting a very
|
||||
+ // large token made the server build strings, byte arrays and a MAC input before it had any
|
||||
+ // reason to believe the token was real. A page-size bound does not bound the token.
|
||||
+ if (encoded.length() > MAX_ENCODED_LENGTH) {
|
||||
+ throw new IllegalArgumentException("cursor exceeds the maximum token length");
|
||||
+ }
|
||||
int payloadSeparator = encoded.indexOf(SEPARATOR);
|
||||
int macSeparator = encoded.lastIndexOf(SEPARATOR);
|
||||
if (payloadSeparator <= 0 || macSeparator <= payloadSeparator) {
|
||||
@@ -73,15 +100,33 @@ public final class SignedJsonCursorCodec<C> implements CursorCodec<C> {
|
||||
if (!VERSION.equals(version)) {
|
||||
throw new IllegalArgumentException("unknown cursor version");
|
||||
}
|
||||
+ // Base64 expands by 4/3, so the encoded payload segment's length bounds the decoded size
|
||||
+ // exactly. Checking it here refuses an oversized payload without allocating it first.
|
||||
+ int encodedPayloadLength = macSeparator - payloadSeparator - 1;
|
||||
+ if (decodedLengthOf(encodedPayloadLength) > MAX_PAYLOAD_BYTES) {
|
||||
+ throw new IllegalArgumentException("cursor payload exceeds the maximum size");
|
||||
+ }
|
||||
String signed = encoded.substring(0, macSeparator);
|
||||
byte[] presented = decodeBase64(encoded.substring(macSeparator + 1));
|
||||
+ if (presented.length != MAC_LENGTH_BYTES) {
|
||||
+ // Checked before the comparison. `MessageDigest.isEqual` is constant time for equal-length
|
||||
+ // inputs; feeding it a differently-sized array asks it a question it was not built to answer.
|
||||
+ throw new IllegalArgumentException("malformed cursor");
|
||||
+ }
|
||||
if (!MessageDigest.isEqual(mac(signed), presented)) {
|
||||
throw new IllegalArgumentException("cursor signature does not verify");
|
||||
}
|
||||
+ // The payload decoder runs only after the signature verified, so an unsigned token never
|
||||
+ // reaches the application's JSON parsing at all.
|
||||
byte[] payload = decodeBase64(encoded.substring(payloadSeparator + 1, macSeparator));
|
||||
return payloadCodec.fromJson(new String(payload, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
+ /** The number of bytes a Base64 segment of this many characters decodes to, at most. */
|
||||
+ private static int decodedLengthOf(int encodedLength) {
|
||||
+ return encodedLength / 4 * 3 + 3;
|
||||
+ }
|
||||
+
|
||||
private byte[] decodeBase64(String value) {
|
||||
try {
|
||||
return decoder.decode(value);
|
||||
=== CapabilitySupport history ===
|
||||
0e61f86 feat(jpa): implement the JPA relational persistence platform
|
||||
=== JpaEntityNotFoundException history ===
|
||||
0e61f86 feat(jpa): implement the JPA relational persistence platform
|
||||
|
||||
Exit code: 0
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
Command: python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
for sr in ['main','test']:
|
||||
base=Path('src/adapter/outbound/persistence-jpa/src')/sr/'java/dev/caskeleton/adapter/outbound/persistence'
|
||||
for top in ['transaction','failure']:
|
||||
p=base/top
|
||||
files=sorted(p.rglob('*.java')) if p.exists() else []
|
||||
print(sr,top,'count=',len(files),'loc=',sum(len(f.read_text(errors='ignore').splitlines()) for f in files))
|
||||
for f in files: print(f)
|
||||
PY
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T08:01:13Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Enumerates the transaction/failure bounded sub-scope production and dedicated test files. It establishes the 51-file denominator only.
|
||||
--- stdout/stderr ---
|
||||
main transaction count= 29 loc= 2427
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/BackoffCalculator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecord.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecorder.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicy.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EffectiveTransactionTimeouts.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/JpaTransactionConfig.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/JpaTransactionSettings.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/OptimisticConflictTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/PersistenceFailureTranslatorChain.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/RetryBudget.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/RetrySleeper.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringPolicyTransactionPort.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/ThreadRetrySleeper.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDeadlineCalculator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDefinitionMapper.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceContext.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceFrame.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceScope.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionLocalTimeoutConfigurer.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistry.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionRetryBackoff.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionRetryClassifier.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionStartBudget.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/UnknownOperation.java
|
||||
main failure count= 3 loc= 165
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/failure/PersistenceExceptionTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/failure/SqlStateErrorMapping.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/failure/StandardSqlStateErrorMapping.java
|
||||
test transaction count= 18 loc= 2203
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/BackoffCalculatorTest.java
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifierTest.java
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecorderTest.java
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicyTest.java
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManagerTest.java
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinatorTest.java
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/JpaTransactionSettingsTest.java
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/PersistenceFailureTranslatorChainTest.java
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/RetryBudgetTest.java
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutorTest.java
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringPolicyTransactionPortTest.java
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPortTest.java
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDeadlineCalculatorTest.java
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDefinitionMapperTest.java
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceScopeTest.java
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistryTest.java
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionRetryBackoffTest.java
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionRetryClassifierTest.java
|
||||
test failure count= 1 loc= 230
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/failure/PersistenceExceptionTranslatorTest.java
|
||||
|
||||
Exit code: 0
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
Command: python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import re,subprocess
|
||||
roots=[Path('src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction'),Path('src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/failure')]
|
||||
alljava=[Path(p) for p in subprocess.check_output(['git','ls-files'],text=True).splitlines() if '/src/main/java/' in p and p.endswith('.java')]
|
||||
for root in roots:
|
||||
for src in sorted(root.rglob('*.java')):
|
||||
txt=src.read_text(); m=re.search(r'public\s+(?:final\s+)?(?:record|class|interface|enum)\s+(\w+)',txt)
|
||||
if not m: continue
|
||||
name=m.group(1); pkg=re.search(r'package\s+([^;]+);',txt).group(1); fqn=pkg+'.'+name
|
||||
refs=[]
|
||||
for p in alljava:
|
||||
if p==src: continue
|
||||
t=p.read_text(errors='ignore'); pp=re.search(r'package\s+([^;]+);',t)
|
||||
if f'import {fqn};' in t or fqn in t or (pp and pp.group(1)==pkg and re.search(r'\b'+re.escape(name)+r'\b',t)):
|
||||
refs.append(str(p))
|
||||
print(f'{fqn}|production_reference_files={len(refs)}')
|
||||
for r in refs: print(' '+r)
|
||||
PY
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T08:01:13Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Searches tracked production Java for transaction/failure public top-level type references. Zero references are negative evidence only; reflection/generated/external consumers remain outside the search.
|
||||
--- stdout/stderr ---
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.BackoffCalculator|production_reference_files=1
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicy.java
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.CommitFailureClassifier|production_reference_files=2
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.CompletionUnknownRecord|production_reference_files=0
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.CompletionUnknownRecorder|production_reference_files=0
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.DefaultJpaRetryPolicy|production_reference_files=2
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.EffectiveTransactionTimeouts|production_reference_files=5
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2LocalTimeoutConfigurer.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlLocalTimeoutConfigurer.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringPolicyTransactionPort.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDeadlineCalculator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionLocalTimeoutConfigurer.java
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.EvidenceAwareJpaTransactionManager|production_reference_files=0
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.FullTransactionRetryCoordinator|production_reference_files=3
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.JpaTransactionConfig|production_reference_files=0
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.JpaTransactionSettings|production_reference_files=4
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/JpaTransactionConfig.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDeadlineCalculator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionRetryBackoff.java
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.OptimisticConflictTranslator|production_reference_files=2
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/PersistenceFailureTranslatorChain.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.PersistenceFailureTranslatorChain|production_reference_files=2
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.RetryBudget|production_reference_files=1
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.RetrySleeper|production_reference_files=3
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/ThreadRetrySleeper.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.SpringJpaTransactionExecutor|production_reference_files=3
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.SpringTransactionPort|production_reference_files=1
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/JpaTransactionConfig.java
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.ThreadRetrySleeper|production_reference_files=1
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.TransactionDefinitionMapper|production_reference_files=1
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.TransactionEvidenceContext|production_reference_files=4
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceScope.java
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.TransactionEvidenceFrame|production_reference_files=3
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceContext.java
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.TransactionEvidenceScope|production_reference_files=2
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceContext.java
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.TransactionLocalTimeoutConfigurer|production_reference_files=7
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2LocalTimeoutConfigurer.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2PersistenceConfig.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlLocalTimeoutConfigurer.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlPersistenceConfig.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringPolicyTransactionPort.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/persistence/SamplePostgreSqlPersistenceConfig.java
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.TransactionProfileRegistry|production_reference_files=0
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.TransactionStartBudget|production_reference_files=2
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringPolicyTransactionPort.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDeadlineCalculator.java
|
||||
dev.caskeleton.adapter.outbound.persistence.transaction.UnknownOperation|production_reference_files=1
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java
|
||||
dev.caskeleton.adapter.outbound.persistence.failure.PersistenceExceptionTranslator|production_reference_files=3
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/failure/SqlStateErrorMapping.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringPolicyTransactionPort.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java
|
||||
dev.caskeleton.adapter.outbound.persistence.failure.SqlStateErrorMapping|production_reference_files=7
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/failure/PersistenceExceptionTranslator.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/failure/StandardSqlStateErrorMapping.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2PersistenceConfig.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2SqlStateErrorMapping.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlPersistenceConfig.java
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlSqlStateErrorMapping.java
|
||||
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/persistence/SamplePostgreSqlPersistenceConfig.java
|
||||
dev.caskeleton.adapter.outbound.persistence.failure.StandardSqlStateErrorMapping|production_reference_files=1
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java
|
||||
|
||||
Exit code: 0
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
Command: printf '%s\n' '--- persistence root ---'; sed -n '1,115p' src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/persistencejpa/PersistenceJpaRootAutoConfiguration.java; printf '%s\n' '--- adapter component scan ---'; cat src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/config/JpaAdapterComponentsConfig.java; printf '%s\n' '--- runtime transaction beans ---'; sed -n '140,195p' src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java; printf '%s\n' '--- canonical/application references ---'; git grep -n -E '\b(SpringTransactionPort|PolicyTransactionPort|FullTransactionRetryCoordinator|SpringJpaTransactionExecutor)\b' -- 'src/**/src/main/java/**/*.java'
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T08:01:16Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Shows shipped persistence root imports, narrow adapter component scan, transaction bean factories, and exact production construction/reference sites for the two transaction stacks. Static wiring does not prove runtime invocation frequency.
|
||||
--- stdout/stderr ---
|
||||
--- persistence root ---
|
||||
package dev.caskeleton.bootstrap.autoconfigure.persistencejpa;
|
||||
|
||||
import dev.caskeleton.adapter.outbound.persistence.config.JpaAdapterComponentsConfig;
|
||||
import dev.caskeleton.adapter.outbound.persistence.config.PersistenceVendorSettings;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.FileserverJpaPersistenceConfig;
|
||||
import dev.caskeleton.adapter.outbound.persistence.h2.H2PersistenceConfig;
|
||||
import dev.caskeleton.adapter.outbound.persistence.notification.configuration.NotificationJpaPersistenceFacade;
|
||||
import dev.caskeleton.adapter.outbound.persistence.postgresql.PostgreSqlPersistenceConfig;
|
||||
import dev.caskeleton.bootstrap.autoconfigure.jpa.JpaDataSourceProfileValidator;
|
||||
import dev.caskeleton.bootstrap.autoconfigure.jpa.JpaPlatformRuntimeAutoConfiguration;
|
||||
import dev.caskeleton.bootstrap.autoconfigure.jpa.PostgreSqlVersionPolicy;
|
||||
import dev.caskeleton.bootstrap.runtime.startup.MigrationStartupConfig;
|
||||
import javax.sql.DataSource;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
/**
|
||||
* The one place that decides whether this application has relational persistence.
|
||||
*
|
||||
* <p>Every JPA configuration is reached through this import rather than through the component scan,
|
||||
* so a bean added to any of them next month is gated without anyone remembering to repeat a
|
||||
* condition. That is the difference between a capability that is off and a capability whose known
|
||||
* beans are individually conditioned.
|
||||
*
|
||||
* <p>Both vendor configurations are named here, and each is the leaf\'s real entry point: a vendor
|
||||
* config imports the core JPA config, not the reverse. Inverting that to give this class a single
|
||||
* import produced a package cycle inside the leaf, so the export surface admits the two vendor
|
||||
* packages instead. They are mutually exclusive on {@code ca-skeleton.persistence.vendor}, so
|
||||
* naming both lets the selector decide which one assembles while the decision about
|
||||
* <em>whether</em> there is a database stays here, in one place.
|
||||
*
|
||||
* <p><b>The notification facade is named here only now that the payload is protected at rest.</b>
|
||||
* Importing it makes {@code JpaNotificationRequestStore} reachable, and that store used to write
|
||||
* the accepted request's template variables to {@code notification_request.variables_payload} in
|
||||
* plaintext — caller content that can be a reset code, an order total or an address (NTF-INT-007).
|
||||
* It was wired here once before that was true; the at-rest contract test failed on exactly that
|
||||
* fact, which is what it was written to do, and the wiring was reverted rather than the test
|
||||
* relaxed. The envelope came first, {@code NotificationRecordMapper} now takes the protection as a
|
||||
* required constructor argument, and only then does this import become a wiring rather than an
|
||||
* exposure.
|
||||
*
|
||||
* <p>{@code FileserverJpaPersistenceConfig} is named for a related reason. Its six entities map six
|
||||
* {@code fs_*} tables that live only in {@code db/migration/jpa/fileserver}, a stream applied only
|
||||
* when that capability is on, while the primary Flyway location creates none of them. They used to
|
||||
* be in the unconditional entity scan, so {@code ddl-auto=validate} against real PostgreSQL failed
|
||||
* on {@code fs_cleanup_item} in every deployment that had switched the capability off — which was
|
||||
* every JPA-on Compose lane. The scan now carries the capability's own condition, and this is the
|
||||
* only place that knows both that the capability is on and that a JPA vendor is composed. Neither
|
||||
* the leaf's {@code config} package nor a component scan can register it: {@code config} may depend
|
||||
* on {@code api} alone, and the whole persistence package is excluded from the root's scan.
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnProperty(
|
||||
prefix = "ca-skeleton.persistence-jpa",
|
||||
name = "enabled",
|
||||
havingValue = "true")
|
||||
@Import({
|
||||
JpaAdapterComponentsConfig.class,
|
||||
PostgreSqlPersistenceConfig.class,
|
||||
H2PersistenceConfig.class,
|
||||
FileserverJpaPersistenceConfig.class,
|
||||
NotificationJpaPersistenceFacade.class,
|
||||
MigrationStartupConfig.class,
|
||||
JpaPlatformRuntimeAutoConfiguration.class
|
||||
})
|
||||
public class PersistenceJpaRootAutoConfiguration {
|
||||
|
||||
/**
|
||||
* Runs the datasource validator, which nothing used to run (JPA-INT-002).
|
||||
*
|
||||
* <p>{@code JpaDataSourceProfileValidator} was created as a bean and never invoked: both of its
|
||||
* methods were reachable only from their own unit test. A validator nobody calls is a comment —
|
||||
* and this one would have failed every deployment if called, because the namespace it read was
|
||||
* supplied by nothing. The two defects cancelled, which is the only reason the application
|
||||
* started.
|
||||
*
|
||||
* <p>Invoked from here rather than from the platform configuration so it runs exactly when JPA is
|
||||
* on and never when it is off, and as an {@link InitializingBean} so the failure lands during
|
||||
* startup rather than at whoever sends the first request.
|
||||
*
|
||||
* <p>The vendor is read from the {@link Environment} rather than injected as {@code
|
||||
* PersistenceVendorSettings}, because that type is not a registered bean here: the composition
|
||||
* root's {@code @ConfigurationPropertiesScan} deliberately excludes the persistence package so an
|
||||
* optional capability cannot bind — or reject — its detail settings in a deployment that never
|
||||
* switched it on. The default matches {@code PostgreSqlPersistenceConfig}'s {@code matchIfMissing
|
||||
* = true}: an unset selector is PostgreSQL, which is the vendor this configuration was
|
||||
* unconditional about before the selector existed.
|
||||
*
|
||||
* <p>The validator is constructed here rather than injected. It is stateless, and the bean that
|
||||
* used to supply it comes from {@code JpaPlatformRuntimeAutoConfiguration}, which carries
|
||||
* {@code @ConditionalOnBean(DataSource.class)} on a plain {@code @Configuration} imported by this
|
||||
* root — a condition evaluated during configuration-class parsing, before the datasource bean
|
||||
* definition is registered. That class therefore drops out silently in the real application,
|
||||
* taking the whole JPA add-on layer with it. Depending on a bean from it would make this check
|
||||
* disappear for the same reason the thing it checks disappeared. The condition-ordering defect is
|
||||
* recorded separately; it is not this task's to fix quietly.
|
||||
*
|
||||
* @param dataSource the resolved datasource, injected so the database checked is the one in use
|
||||
* @param environment the resolved environment, for the vendor selector
|
||||
* @return the startup check
|
||||
*/
|
||||
@Bean
|
||||
public InitializingBean jpaResolvedDataSourceCheck(
|
||||
DataSource dataSource, Environment environment) {
|
||||
JpaDataSourceProfileValidator validator =
|
||||
new JpaDataSourceProfileValidator(new PostgreSqlVersionPolicy());
|
||||
String vendor =
|
||||
environment.getProperty(PersistenceVendorSettings.VENDOR_PROPERTY, "postgresql").trim();
|
||||
return () -> validator.validateResolved(dataSource, "postgresql".equalsIgnoreCase(vendor));
|
||||
}
|
||||
}
|
||||
--- adapter component scan ---
|
||||
package dev.caskeleton.adapter.outbound.persistence.config;
|
||||
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Registers the JPA adapter's scanned components, which nothing registered (JPA-INT-006).
|
||||
*
|
||||
* <p>The composition root's {@code @ComponentScan} excludes {@code
|
||||
* dev.caskeleton.adapter.outbound.persistence.**} by regex, and that exclusion is correct: it is
|
||||
* what makes an optional capability optional, so a deployment with JPA off assembles no persistence
|
||||
* beans rather than assembling them and hoping each one remembered to carry the switch.
|
||||
*
|
||||
* <p>What was missing is the other half. Eight classes in this leaf are written as scanned
|
||||
* components — {@code SpringTransactionPort}, {@code PersistenceExceptionTranslator}, {@code
|
||||
* StandardSqlStateErrorMapping}, {@code DomainContextAuditContextPort}, the idempotency store and
|
||||
* its reaper, and the outbox store and its reaper — and once the broad scan stopped reaching them,
|
||||
* nothing else did. They are annotated {@code @Component} and {@code @Repository} and were beans in
|
||||
* no running application: {@code TransactionPort} in particular had no implementation at all, so
|
||||
* every use case that opens a transaction had no port to open it with.
|
||||
*
|
||||
* <p>It surfaced as an unsatisfied dependency the first time a capability that needs a transaction
|
||||
* was actually assembled — the notification orchestrator, in the local-notification-ingest lane —
|
||||
* rather than as anything a unit test could see, because each of these classes is constructed
|
||||
* directly by its own tests.
|
||||
*
|
||||
* <p>So the scan is restored, narrowed to the packages it should always have covered and reachable
|
||||
* only through {@code PersistenceJpaRootAutoConfiguration}, which carries the JPA master switch.
|
||||
* Off is still structural.
|
||||
*
|
||||
* <p>Two packages are deliberately absent:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code ..persistence.fileserver} — gated on its own capability switch, scanned by {@link
|
||||
* dev.caskeleton.adapter.outbound.persistence.fileserver.FileserverJpaPersistenceConfig};
|
||||
* <li>{@code ..persistence.notification} — assembled explicitly, bean by bean, by {@code
|
||||
* NotificationJpaPersistenceFacade}.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Components under these packages keep their own {@code @ConditionalOnProperty} guards; being
|
||||
* scanned makes them candidates, not unconditional beans.
|
||||
*
|
||||
* <p>{@code ..persistence.lock} is in the list for the same reason and with the same history.
|
||||
* {@code DistributedLockPersistenceConfig} owns both lock providers — the in-process registry and
|
||||
* the JDBC one — and was registered by nothing but a test calling {@code ctx.register(...)}. So a
|
||||
* single-instance deployment had no {@code DistributedLockPort} at all, and a multi-instance one
|
||||
* could not start: the composition root's own {@code DistributedLockConfig} asks for a bean
|
||||
* qualified {@code jdbcDistributedLock} that only that configuration declares.
|
||||
*
|
||||
* <p>Each package's {@code @ConfigurationProperties} type is enabled by a configuration inside that
|
||||
* same package — {@code JpaTransactionConfig} for {@code JpaTransactionSettings}, {@code
|
||||
* DistributedLockPersistenceConfig} for {@code LockSettings} — rather than from here. Enabling them
|
||||
* centrally would give {@code config} an edge to {@code lock} and {@code transaction} that the
|
||||
* module map does not grant it, and the map is right: this class knows which packages to scan, not
|
||||
* what is inside them. They need enabling at all because {@code @ConfigurationPropertiesScan}
|
||||
* excludes this tree as deliberately as {@code @ComponentScan} does.
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ComponentScan(
|
||||
basePackages = {
|
||||
"dev.caskeleton.adapter.outbound.persistence.audit",
|
||||
"dev.caskeleton.adapter.outbound.persistence.failure",
|
||||
"dev.caskeleton.adapter.outbound.persistence.idempotency",
|
||||
"dev.caskeleton.adapter.outbound.persistence.lock",
|
||||
"dev.caskeleton.adapter.outbound.persistence.outbox",
|
||||
"dev.caskeleton.adapter.outbound.persistence.transaction"
|
||||
})
|
||||
public class JpaAdapterComponentsConfig {}
|
||||
--- runtime transaction beans ---
|
||||
}
|
||||
|
||||
/**
|
||||
* The programmatic transaction boundary, over whichever manager the application installed.
|
||||
*
|
||||
* <p>The composed vendor's translator is taken when one exists. Without it the executor's chain
|
||||
* has no SQLSTATE stage, so a serialization failure or deadlock stays a raw {@code
|
||||
* DataAccessException}, never becomes the {@code JpaPersistenceException} the retry coordinator
|
||||
* catches, and contention is not retried at all.
|
||||
*
|
||||
* @param vendorFailures the composed vendor's SQLSTATE translator, when a vendor registered one
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnBean(PlatformTransactionManager.class)
|
||||
public SpringJpaTransactionExecutor jpaTransactionExecutor(
|
||||
PlatformTransactionManager transactionManager,
|
||||
Clock clock,
|
||||
ObjectProvider<VendorFailureTranslator> vendorFailures) {
|
||||
JpaTransactionAutoConfiguration composition = new JpaTransactionAutoConfiguration(clock);
|
||||
VendorFailureTranslator vendor = vendorFailures.getIfAvailable();
|
||||
return vendor == null
|
||||
? composition.transactionExecutor(transactionManager)
|
||||
: composition.transactionExecutor(transactionManager, vendor);
|
||||
}
|
||||
|
||||
/** The whole-transaction retry coordinator. */
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnBean(SpringJpaTransactionExecutor.class)
|
||||
public FullTransactionRetryCoordinator jpaRetryCoordinator(
|
||||
SpringJpaTransactionExecutor executor, RetryEventListener retryListener, Clock clock) {
|
||||
return new JpaTransactionAutoConfiguration(clock)
|
||||
.retryCoordinator(
|
||||
executor,
|
||||
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryProfile
|
||||
.boundedContention("jpa-platform-default", 3),
|
||||
retryListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* The retry listener.
|
||||
*
|
||||
* <p>The interface's methods all have defaults, so an empty implementation is the honest "no
|
||||
* observer wired" value; an application that wants retry telemetry supplies its own bean.
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public RetryEventListener jpaRetryEventListener() {
|
||||
return new RetryEventListener() {};
|
||||
}
|
||||
|
||||
/** Runs the dangerous-configuration guard during refresh. */
|
||||
@Bean
|
||||
public InitializingBean jpaPlatformStartupCheck(
|
||||
JpaDangerousConfigurationGuard guard, Environment environment) {
|
||||
--- canonical/application references ---
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/config/JpaAdapterComponentsConfig.java:15: * components — {@code SpringTransactionPort}, {@code PersistenceExceptionTranslator}, {@code
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:30:public final class FullTransactionRetryCoordinator {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:32: private final SpringJpaTransactionExecutor transactionExecutor;
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:50: public FullTransactionRetryCoordinator(
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:51: SpringJpaTransactionExecutor transactionExecutor,
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/JpaTransactionConfig.java:9: * <p>{@link SpringTransactionPort} takes it as a constructor argument, and the composition root's
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:21: * <p>This class does not retry. Retry lives in {@link FullTransactionRetryCoordinator}, which calls
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:25:public final class SpringJpaTransactionExecutor implements JpaTransactionExecutor {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:32: public SpringJpaTransactionExecutor(PlatformTransactionManager transactionManager, Clock clock) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:41: public SpringJpaTransactionExecutor(
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:8:import dev.caskeleton.application.transaction.PolicyTransactionPort;
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:26: * Spring-backed {@link PolicyTransactionPort}: one pre-built {@link TransactionTemplate} per mode,
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:31:public class SpringTransactionPort implements PolicyTransactionPort {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:39: public SpringTransactionPort(PlatformTransactionManager transactionManager) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:49: SpringTransactionPort(
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:61: public SpringTransactionPort(
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:77: SpringTransactionPort(
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:90: SpringTransactionPort(
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:105: SpringTransactionPort(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:7:import dev.caskeleton.adapter.outbound.persistence.transaction.FullTransactionRetryCoordinator;
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:8:import dev.caskeleton.adapter.outbound.persistence.transaction.SpringJpaTransactionExecutor;
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:155: public SpringJpaTransactionExecutor jpaTransactionExecutor(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:169: @ConditionalOnBean(SpringJpaTransactionExecutor.class)
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:170: public FullTransactionRetryCoordinator jpaRetryCoordinator(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:171: SpringJpaTransactionExecutor executor, RetryEventListener retryListener, Clock clock) {
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:9:import dev.caskeleton.adapter.outbound.persistence.transaction.FullTransactionRetryCoordinator;
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:13:import dev.caskeleton.adapter.outbound.persistence.transaction.SpringJpaTransactionExecutor;
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:37: * PolicyTransactionPort.inTransaction(TransactionRequest, Supplier)}; the retry coordinator below
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:66: public SpringJpaTransactionExecutor transactionExecutor(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:68: return new SpringJpaTransactionExecutor(transactionManager, clock);
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:82: public SpringJpaTransactionExecutor transactionExecutor(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:84: return new SpringJpaTransactionExecutor(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:99: public FullTransactionRetryCoordinator retryCoordinator(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:100: SpringJpaTransactionExecutor executor,
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:107: public FullTransactionRetryCoordinator retryCoordinator(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:108: SpringJpaTransactionExecutor executor, JpaRetryPolicy policy, RetryEventListener listener) {
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:109: return new FullTransactionRetryCoordinator(
|
||||
src/application-core/src/main/java/dev/caskeleton/application/transaction/PolicyTransactionPort.java:10:public interface PolicyTransactionPort extends TransactionPort {
|
||||
src/application-core/src/main/java/dev/caskeleton/application/transaction/TransactionPort.java:8: * {@code SpringTransactionPort}) implements it over Spring's {@code PlatformTransactionManager}.
|
||||
|
||||
Exit code: 0
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
Command: printf '%s\n' '--- canonical boundary claim ---'; sed -n '18,118p' src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java; printf '%s\n' '--- transaction guide ---'; cat docs/jpa/transaction-guide.md; printf '%s\n' '--- FullTransactionRetryCoordinator production refs ---'; git grep -n -w 'FullTransactionRetryCoordinator' -- 'src/**/src/main/java/**/*.java'; printf '%s\n' '--- coordinator execute call candidates ---'; git grep -n -E '\.(execute)\(' -- 'src/**/src/main/java/**/*.java' | grep -E 'retry|transaction|coordinator' || true
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T08:01:16Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Compares current source/document claims for the application PolicyTransactionPort stack versus the public JPA executor/coordinator stack, including exact production execute references. It identifies coexistence/drift but does not by itself prove external consumers are absent.
|
||||
--- stdout/stderr ---
|
||||
--- canonical boundary claim ---
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
/**
|
||||
* Composes the transaction half of the platform (design §9.3, §17, §19).
|
||||
*
|
||||
* <p>Separate from {@link JpaPlatformAutoConfiguration} because it backs off separately. An
|
||||
* application that already installs its own {@link PlatformTransactionManager} — a JTA setup, a
|
||||
* chained manager across two data sources — must keep it, and the rest of the platform is still
|
||||
* useful to it. Folding the two together would make "I have my own transaction manager" mean "I get
|
||||
* none of the platform".
|
||||
*
|
||||
* <p>Plain construction rather than Spring auto-configuration: this repository's composition root
|
||||
* owns wiring, and an adapter leaf that auto-configured a transaction manager would silently
|
||||
* replace one the application had deliberately chosen.
|
||||
*
|
||||
* <p>There is no declarative retry annotation any more. {@code @RetryableJpaTransaction} lived in
|
||||
* the persistence leaf and documented itself as something an application service would put on its
|
||||
* own methods — which application-core cannot do without importing an outbound adapter and
|
||||
* inverting the dependency this architecture is built on. The canonical boundary is {@code
|
||||
* PolicyTransactionPort.inTransaction(TransactionRequest, Supplier)}; the retry coordinator below
|
||||
* is what implements it, not a second way to ask for the same thing.
|
||||
*/
|
||||
public final class JpaTransactionAutoConfiguration {
|
||||
|
||||
/** How long a logical operation may keep retrying before the budget is spent regardless. */
|
||||
public static final Duration DEFAULT_MAX_RETRY_ELAPSED = Duration.ofSeconds(30);
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
public JpaTransactionAutoConfiguration(Clock clock) {
|
||||
this.clock = Objects.requireNonNull(clock, "clock");
|
||||
}
|
||||
|
||||
/**
|
||||
* The commit-phase classifier the evidence-aware transaction manager is built with.
|
||||
*
|
||||
* <p>The manager itself is constructed inside the persistence leaf, through {@code
|
||||
* EvidenceAwareJpaTransactionManager.standard(entityManagerFactory, clock)}. The composition root
|
||||
* deliberately does not do it here: that would require {@code jakarta.persistence} and {@code
|
||||
* org.hibernate} on the composition root's compile classpath, and this repository keeps ORM types
|
||||
* inside the persistence leaf. What the root owns is the decision — whether to install the
|
||||
* platform's manager at all — and the classifier it uses.
|
||||
*/
|
||||
public CommitFailureClassifier commitFailureClassifier() {
|
||||
return CommitFailureClassifier.standard(clock);
|
||||
}
|
||||
|
||||
/** The programmatic transaction boundary, with no vendor classification. */
|
||||
public SpringJpaTransactionExecutor transactionExecutor(
|
||||
PlatformTransactionManager transactionManager) {
|
||||
return new SpringJpaTransactionExecutor(transactionManager, clock);
|
||||
}
|
||||
|
||||
/**
|
||||
* The programmatic transaction boundary that classifies its attempts with a vendor's SQLSTATEs.
|
||||
*
|
||||
* <p>The overload above builds the catalog-free chain, whose vendor stage returns the failure
|
||||
* unchanged. That is the honest answer when no vendor translator exists, and it is the wrong one
|
||||
* whenever a vendor is composed: a 40001 or 40P01 stays a raw {@code DataAccessException}, never
|
||||
* becomes a {@code JpaPersistenceException}, and so never reaches the retry coordinator's catch.
|
||||
* Contention then goes unretried in exactly the deployments that have a database.
|
||||
*
|
||||
* @param vendorFailures the composed vendor's SQLSTATE translator
|
||||
*/
|
||||
public SpringJpaTransactionExecutor transactionExecutor(
|
||||
PlatformTransactionManager transactionManager, VendorFailureTranslator vendorFailures) {
|
||||
return new SpringJpaTransactionExecutor(
|
||||
transactionManager,
|
||||
clock,
|
||||
new PersistenceFailureTranslatorChain(
|
||||
OptimisticConflictTranslator.withoutCatalog(), vendorFailures));
|
||||
}
|
||||
|
||||
/**
|
||||
* The retry coordinator for one retry profile.
|
||||
*
|
||||
* <p>The policy and the coordinator's budget are built from the <em>same</em> profile on purpose.
|
||||
* Configuring them independently is how a deployment ends up with a policy that says "retry" and
|
||||
* a budget that permits one attempt — which looks like retry being broken rather than like a
|
||||
* misconfiguration.
|
||||
*/
|
||||
public FullTransactionRetryCoordinator retryCoordinator(
|
||||
SpringJpaTransactionExecutor executor,
|
||||
RetryProfile retryProfile,
|
||||
RetryEventListener listener) {
|
||||
return retryCoordinator(executor, DefaultJpaRetryPolicy.forProfile(retryProfile), listener);
|
||||
}
|
||||
|
||||
/** The retry coordinator for an application-supplied policy. */
|
||||
public FullTransactionRetryCoordinator retryCoordinator(
|
||||
SpringJpaTransactionExecutor executor, JpaRetryPolicy policy, RetryEventListener listener) {
|
||||
return new FullTransactionRetryCoordinator(
|
||||
executor, policy, retrySleeper(), clock, DEFAULT_MAX_RETRY_ELAPSED, listener);
|
||||
}
|
||||
|
||||
/** The production sleeper; tests substitute a recording one. */
|
||||
public RetrySleeper retrySleeper() {
|
||||
return new ThreadRetrySleeper();
|
||||
}
|
||||
}
|
||||
--- transaction guide ---
|
||||
# Transaction Guide
|
||||
|
||||
Design §15-§20. What owns a transaction, what may be retried, and what must never be.
|
||||
|
||||
## The application service owns the boundary
|
||||
|
||||
Repository adapters do not open transactions. The use case does, through `TransactionPort` or
|
||||
`JpaTransactionExecutor`, because the unit of work is a business decision and only the use case
|
||||
knows where it starts and ends.
|
||||
|
||||
Open Session In View is off in every runtime profile. It is on by default in Spring Boot, which is
|
||||
why `JpaDangerousConfigurationGuard` fails startup rather than trusting configuration review.
|
||||
|
||||
## Profiles
|
||||
|
||||
A `TransactionProfile` fixes propagation, isolation, timeout, read-only, and the retry budget. A
|
||||
write profile must carry a positive finite timeout — the type refuses to represent one without —
|
||||
because an unbounded write transaction holds a connection, its locks, and its row versions for as
|
||||
long as one stuck statement takes.
|
||||
|
||||
`REQUIRES_NEW` is opt-in. It acquires a second physical connection while pinning the first, so a
|
||||
profile using it must be paired with the pool-pressure evidence in design §38:
|
||||
|
||||
```text
|
||||
maximumPoolSize >= (concurrent_threads x (1 + max_requires_new_depth)) + 1
|
||||
```
|
||||
|
||||
## Retry is per use case, never per statement
|
||||
|
||||
`FullTransactionRetryCoordinator` re-enters the executor, which produces a new transaction and a new
|
||||
Persistence Context for every attempt. That granularity is the whole point: an optimistic conflict
|
||||
means the state the attempt computed against is no longer the committed state, so re-issuing the
|
||||
same statement would compute the same wrong answer. The domain rules have to run again against
|
||||
reloaded data.
|
||||
|
||||
Retryable: serialization failure (`40001`), deadlock (`40P01`), optimistic conflict.
|
||||
Not retryable: constraint violations, schema mismatch, query timeout, and anything unclassified.
|
||||
|
||||
Two additional refusals, independent of budget:
|
||||
|
||||
- An attempt that declared an irreversible external effect through `IrreversibleSideEffectContext`.
|
||||
Rollback reverses database work only; an email or a card charge has already changed the world.
|
||||
- Anything completion-unknown.
|
||||
|
||||
## Completion unknown
|
||||
|
||||
`TransactionCompletionUnknownException` is never retried, and the type system enforces it twice:
|
||||
`JpaFailureContext` refuses to represent a retryable completion-unknown failure, and the exception
|
||||
rebuilds its context through the safe factory whatever it is handed.
|
||||
|
||||
`EvidenceAwareJpaTransactionManager` marks the phase `COMMITTING` immediately before delegating to
|
||||
the provider commit and never after. If the network, the JVM, or the server dies inside that call,
|
||||
the last thing written is "we asked, we do not know" — which is exactly the state that must not be
|
||||
mistaken for a rollback.
|
||||
|
||||
Recovery is reconciliation, not retry:
|
||||
|
||||
```text
|
||||
record the transaction key -> check the idempotency record
|
||||
-> check the business row
|
||||
-> check the outbox
|
||||
-> still undetermined? reconciliation queue
|
||||
```
|
||||
|
||||
`CompletionUnknownRecorder` writes that record through a channel outside the unknown transaction.
|
||||
Writing it through the same connection would make the audit trail share the failure it documents.
|
||||
--- FullTransactionRetryCoordinator production refs ---
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:30:public final class FullTransactionRetryCoordinator {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:50: public FullTransactionRetryCoordinator(
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:21: * <p>This class does not retry. Retry lives in {@link FullTransactionRetryCoordinator}, which calls
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:7:import dev.caskeleton.adapter.outbound.persistence.transaction.FullTransactionRetryCoordinator;
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:170: public FullTransactionRetryCoordinator jpaRetryCoordinator(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:9:import dev.caskeleton.adapter.outbound.persistence.transaction.FullTransactionRetryCoordinator;
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:99: public FullTransactionRetryCoordinator retryCoordinator(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:107: public FullTransactionRetryCoordinator retryCoordinator(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:109: return new FullTransactionRetryCoordinator(
|
||||
--- coordinator execute call candidates ---
|
||||
src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/restclient/DefaultGenericHttpGateway.java:118: HttpCallResult<T> result = coordinator.execute(call);
|
||||
src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/webclient/DefaultReactiveHttpGateway.java:149: coordinator.execute(
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:101: transactionExecutor.execute(operation, profile, work, attemptNumber, transactionKey);
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:83: return template.execute(status -> work.get());
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:166: return policyExecutor.execute(request, action);
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:197: return template.execute(status -> action.get());
|
||||
src/adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/transaction/SpringMongoTransactionExecutor.java:57: return coordinator.execute(profile, work);
|
||||
|
||||
Exit code: 0
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
Command: printf '%s\n' '--- EvidenceAware manager refs ---'; git grep -n -w 'EvidenceAwareJpaTransactionManager' -- 'src/**/src/main/java/**/*.java'; printf '%s\n' '--- actual construction calls ---'; git grep -n -E 'EvidenceAwareJpaTransactionManager\.standard|new EvidenceAwareJpaTransactionManager' -- 'src/**/src/main/java/**/*.java' || true; printf '%s\n' '--- recorder/record refs ---'; git grep -n -E '\bCompletionUnknown(Recorder|Record)\b' -- 'src/**/src/main/java/**/*.java' || true; printf '%s\n' '--- metric implementation/calls ---'; git grep -n -E 'jpa\.transaction\.completion\.unknown|recordCompletionUnknown|JpaTransactionObservation' -- 'src/**/src/main/java/**/*.java' 'docs/jpa/**'; printf '%s\n' '--- observability composition refs ---'; git grep -n -w 'JpaObservabilityAutoConfiguration' -- 'src/**/src/main/java/**/*.java' || true; printf '%s\n' '--- application indeterminate path ---'; sed -n '105,185p' src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringPolicyTransactionPort.java
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T08:01:16Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Traces current production reachability for the custom evidence-aware transaction manager, completion-unknown recorder/record, transaction observation, completion-unknown meter, and application indeterminate result. It does not simulate a real lost-commit-ack database/network failure.
|
||||
--- stdout/stderr ---
|
||||
--- EvidenceAware manager refs ---
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java:24:public class EvidenceAwareJpaTransactionManager extends JpaTransactionManager {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java:30: public EvidenceAwareJpaTransactionManager(
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java:37: public static EvidenceAwareJpaTransactionManager standard(
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java:39: return new EvidenceAwareJpaTransactionManager(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:55: * EvidenceAwareJpaTransactionManager.standard(entityManagerFactory, clock)}. The composition root
|
||||
--- actual construction calls ---
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java:39: return new EvidenceAwareJpaTransactionManager(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:55: * EvidenceAwareJpaTransactionManager.standard(entityManagerFactory, clock)}. The composition root
|
||||
--- recorder/record refs ---
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecord.java:19:public record CompletionUnknownRecord(
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecord.java:28: public CompletionUnknownRecord {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecord.java:41: public static CompletionUnknownRecord from(
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecord.java:44: return new CompletionUnknownRecord(
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecorder.java:13:public interface CompletionUnknownRecorder {
|
||||
--- metric implementation/calls ---
|
||||
docs/jpa/observability.md:23:| `jpa.transaction.completion.unknown` | its own counter, deliberately |
|
||||
docs/jpa/runbooks.md:7:**Signal:** `jpa.transaction.completion.unknown` incremented; a `CompletionUnknownRecord` in the
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaTransactionObservation.java:19:public final class JpaTransactionObservation {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaTransactionObservation.java:31: public static final String COMPLETION_UNKNOWN_METER = "jpa.transaction.completion.unknown";
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaTransactionObservation.java:36: public JpaTransactionObservation(MeterRegistry registry, String persistenceUnit) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaTransactionObservation.java:79: public void recordCompletionUnknown(PersistenceOperationName operation) {
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaObservabilityAutoConfiguration.java:7:import dev.caskeleton.adapter.outbound.persistence.observation.JpaTransactionObservation;
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaObservabilityAutoConfiguration.java:49: public Optional<JpaTransactionObservation> transactionObservation() {
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaObservabilityAutoConfiguration.java:50: return registry.map(meters -> new JpaTransactionObservation(meters, persistenceUnit));
|
||||
--- observability composition refs ---
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaObservabilityAutoConfiguration.java:26:public final class JpaObservabilityAutoConfiguration {
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaObservabilityAutoConfiguration.java:35: public JpaObservabilityAutoConfiguration(
|
||||
--- application indeterminate path ---
|
||||
T value;
|
||||
try {
|
||||
value = action.get();
|
||||
} catch (RuntimeException actionFailure) {
|
||||
return new AttemptResult<>(
|
||||
rollback(status, request.operationId(), tracker, actionFailure), physicalOwner);
|
||||
}
|
||||
|
||||
tracker.observe(TransactionPhase.COMMIT_REQUESTED);
|
||||
try {
|
||||
transactionManager.commit(status);
|
||||
} catch (RuntimeException commitFailure) {
|
||||
if (sentinel.commitAcknowledged()) {
|
||||
return new AttemptResult<>(
|
||||
new TransactionResult.CommittedWithPostCommitFailure<>(
|
||||
value, request.operationId(), commitFailure),
|
||||
physicalOwner);
|
||||
}
|
||||
if (sentinel.rolledBack()
|
||||
|| commitFailure instanceof UnexpectedRollbackException
|
||||
|| TransactionRetryClassifier.isReplayCandidate(commitFailure)) {
|
||||
return new AttemptResult<>(
|
||||
new TransactionResult.DeterminateRollback<>(translate(commitFailure)), physicalOwner);
|
||||
}
|
||||
return new AttemptResult<>(
|
||||
new TransactionResult.Indeterminate<>(
|
||||
request.operationId(), tracker.lastObserved(), Optional.empty()),
|
||||
physicalOwner);
|
||||
}
|
||||
|
||||
if (!physicalOwner) {
|
||||
return new AttemptResult<>(new TransactionResult.Participating<>(value), false);
|
||||
}
|
||||
tracker.observe(TransactionPhase.COMMIT_ACKED);
|
||||
return new AttemptResult<>(
|
||||
new TransactionResult.Committed<>(value, request.operationId()), true);
|
||||
}
|
||||
|
||||
private boolean shouldRetry(
|
||||
TransactionPolicyId policyId, AttemptResult<?> attemptResult, int attempt) {
|
||||
if (policyId != TransactionPolicyId.COMMAND_SERIALIZABLE_REPLAY_SAFE
|
||||
|| !attemptResult.physicalOwner()
|
||||
|| attempt >= retryBackoff.maximumAttempts()
|
||||
|| Thread.currentThread().isInterrupted()) {
|
||||
return false;
|
||||
}
|
||||
TransactionResult<?> result = attemptResult.result();
|
||||
if (result instanceof TransactionResult.DeterminateRollback<?> rollback) {
|
||||
return TransactionRetryClassifier.isReplayCandidate(rollback.failure());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private <T> TransactionResult<T> rollback(
|
||||
TransactionStatus status,
|
||||
Optional<OperationId> operationId,
|
||||
PhaseTracker tracker,
|
||||
RuntimeException actionFailure) {
|
||||
try {
|
||||
transactionManager.rollback(status);
|
||||
return new TransactionResult.DeterminateRollback<>(translate(actionFailure));
|
||||
} catch (RuntimeException rollbackFailure) {
|
||||
actionFailure.addSuppressed(rollbackFailure);
|
||||
return new TransactionResult.Indeterminate<>(
|
||||
operationId, tracker.lastObserved(), Optional.empty());
|
||||
}
|
||||
}
|
||||
|
||||
private RuntimeException translate(RuntimeException failure) {
|
||||
return exceptionTranslator.translate(failure).map(RuntimeException.class::cast).orElse(failure);
|
||||
}
|
||||
|
||||
private DefaultTransactionDefinition definition(
|
||||
TransactionPolicyId policyId, TransactionStartBudget startBudget, PolicySpec policy) {
|
||||
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
|
||||
definition.setName("application-" + policyId.name().toLowerCase(Locale.ROOT));
|
||||
definition.setPropagationBehavior(policy.propagation());
|
||||
definition.setIsolationLevel(policy.isolation());
|
||||
definition.setReadOnly(policy.readOnly());
|
||||
definition.setTimeout(startBudget.springTimeoutSeconds());
|
||||
return definition;
|
||||
|
||||
Exit code: 0
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
Command: echo '=== evidence manager ==='; git log --all --oneline --follow -- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java; echo '=== evidence manager construction symbol ==='; git log --all --oneline -S'EvidenceAwareJpaTransactionManager.standard' -- src; echo '=== completion recorder ==='; git log --all --oneline --follow -- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecorder.java; echo '=== coordinator ==='; git log --all --oneline --follow -- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java; echo '=== canonical PolicyTransactionPort phrase ==='; git log --all --oneline -S'PolicyTransactionPort.inTransaction' -- src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T08:01:16Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Repository history for evidence manager, completion recorder, coordinator, and the later PolicyTransactionPort canonical-boundary statement. Commit history establishes evolution but not unstated intent.
|
||||
--- stdout/stderr ---
|
||||
=== evidence manager ===
|
||||
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
|
||||
0e61f86 feat(jpa): implement the JPA relational persistence platform
|
||||
=== evidence manager construction symbol ===
|
||||
0e61f86 feat(jpa): implement the JPA relational persistence platform
|
||||
=== completion recorder ===
|
||||
0e61f86 feat(jpa): implement the JPA relational persistence platform
|
||||
=== coordinator ===
|
||||
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
|
||||
0e61f86 feat(jpa): implement the JPA relational persistence platform
|
||||
=== canonical PolicyTransactionPort phrase ===
|
||||
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
|
||||
|
||||
Exit code: 0
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
Command: java source-file probe with current compiled persistence/application/shared classes and Spring transaction dependencies
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T08:01:29Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Supplies a custom JpaRetryPolicy that always fails and counts invocations to FullTransactionRetryCoordinator, then executes a valid TransactionProfile with its mandatory RetryProfile. This isolates whether the application-supplied policy participates in valid coordinator execution; it does not model a real database.
|
||||
--- stdout/stderr ---
|
||||
thrown=SERIALIZATION_FAILURE
|
||||
customPolicyCalls=0
|
||||
workCalls=2
|
||||
|
||||
Exit code: 0
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import dev.caskeleton.adapter.outbound.persistence.api.PersistenceOperationName;
|
||||
import dev.caskeleton.adapter.outbound.persistence.api.error.JpaFailureContext;
|
||||
import dev.caskeleton.adapter.outbound.persistence.api.error.SerializationFailureException;
|
||||
import dev.caskeleton.adapter.outbound.persistence.api.transaction.*;
|
||||
import dev.caskeleton.adapter.outbound.persistence.transaction.*;
|
||||
import java.time.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.springframework.transaction.*;
|
||||
import org.springframework.transaction.support.SimpleTransactionStatus;
|
||||
|
||||
public class JpaCustomRetryPolicyProbe {
|
||||
public static void main(String[] args) {
|
||||
AtomicInteger customPolicyCalls = new AtomicInteger();
|
||||
AtomicInteger workCalls = new AtomicInteger();
|
||||
PlatformTransactionManager tm = new PlatformTransactionManager() {
|
||||
public TransactionStatus getTransaction(TransactionDefinition d) { return new SimpleTransactionStatus(true); }
|
||||
public void commit(TransactionStatus s) {}
|
||||
public void rollback(TransactionStatus s) {}
|
||||
};
|
||||
Clock clock = Clock.fixed(Instant.parse("2026-08-29T00:00:00Z"), ZoneOffset.UTC);
|
||||
SpringJpaTransactionExecutor executor = new SpringJpaTransactionExecutor(tm, clock);
|
||||
JpaRetryPolicy custom = (failure, attempt) -> {
|
||||
customPolicyCalls.incrementAndGet();
|
||||
return RetryDecision.fail("custom-policy-called");
|
||||
};
|
||||
FullTransactionRetryCoordinator coordinator = new FullTransactionRetryCoordinator(
|
||||
executor, custom, ignored -> {}, clock, Duration.ofSeconds(30), RetryEventListener.noop());
|
||||
RetryProfile retry = RetryProfile.boundedContention("probe.write", 2);
|
||||
TransactionProfile profile = TransactionProfile.write("probe.write", Duration.ofSeconds(5)).withRetryProfile(retry);
|
||||
PersistenceOperationName operation = new PersistenceOperationName("probe.write");
|
||||
try {
|
||||
coordinator.execute(operation, profile, () -> {
|
||||
int call = workCalls.incrementAndGet();
|
||||
throw new SerializationFailureException(
|
||||
JpaFailureContext.retryable(operation, "40001", call, Duration.ZERO, null));
|
||||
});
|
||||
} catch (SerializationFailureException expected) {
|
||||
System.out.println("thrown=" + expected.category());
|
||||
}
|
||||
System.out.println("customPolicyCalls=" + customPolicyCalls.get());
|
||||
System.out.println("workCalls=" + workCalls.get());
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
Command: ./gradlew :adapter:outbound:persistence-jpa:test --tests 'dev.caskeleton.adapter.outbound.persistence.transaction.*' --tests 'dev.caskeleton.adapter.outbound.persistence.failure.*' --rerun-tasks --console=plain
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template/src
|
||||
Executed at: 2026-08-29T08:01:40Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Fresh execution of all dedicated transaction and failure package tests in the persistence-jpa leaf; it is unit/focused evidence and does not reproduce a real database commit acknowledgement loss.
|
||||
--- stdout/stderr ---
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
> Task :shared-contract:compileJava
|
||||
> Task :shared-contract:processResources
|
||||
> Task :shared-contract:classes
|
||||
> Task :shared-contract:jar
|
||||
> Task :application-core:compileJava
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes
|
||||
> Task :application-core:jar
|
||||
> Task :adapter:outbound:persistence-jpa:compileJava
|
||||
> Task :adapter:outbound:persistence-jpa:processResources
|
||||
> Task :adapter:outbound:persistence-jpa:classes
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testClasses
|
||||
> Task :adapter:outbound:persistence-jpa:test
|
||||
|
||||
BUILD SUCCESSFUL in 26s
|
||||
18 actionable tasks: 18 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
Exit code: 0
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
Command: ./gradlew :app-bootstrap:test --tests dev.caskeleton.bootstrap.autoconfigure.jpa.JpaPlatformAddonAssemblyTest --tests dev.caskeleton.bootstrap.autoconfigure.jpa.JpaPlatformRuntimeAutoConfigurationTest --tests dev.caskeleton.bootstrap.autoconfigure.persistencejpa.CapabilityEntityScanRegistrationTest --rerun-tasks --console=plain
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template/src
|
||||
Executed at: 2026-08-29T08:02:07Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Fresh app-bootstrap tests for JPA add-on assembly, runtime executor/coordinator composition, and persistence root import/entity-scan registration. These are composition tests, not a real PostgreSQL lost-ack qualification.
|
||||
--- stdout/stderr ---
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
> Task :shared-contract:compileJava
|
||||
> Task :shared-contract:processResources
|
||||
> Task :shared-contract:classes
|
||||
> Task :shared-contract:jar
|
||||
> Task :application-core:compileJava
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes
|
||||
> Task :application-core:jar
|
||||
> Task :domain-core:compileJava
|
||||
> Task :domain-core:processResources NO-SOURCE
|
||||
> Task :domain-core:classes
|
||||
> Task :domain-core:jar
|
||||
> Task :messaging:messaging-core-api:compileJava
|
||||
> Task :messaging:messaging-observability:compileJava
|
||||
> Task :messaging:messaging-schema-api:compileJava
|
||||
> Task :messaging:messaging-policy:compileJava
|
||||
> Task :messaging:messaging-reliability-api:compileJava
|
||||
> Task :messaging:messaging-security:compileJava
|
||||
> Task :messaging:messaging-admin-api:compileJava
|
||||
> Task :messaging:messaging-transport-spi:compileJava
|
||||
> Task :messaging:messaging-admin-runtime:compileJava
|
||||
> Task :messaging:messaging-claim-check:compileJava
|
||||
> Task :messaging:messaging-cloudevents:compileJava
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:compileJava
|
||||
> Task :messaging:messaging-kafka:compileJava
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:compileJava
|
||||
> Task :messaging:messaging-rabbit:compileJava
|
||||
> Task :messaging:messaging-runtime-core:compileJava
|
||||
> Task :messaging:messaging-schema-json:compileJava
|
||||
> Task :messaging:messaging-spring-boot-starter:compileJava
|
||||
> Task :adapter:inbound:graphql:compileJava
|
||||
> Task :adapter:inbound:graphql:processResources
|
||||
> Task :adapter:inbound:graphql:classes
|
||||
> Task :adapter:inbound:graphql:jar
|
||||
> Task :adapter:inbound:web:compileJava
|
||||
> Task :adapter:inbound:web:processResources
|
||||
> Task :adapter:inbound:web:classes
|
||||
> Task :adapter:inbound:web:jar
|
||||
> Task :adapter:outbound:support:compileJava
|
||||
> Task :adapter:outbound:support:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:support:classes
|
||||
> Task :adapter:outbound:support:jar
|
||||
> Task :adapter:outbound:cache-redis:compileJava
|
||||
> Task :adapter:outbound:cache-redis:processResources
|
||||
> Task :adapter:outbound:cache-redis:classes
|
||||
> Task :adapter:outbound:cache-redis:jar
|
||||
> Task :adapter:outbound:fileserver:compileJava
|
||||
> Task :adapter:outbound:fileserver:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:fileserver:classes
|
||||
> Task :adapter:outbound:fileserver:jar
|
||||
> Task :adapter:outbound:httpclient:compileJava
|
||||
> Task :adapter:outbound:httpclient:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:httpclient:classes
|
||||
> Task :adapter:outbound:httpclient:jar
|
||||
> Task :adapter:outbound:identifier:compileJava
|
||||
> Task :adapter:outbound:identifier:compileGroovy NO-SOURCE
|
||||
> Task :adapter:outbound:identifier:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:identifier:classes
|
||||
> Task :adapter:outbound:identifier:jar
|
||||
> Task :adapter:outbound:messaging:compileJava
|
||||
> Task :adapter:outbound:messaging:processResources
|
||||
> Task :adapter:outbound:messaging:classes
|
||||
> Task :adapter:outbound:messaging:jar
|
||||
> Task :adapter:outbound:notification:compileJava
|
||||
> Task :adapter:outbound:notification:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:notification:classes
|
||||
> Task :adapter:outbound:notification:jar
|
||||
> Task :adapter:outbound:persistence-jpa:compileJava
|
||||
> Task :adapter:outbound:persistence-jpa:processResources
|
||||
> Task :adapter:outbound:persistence-jpa:classes
|
||||
> Task :adapter:outbound:persistence-jpa:jar
|
||||
> Task :adapter:outbound:persistence-mongo:compileJava
|
||||
> Task :adapter:outbound:persistence-mongo:processResources
|
||||
> Task :adapter:outbound:persistence-mongo:classes
|
||||
> Task :adapter:outbound:persistence-mongo:jar
|
||||
> Task :app-bootstrap:compileJava
|
||||
> Task :app-bootstrap:processResources
|
||||
> Task :app-bootstrap:classes
|
||||
> Task :sample-portfolio:compileJava
|
||||
> Task :sample-portfolio:processResources
|
||||
> Task :sample-portfolio:classes
|
||||
> Task :sample-portfolio:jar
|
||||
> Task :adapter:inbound:web:compileTestkitJava
|
||||
> Task :adapter:inbound:web:processTestkitResources
|
||||
> Task :adapter:inbound:web:testkitClasses
|
||||
> Task :adapter:inbound:web:testkitJar
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses
|
||||
> Task :adapter:outbound:persistence-jpa:testkitJar
|
||||
> Task :app-bootstrap:compileTestJava
|
||||
> Task :app-bootstrap:runtimeClasspathManifest
|
||||
> Task :app-bootstrap:processTestResources
|
||||
> Task :app-bootstrap:testClasses
|
||||
> Task :messaging:messaging-admin-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-admin-api:classes
|
||||
> Task :messaging:messaging-admin-api:jar
|
||||
> Task :messaging:messaging-admin-runtime:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-admin-runtime:classes
|
||||
> Task :messaging:messaging-admin-runtime:jar
|
||||
> Task :messaging:messaging-claim-check:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-claim-check:classes
|
||||
> Task :messaging:messaging-claim-check:jar
|
||||
> Task :messaging:messaging-cloudevents:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-cloudevents:classes
|
||||
> Task :messaging:messaging-cloudevents:jar
|
||||
> Task :messaging:messaging-core-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-core-api:classes
|
||||
> Task :messaging:messaging-core-api:jar
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:processResources
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:classes
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:jar
|
||||
> Task :messaging:messaging-kafka:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-kafka:classes
|
||||
> Task :messaging:messaging-kafka:jar
|
||||
> Task :messaging:messaging-observability:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-observability:classes
|
||||
> Task :messaging:messaging-observability:jar
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:processResources
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:classes
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:jar
|
||||
> Task :messaging:messaging-policy:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-policy:classes
|
||||
> Task :messaging:messaging-policy:jar
|
||||
> Task :messaging:messaging-rabbit:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-rabbit:classes
|
||||
> Task :messaging:messaging-rabbit:jar
|
||||
> Task :messaging:messaging-reliability-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-reliability-api:classes
|
||||
> Task :messaging:messaging-reliability-api:jar
|
||||
> Task :messaging:messaging-runtime-core:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-runtime-core:classes
|
||||
> Task :messaging:messaging-runtime-core:jar
|
||||
> Task :messaging:messaging-schema-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-schema-api:classes
|
||||
> Task :messaging:messaging-schema-api:jar
|
||||
> Task :messaging:messaging-schema-json:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-schema-json:classes
|
||||
> Task :messaging:messaging-schema-json:jar
|
||||
> Task :messaging:messaging-security:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-security:classes
|
||||
> Task :messaging:messaging-security:jar
|
||||
> Task :messaging:messaging-spring-boot-starter:processResources
|
||||
> Task :messaging:messaging-spring-boot-starter:classes
|
||||
> Task :messaging:messaging-spring-boot-starter:jar
|
||||
> Task :messaging:messaging-transport-spi:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-transport-spi:classes
|
||||
> Task :messaging:messaging-transport-spi:jar
|
||||
> Task :adapter:outbound:objectstorage:compileJava
|
||||
> Task :adapter:outbound:objectstorage:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:objectstorage:classes
|
||||
> Task :adapter:outbound:objectstorage:jar
|
||||
> Task :app-bootstrap:test
|
||||
|
||||
BUILD SUCCESSFUL in 1m 32s
|
||||
98 actionable tasks: 98 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
Exit code: 0
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
Command: java source-file probe using a fake PlatformTransactionManager whose commit throws SQLSTATE 08006, current SpringJpaTransactionExecutor, and current PostgreSQL vendor translator
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T08:04:09Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Reproduces classification when commit acknowledgement is lost through a normal PlatformTransactionManager plus current vendor translator. It does not simulate a real PostgreSQL socket loss or prove the database actually committed.
|
||||
--- stdout/stderr ---
|
||||
type=ConnectionUnavailableException
|
||||
category=CONNECTION_UNAVAILABLE
|
||||
completionUnknown=false
|
||||
retryable=false
|
||||
|
||||
Exit code: 0
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import dev.caskeleton.adapter.outbound.persistence.api.PersistenceOperationName;
|
||||
import dev.caskeleton.adapter.outbound.persistence.api.error.JpaPersistenceException;
|
||||
import dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionProfile;
|
||||
import dev.caskeleton.adapter.outbound.persistence.api.error.ConstraintCode;
|
||||
import dev.caskeleton.adapter.outbound.persistence.postgresql.error.PostgreSqlExceptionTranslator;
|
||||
import dev.caskeleton.adapter.outbound.persistence.transaction.OptimisticConflictTranslator;
|
||||
import dev.caskeleton.adapter.outbound.persistence.transaction.PersistenceFailureTranslatorChain;
|
||||
import dev.caskeleton.adapter.outbound.persistence.transaction.SpringJpaTransactionExecutor;
|
||||
import java.sql.SQLException;
|
||||
import java.time.*;
|
||||
import org.springframework.transaction.*;
|
||||
import org.springframework.transaction.support.SimpleTransactionStatus;
|
||||
|
||||
public class JpaCommitAmbiguityProbe {
|
||||
public static void main(String[] args) {
|
||||
PlatformTransactionManager tm = new PlatformTransactionManager() {
|
||||
public TransactionStatus getTransaction(TransactionDefinition d) { return new SimpleTransactionStatus(true); }
|
||||
public void commit(TransactionStatus s) {
|
||||
throw new TransactionSystemException("commit acknowledgement lost", new SQLException("connection lost", "08006"));
|
||||
}
|
||||
public void rollback(TransactionStatus s) {}
|
||||
};
|
||||
var translator = PostgreSqlExceptionTranslator.with(name -> new ConstraintCode("unknown.constraint"));
|
||||
var executor = new SpringJpaTransactionExecutor(
|
||||
tm,
|
||||
Clock.fixed(Instant.parse("2026-08-29T00:00:00Z"), ZoneOffset.UTC),
|
||||
new PersistenceFailureTranslatorChain(OptimisticConflictTranslator.withoutCatalog(), translator));
|
||||
try {
|
||||
executor.execute(
|
||||
new PersistenceOperationName("probe.commit"),
|
||||
TransactionProfile.write("probe.commit", Duration.ofSeconds(5)),
|
||||
() -> "written",
|
||||
1,
|
||||
"reconcile-key-1");
|
||||
} catch (JpaPersistenceException ex) {
|
||||
System.out.println("type=" + ex.getClass().getSimpleName());
|
||||
System.out.println("category=" + ex.category());
|
||||
System.out.println("completionUnknown=" + ex.completionUnknown());
|
||||
System.out.println("retryable=" + ex.retryable());
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
Command: current reachability and history for JpaTransactionConfig / TransactionProfileRegistry / removed declarative retry interceptor
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T08:05:13Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Distinguishes a zero-direct-reference Spring configuration reached by component scan from a zero-production-reference registry left after declarative retry interceptor removal. It does not prove external consumers of non-api public classes.
|
||||
--- stdout/stderr ---
|
||||
=== JpaTransactionConfig current refs ===
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/config/JpaAdapterComponentsConfig.java:51: * same package — {@code JpaTransactionConfig} for {@code JpaTransactionSettings}, {@code
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/JpaTransactionConfig.java:24:public class JpaTransactionConfig {}
|
||||
=== adapter component scan ===
|
||||
* the JDBC one — and was registered by nothing but a test calling {@code ctx.register(...)}. So a
|
||||
* single-instance deployment had no {@code DistributedLockPort} at all, and a multi-instance one
|
||||
* could not start: the composition root's own {@code DistributedLockConfig} asks for a bean
|
||||
* qualified {@code jdbcDistributedLock} that only that configuration declares.
|
||||
*
|
||||
* <p>Each package's {@code @ConfigurationProperties} type is enabled by a configuration inside that
|
||||
* same package — {@code JpaTransactionConfig} for {@code JpaTransactionSettings}, {@code
|
||||
* DistributedLockPersistenceConfig} for {@code LockSettings} — rather than from here. Enabling them
|
||||
* centrally would give {@code config} an edge to {@code lock} and {@code transaction} that the
|
||||
* module map does not grant it, and the map is right: this class knows which packages to scan, not
|
||||
* what is inside them. They need enabling at all because {@code @ConfigurationPropertiesScan}
|
||||
* excludes this tree as deliberately as {@code @ComponentScan} does.
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ComponentScan(
|
||||
basePackages = {
|
||||
"dev.caskeleton.adapter.outbound.persistence.audit",
|
||||
"dev.caskeleton.adapter.outbound.persistence.failure",
|
||||
"dev.caskeleton.adapter.outbound.persistence.idempotency",
|
||||
"dev.caskeleton.adapter.outbound.persistence.lock",
|
||||
"dev.caskeleton.adapter.outbound.persistence.outbox",
|
||||
"dev.caskeleton.adapter.outbound.persistence.transaction"
|
||||
})
|
||||
public class JpaAdapterComponentsConfig {}
|
||||
=== TransactionProfileRegistry current refs ===
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistry.java:16:public final class TransactionProfileRegistry {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistry.java:20: public TransactionProfileRegistry(Map<String, TransactionProfile> profiles) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistry.java:37: public static TransactionProfileRegistry of(TransactionProfile... profiles) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistry.java:44: return new TransactionProfileRegistry(byName);
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistryTest.java:14: private final TransactionProfileRegistry registry =
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistryTest.java:15: TransactionProfileRegistry.of(
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistryTest.java:39: TransactionProfileRegistry.of(
|
||||
=== removed interceptor history ===
|
||||
COMMIT 2f5d2fc21954286213c1474d19935f571ef896ea feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
|
||||
|
||||
D src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/RetryableJpaTransactionInterceptor.java
|
||||
D src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/RetryableJpaTransactionInterceptorTest.java
|
||||
M src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
|
||||
COMMIT 0e61f86eb58512fb117093c359492d2fe2958382 feat(jpa): implement the JPA relational persistence platform
|
||||
|
||||
A src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/RetryableJpaTransactionInterceptor.java
|
||||
A src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/RetryableJpaTransactionInterceptorTest.java
|
||||
A src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
|
||||
|
||||
Exit code: 0
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
Command: ./gradlew :adapter:outbound:persistence-jpa:test --tests 'dev.caskeleton.adapter.outbound.persistence.hibernate.*' --tests 'dev.caskeleton.adapter.outbound.persistence.springdata.*' --tests 'dev.caskeleton.adapter.outbound.persistence.querydsl.*' --rerun-tasks --console=plain
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template/src
|
||||
Executed at: 2026-08-29T08:21:21Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Fresh focused unit tests for the 53-file hibernate/springdata/querydsl bounded sub-scope; does not prove real PostgreSQL behavior or runtime composition.
|
||||
--- stdout/stderr ---
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
> Task :shared-contract:compileJava
|
||||
> Task :shared-contract:processResources
|
||||
> Task :shared-contract:classes
|
||||
> Task :shared-contract:jar
|
||||
> Task :application-core:compileJava
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes
|
||||
> Task :application-core:jar
|
||||
> Task :adapter:outbound:persistence-jpa:compileJava
|
||||
> Task :adapter:outbound:persistence-jpa:processResources
|
||||
> Task :adapter:outbound:persistence-jpa:classes
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testClasses
|
||||
> Task :adapter:outbound:persistence-jpa:test
|
||||
|
||||
BUILD SUCCESSFUL in 23s
|
||||
18 actionable tasks: 18 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
Exit code: 0
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
Command: java source-file probe for BatchExecutionResult.batched, property-access IDENTITY detection, and non-null Specification returning null Predicate
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T08:21:49Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Pure contract/guard probe against current compiled classes; no real database. The Specification lambda is deliberately non-null but returns null Predicate.
|
||||
--- stdout/stderr ---
|
||||
oneJdbcBatch.batched=false
|
||||
propertyIdentityDetected=false
|
||||
propertyIdentityValidation=PASSED
|
||||
nonNullNullPredicateSpecification=PASSED
|
||||
|
||||
Exit code: 0
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import dev.caskeleton.adapter.outbound.persistence.hibernate.batch.*;
|
||||
import dev.caskeleton.adapter.outbound.persistence.springdata.SpecificationPolicy;
|
||||
import jakarta.persistence.*;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
|
||||
public class JpaQueryHibernateBoundaryProbe {
|
||||
static class PropertyIdentityEntity {
|
||||
private Long id;
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
JpaBatchProfile profile = JpaBatchProfile.uniform("probe.batch", 10);
|
||||
BatchExecutionResult oneBatch = new BatchExecutionResult(profile, 10, 1, 1, 1, 10);
|
||||
System.out.println("oneJdbcBatch.batched=" + oneBatch.batched());
|
||||
|
||||
HibernateBatchConfigurationGuard guard = new HibernateBatchConfigurationGuard();
|
||||
System.out.println("propertyIdentityDetected=" + guard.usesIdentityGeneration(PropertyIdentityEntity.class));
|
||||
try {
|
||||
guard.validate(profile, PropertyIdentityEntity.class);
|
||||
System.out.println("propertyIdentityValidation=PASSED");
|
||||
} catch (Exception e) {
|
||||
System.out.println("propertyIdentityValidation=" + e.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
Specification<Object> noPredicate = (root, query, cb) -> null;
|
||||
try {
|
||||
SpecificationPolicy.requireBounded(noPredicate, PageRequest.of(0, 20), null);
|
||||
System.out.println("nonNullNullPredicateSpecification=PASSED");
|
||||
} catch (Exception e) {
|
||||
System.out.println("nonNullNullPredicateSpecification=" + e.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
Command: static reachability/wiring/export/package evidence for hibernate, springdata, querydsl
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T08:22:05Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Tracked production Java/static configuration only. Zero references do not exclude reflection/external adopters; package export rules and test/integration consumers are shown separately.
|
||||
--- stdout/stderr ---
|
||||
=== package denominator ===
|
||||
hibernate main count= 22 loc= 1111
|
||||
hibernate test count= 2 loc= 149
|
||||
springdata main count= 17 loc= 1028
|
||||
springdata test count= 8 loc= 598
|
||||
querydsl main count= 3 loc= 139
|
||||
querydsl test count= 1 loc= 53
|
||||
=== key implementation production refs ===
|
||||
--- NamedStatementInspector
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/NamedStatementInspector.java:20:public final class NamedStatementInspector implements StatementInspector {
|
||||
--- HibernateBatchConfigurationGuard
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/HibernateBatchConfigurationGuard.java:23:public final class HibernateBatchConfigurationGuard {
|
||||
--- HibernateJpaBatchExecutor
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/HibernateJpaBatchExecutor.java:22:public final class HibernateJpaBatchExecutor implements JpaBatchExecutor {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/HibernateJpaBatchExecutor.java:27: public HibernateJpaBatchExecutor(
|
||||
--- JpaBatchProfileRegistry
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/JpaBatchProfileRegistry.java:14:public final class JpaBatchProfileRegistry {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/JpaBatchProfileRegistry.java:18: public JpaBatchProfileRegistry(Map<String, JpaBatchProfile> profiles) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/JpaBatchProfileRegistry.java:35: public static JpaBatchProfileRegistry of(JpaBatchProfile... profiles) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/JpaBatchProfileRegistry.java:42: return new JpaBatchProfileRegistry(byName);
|
||||
--- HibernateBulkDmlExecutor
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/bulk/HibernateBulkDmlExecutor.java:28:public final class HibernateBulkDmlExecutor implements BulkDmlExecutor {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/bulk/HibernateBulkDmlExecutor.java:38: public HibernateBulkDmlExecutor(
|
||||
--- HibernateStatelessSessionRunner
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/stateless/HibernateStatelessSessionRunner.java:23:public final class HibernateStatelessSessionRunner implements StatelessSessionRunner {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/stateless/HibernateStatelessSessionRunner.java:28: public HibernateStatelessSessionRunner(
|
||||
--- EntityGraphCatalog
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/EntityGraphCatalog.java:20:public final class EntityGraphCatalog {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/EntityGraphCatalog.java:25: public EntityGraphCatalog(
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/EntityGraphCatalog.java:40: public static EntityGraphCatalog empty() {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/EntityGraphCatalog.java:41: return new EntityGraphCatalog(Map.of());
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/FetchPlanApplier.java:30: private final EntityGraphCatalog catalog;
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/FetchPlanApplier.java:33: public FetchPlanApplier(EntityGraphCatalog catalog, EntityManager entityManager) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/FetchPlanApplier.java:61: public EntityGraphCatalog catalog() {
|
||||
--- FetchPlanApplier
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/FetchPlanApplier.java:22:public final class FetchPlanApplier {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/FetchPlanApplier.java:33: public FetchPlanApplier(EntityGraphCatalog catalog, EntityManager entityManager) {
|
||||
--- JpaKeysetQuerySupport
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaKeysetQuerySupport.java:20:public final class JpaKeysetQuerySupport {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaKeysetQuerySupport.java:24: public JpaKeysetQuerySupport(KeysetSliceAssembler assembler) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaKeysetQuerySupport.java:29: public static JpaKeysetQuerySupport standard() {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaKeysetQuerySupport.java:30: return new JpaKeysetQuerySupport(new KeysetSliceAssembler());
|
||||
--- JpaRepositoryFragmentSupport
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaRepositoryFragmentSupport.java:24:public abstract class JpaRepositoryFragmentSupport {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaRepositoryFragmentSupport.java:31: protected JpaRepositoryFragmentSupport(EntityManager entityManager) {
|
||||
--- JpaStreamExecutor
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaStreamExecutor.java:27:public final class JpaStreamExecutor {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaStreamExecutor.java:41: public JpaStreamExecutor(QueryObservation observation) {
|
||||
--- KeysetPredicateBuilder
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/KeysetPredicateBuilder.java:32:public final class KeysetPredicateBuilder {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/KeysetPredicateBuilder.java:34: private KeysetPredicateBuilder() {}
|
||||
--- SpecificationPolicy
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/SpecificationPolicy.java:18:public final class SpecificationPolicy {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/SpecificationPolicy.java:23: private SpecificationPolicy() {}
|
||||
--- QuerydslJpaSupport
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/querydsl/QuerydslJpaSupport.java:21:public final class QuerydslJpaSupport {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/querydsl/QuerydslJpaSupport.java:28: public QuerydslJpaSupport(JPAQueryFactory queryFactory) {
|
||||
=== querydsl lock/declarations ===
|
||||
43-com.puppycrawl.tools:checkstyle:13.5.0=checkstyle
|
||||
44-com.querydsl:querydsl-core:5.1.0=compileClasspath,jpaPlatformPerformanceTestCompileClasspath,jpaPlatformPerformanceTestRuntimeClasspath,postgresqlIntegrationTestCompileClasspath,postgresqlIntegrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
|
||||
45:com.querydsl:querydsl-jpa:5.1.0=compileClasspath,jpaPlatformPerformanceTestCompileClasspath,jpaPlatformPerformanceTestRuntimeClasspath,postgresqlIntegrationTestCompileClasspath,postgresqlIntegrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
|
||||
46-com.sun.istack:istack-commons-runtime:4.1.2=jpaPlatformPerformanceTestRuntimeClasspath,postgresqlIntegrationTestRuntimeClasspath,runtimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath
|
||||
47-com.tngtech.archunit:archunit-junit5-api:1.3.0=jpaPlatformPerformanceTestCompileClasspath,jpaPlatformPerformanceTestRuntimeClasspath,postgresqlIntegrationTestCompileClasspath,postgresqlIntegrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
|
||||
69: compileOnly 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
|
||||
72: testImplementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
|
||||
=== statement inspector configuration ===
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/NamedStatementInspector.java:20:public final class NamedStatementInspector implements StatementInspector {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaRepositoryFragmentSupport.java:66: * Runs {@code work} with {@code name} bound as the statement-inspector identity.
|
||||
=== export lists ===
|
||||
/**
|
||||
* The packages a leaf outside this one may import from.
|
||||
*
|
||||
* <p>An export list, because 318 of 324 production files declare a public top-level type and
|
||||
* "public" inside one jar is public to everybody who has the jar. The composition root imported
|
||||
* twenty-two notification implementation types by name, which turned each of them into a
|
||||
* cross-leaf source contract: moving one to another package, or reducing its visibility, broke
|
||||
* the root's compile. The notification wiring is now a facade the leaf owns, and this names what
|
||||
* the rest of the repository may reach for.
|
||||
*
|
||||
* <p>Everything absent from this list is internal. It may still be {@code public} — Java has no
|
||||
* finer granularity inside one jar — but importing it from another leaf is a boundary violation
|
||||
* this test reports, not a compile error the language provides.
|
||||
*/
|
||||
private static final Set<String> EXPORTED_PACKAGES =
|
||||
Set.of(
|
||||
// The contracts other leaves are meant to speak.
|
||||
"api",
|
||||
// Capability configuration facades, each assembling its own capability's internals.
|
||||
"notification.configuration",
|
||||
// Cross-cutting seams a composition root legitimately wires.
|
||||
"transaction",
|
||||
"security",
|
||||
"observation",
|
||||
"migration",
|
||||
"hibernate",
|
||||
"fileserver",
|
||||
"failure",
|
||||
"config");
|
||||
|
||||
private static final JavaClasses PRODUCTION_CLASSES =
|
||||
// Future NoSQL adapter modules (for example adapter-persistence-mongodb) must be added as
|
||||
// sibling modules that implement application/domain ports directly. When such a module exists,
|
||||
// add an ArchUnit rule forbidding jakarta.persistence.., org.hibernate..,
|
||||
// org.springframework.data.jpa.., org.flywaydb.., and
|
||||
// dev.caskeleton.adapter.outbound.persistence.postgresql..
|
||||
// dependencies from that module.
|
||||
|
||||
/**
|
||||
* The composition root reaches only the persistence leaf's exported surface.
|
||||
*
|
||||
* <p>318 of that leaf's 324 production files declare a public top-level type, and {@code public}
|
||||
* inside one jar is public to everyone holding the jar. The root imported twenty-two notification
|
||||
* implementation types by name — every store, every repository, the record mapper — which made
|
||||
* each of them a cross-leaf source contract: moving one to another package, or reducing its
|
||||
* visibility, broke this module's compile. The notification wiring is a facade the leaf owns now,
|
||||
* and this keeps the surface from re-growing one convenient import at a time.
|
||||
*
|
||||
* <p>The list is the leaf's export allowlist, mirrored here because this is the consumer side of
|
||||
* the same boundary. {@code JpaModuleBoundaryTest} owns the definition.
|
||||
*/
|
||||
@ArchTest
|
||||
static final ArchRule BOOTSTRAP_USES_ONLY_THE_PERSISTENCE_EXPORT_SURFACE =
|
||||
noClasses()
|
||||
.that()
|
||||
.resideInAPackage("dev.caskeleton.bootstrap..")
|
||||
.should()
|
||||
.dependOnClassesThat(
|
||||
new com.tngtech.archunit.base.DescribedPredicate<>(
|
||||
"a persistence type outside the exported surface") {
|
||||
private static final java.util.Set<String> EXPORTED =
|
||||
java.util.Set.of(
|
||||
"api",
|
||||
"notification.configuration",
|
||||
"transaction",
|
||||
"security",
|
||||
"observation",
|
||||
"migration",
|
||||
"hibernate",
|
||||
"fileserver",
|
||||
"failure",
|
||||
"config",
|
||||
// The two vendor entry points. A vendor configuration imports the core JPA
|
||||
// config rather than the reverse, so there is no single internal entry the
|
||||
// composition root could gate instead — inverting the import to make one
|
||||
// produced a package cycle. Admitting exactly these two, and no other
|
||||
// vendor
|
||||
// internals, is the narrower price.
|
||||
"postgresql",
|
||||
"h2");
|
||||
private static final String ROOT = "dev.caskeleton.adapter.outbound.persistence.";
|
||||
|
||||
@Override
|
||||
public boolean test(com.tngtech.archunit.core.domain.JavaClass candidate) {
|
||||
String packageName = candidate.getPackageName();
|
||||
if (!packageName.startsWith(ROOT)) {
|
||||
return false;
|
||||
}
|
||||
String remainder = packageName.substring(ROOT.length());
|
||||
return EXPORTED.stream()
|
||||
.noneMatch(
|
||||
exported ->
|
||||
remainder.equals(exported) || remainder.startsWith(exported + "."));
|
||||
}
|
||||
})
|
||||
.as(
|
||||
"app-bootstrap imports only the persistence leaf's exported packages: every other"
|
||||
+ " public type there is internal, and importing one freezes it as a contract.")
|
||||
.allowEmptyShould(true);
|
||||
|
||||
/**
|
||||
* A persistence adapter stores what a use case decided; it does not make the decision.
|
||||
*
|
||||
* <p>{@code JpaNotificationRequestStore} imported {@code NotificationSubmissionService} and
|
||||
|
||||
Exit code: 0
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
Command: ./gradlew :adapter:outbound:persistence-jpa:test --tests dev.caskeleton.adapter.outbound.persistence.JpaModuleBoundaryTest :app-bootstrap:test --tests dev.caskeleton.bootstrap.architecture.CleanArchitectureTest --rerun-tasks --console=plain
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template/src
|
||||
Executed at: 2026-08-29T08:22:20Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Fresh module-package DAG and app-bootstrap persistence export-surface architecture tests; does not prove runtime feature usage.
|
||||
--- stdout/stderr ---
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
> Task :shared-contract:compileJava
|
||||
> Task :shared-contract:processResources
|
||||
> Task :shared-contract:classes
|
||||
> Task :shared-contract:jar
|
||||
> Task :application-core:compileJava
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes
|
||||
> Task :application-core:jar
|
||||
> Task :adapter:outbound:persistence-jpa:compileJava
|
||||
> Task :adapter:outbound:persistence-jpa:processResources
|
||||
> Task :adapter:outbound:persistence-jpa:classes
|
||||
> Task :adapter:outbound:persistence-jpa:jar
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses
|
||||
> Task :adapter:outbound:persistence-jpa:testkitJar
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testClasses
|
||||
> Task :adapter:outbound:persistence-jpa:test
|
||||
> Task :domain-core:compileJava
|
||||
> Task :domain-core:processResources NO-SOURCE
|
||||
> Task :domain-core:classes
|
||||
> Task :domain-core:jar
|
||||
> Task :messaging:messaging-core-api:compileJava
|
||||
> Task :messaging:messaging-observability:compileJava
|
||||
> Task :messaging:messaging-schema-api:compileJava
|
||||
> Task :messaging:messaging-policy:compileJava
|
||||
> Task :messaging:messaging-reliability-api:compileJava
|
||||
> Task :messaging:messaging-security:compileJava
|
||||
> Task :messaging:messaging-admin-api:compileJava
|
||||
> Task :messaging:messaging-transport-spi:compileJava
|
||||
> Task :messaging:messaging-admin-runtime:compileJava
|
||||
> Task :messaging:messaging-claim-check:compileJava
|
||||
> Task :messaging:messaging-cloudevents:compileJava
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:compileJava
|
||||
> Task :messaging:messaging-kafka:compileJava
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:compileJava
|
||||
> Task :messaging:messaging-rabbit:compileJava
|
||||
> Task :messaging:messaging-runtime-core:compileJava
|
||||
> Task :messaging:messaging-schema-json:compileJava
|
||||
> Task :messaging:messaging-spring-boot-starter:compileJava
|
||||
> Task :adapter:inbound:graphql:compileJava
|
||||
> Task :adapter:inbound:graphql:processResources
|
||||
> Task :adapter:inbound:graphql:classes
|
||||
> Task :adapter:inbound:graphql:jar
|
||||
> Task :adapter:inbound:web:compileJava
|
||||
> Task :adapter:inbound:web:processResources
|
||||
> Task :adapter:inbound:web:classes
|
||||
> Task :adapter:inbound:web:jar
|
||||
> Task :adapter:outbound:support:compileJava
|
||||
> Task :adapter:outbound:support:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:support:classes
|
||||
> Task :adapter:outbound:support:jar
|
||||
> Task :adapter:outbound:cache-redis:compileJava
|
||||
> Task :adapter:outbound:cache-redis:processResources
|
||||
> Task :adapter:outbound:cache-redis:classes
|
||||
> Task :adapter:outbound:cache-redis:jar
|
||||
> Task :adapter:outbound:fileserver:compileJava
|
||||
> Task :adapter:outbound:fileserver:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:fileserver:classes
|
||||
> Task :adapter:outbound:fileserver:jar
|
||||
> Task :adapter:outbound:httpclient:compileJava
|
||||
> Task :adapter:outbound:httpclient:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:httpclient:classes
|
||||
> Task :adapter:outbound:httpclient:jar
|
||||
> Task :adapter:outbound:identifier:compileJava
|
||||
> Task :adapter:outbound:identifier:compileGroovy NO-SOURCE
|
||||
> Task :adapter:outbound:identifier:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:identifier:classes
|
||||
> Task :adapter:outbound:identifier:jar
|
||||
> Task :adapter:outbound:messaging:compileJava
|
||||
> Task :adapter:outbound:messaging:processResources
|
||||
> Task :adapter:outbound:messaging:classes
|
||||
> Task :adapter:outbound:messaging:jar
|
||||
> Task :adapter:outbound:notification:compileJava
|
||||
> Task :adapter:outbound:notification:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:notification:classes
|
||||
> Task :adapter:outbound:notification:jar
|
||||
> Task :adapter:outbound:persistence-mongo:compileJava
|
||||
> Task :adapter:outbound:persistence-mongo:processResources
|
||||
> Task :adapter:outbound:persistence-mongo:classes
|
||||
> Task :adapter:outbound:persistence-mongo:jar
|
||||
> Task :app-bootstrap:compileJava
|
||||
> Task :app-bootstrap:processResources
|
||||
> Task :app-bootstrap:classes
|
||||
> Task :sample-portfolio:compileJava
|
||||
> Task :sample-portfolio:processResources
|
||||
> Task :sample-portfolio:classes
|
||||
> Task :sample-portfolio:jar
|
||||
> Task :adapter:inbound:web:compileTestkitJava
|
||||
> Task :adapter:inbound:web:processTestkitResources
|
||||
> Task :adapter:inbound:web:testkitClasses
|
||||
> Task :adapter:inbound:web:testkitJar
|
||||
> Task :app-bootstrap:compileTestJava
|
||||
> Task :app-bootstrap:runtimeClasspathManifest
|
||||
> Task :app-bootstrap:processTestResources
|
||||
> Task :app-bootstrap:testClasses
|
||||
> Task :messaging:messaging-admin-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-admin-api:classes
|
||||
> Task :messaging:messaging-admin-api:jar
|
||||
> Task :messaging:messaging-admin-runtime:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-admin-runtime:classes
|
||||
> Task :messaging:messaging-admin-runtime:jar
|
||||
> Task :messaging:messaging-claim-check:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-claim-check:classes
|
||||
> Task :messaging:messaging-claim-check:jar
|
||||
> Task :messaging:messaging-cloudevents:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-cloudevents:classes
|
||||
> Task :messaging:messaging-cloudevents:jar
|
||||
> Task :messaging:messaging-core-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-core-api:classes
|
||||
> Task :messaging:messaging-core-api:jar
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:processResources
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:classes
|
||||
> Task :messaging:messaging-inbox-jdbc-postgresql:jar
|
||||
> Task :messaging:messaging-kafka:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-kafka:classes
|
||||
> Task :messaging:messaging-kafka:jar
|
||||
> Task :messaging:messaging-observability:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-observability:classes
|
||||
> Task :messaging:messaging-observability:jar
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:processResources
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:classes
|
||||
> Task :messaging:messaging-outbox-jdbc-postgresql:jar
|
||||
> Task :messaging:messaging-policy:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-policy:classes
|
||||
> Task :messaging:messaging-policy:jar
|
||||
> Task :messaging:messaging-rabbit:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-rabbit:classes
|
||||
> Task :messaging:messaging-rabbit:jar
|
||||
> Task :messaging:messaging-reliability-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-reliability-api:classes
|
||||
> Task :messaging:messaging-reliability-api:jar
|
||||
> Task :messaging:messaging-runtime-core:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-runtime-core:classes
|
||||
> Task :messaging:messaging-runtime-core:jar
|
||||
> Task :messaging:messaging-schema-api:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-schema-api:classes
|
||||
> Task :messaging:messaging-schema-api:jar
|
||||
> Task :messaging:messaging-schema-json:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-schema-json:classes
|
||||
> Task :messaging:messaging-schema-json:jar
|
||||
> Task :messaging:messaging-security:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-security:classes
|
||||
> Task :messaging:messaging-security:jar
|
||||
> Task :messaging:messaging-spring-boot-starter:processResources
|
||||
> Task :messaging:messaging-spring-boot-starter:classes
|
||||
> Task :messaging:messaging-spring-boot-starter:jar
|
||||
> Task :messaging:messaging-transport-spi:processResources NO-SOURCE
|
||||
> Task :messaging:messaging-transport-spi:classes
|
||||
> Task :messaging:messaging-transport-spi:jar
|
||||
> Task :adapter:outbound:objectstorage:compileJava
|
||||
> Task :adapter:outbound:objectstorage:processResources NO-SOURCE
|
||||
> Task :adapter:outbound:objectstorage:classes
|
||||
> Task :adapter:outbound:objectstorage:jar
|
||||
> Task :app-bootstrap:test
|
||||
|
||||
BUILD SUCCESSFUL in 1m 48s
|
||||
100 actionable tasks: 100 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
Exit code: 0
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
Command: static release-registry/task-tag/test-assertion provenance for collection-fetch-pagination
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T08:22:42Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Static provenance only. Shows which Gradle tag the registry task selects and what the named collection-fetch test actually asserts; does not execute PostgreSQL.
|
||||
--- stdout/stderr ---
|
||||
=== registry gate ===
|
||||
{
|
||||
"name": "runtime-role-no-ddl",
|
||||
"task": ":adapter:outbound:persistence-jpa:jpaPlatformSecurityTest",
|
||||
"blocking": true
|
||||
},
|
||||
{
|
||||
"name": "collection-fetch-pagination",
|
||||
"task": ":adapter:outbound:persistence-jpa:jpaPlatformQueryPlanTest",
|
||||
"blocking": true
|
||||
}
|
||||
]
|
||||
}
|
||||
=== lane registration ===
|
||||
// leaf's existing Docker-backed source set; the mapping is recorded in
|
||||
// docs/jpa/repository-adaptation.md §3.
|
||||
//
|
||||
// Every lane fails closed. `failOnNoDiscoveredTests` matters more here than usual: a selected lane
|
||||
// that discovers nothing reports success, and a contract suite that silently stopped running is
|
||||
// indistinguishable from one that passes.
|
||||
Closure<Void> registerJpaPlatformLane = { String taskName, String tag, String description ->
|
||||
tasks.register(taskName, Test) {
|
||||
group = 'verification'
|
||||
it.description = description
|
||||
testClassesDirs = sourceSets.postgresqlIntegrationTest.output.classesDirs
|
||||
classpath = sourceSets.postgresqlIntegrationTest.runtimeClasspath
|
||||
useJUnitPlatform {
|
||||
includeTags tag
|
||||
}
|
||||
failOnNoDiscoveredTests = true
|
||||
outputs.upToDateWhen { false }
|
||||
jvmArgs('-Duser.timezone=UTC')
|
||||
// The Stable matrix selection. An unknown or empty value is an error in
|
||||
// PostgreSqlVersion.parseSelection rather than an empty run.
|
||||
systemProperty 'jpa.matrix.versions',
|
||||
(project.findProperty('jpa.matrix.versions') ?: '16').toString()
|
||||
}
|
||||
}
|
||||
|
||||
def jpaPlatformContractTest = registerJpaPlatformLane(
|
||||
'jpaPlatformContractTest',
|
||||
'jpa-contract',
|
||||
'Runs the JPA platform contract suite against real PostgreSQL (design §40).')
|
||||
def jpaPlatformMigrationTest = registerJpaPlatformLane(
|
||||
'jpaPlatformMigrationTest',
|
||||
'jpa-migration',
|
||||
'Runs the Flyway upgrade snapshot scenarios (design §31).')
|
||||
def jpaPlatformFailureTest = registerJpaPlatformLane(
|
||||
'jpaPlatformFailureTest',
|
||||
'jpa-failure',
|
||||
'Reproduces deadlock, serialization, and commit-ambiguity failures (design §39).')
|
||||
def jpaPlatformQueryPlanTest = registerJpaPlatformLane(
|
||||
'jpaPlatformQueryPlanTest',
|
||||
'jpa-queryplan',
|
||||
'Asserts query plan structure and planner estimate error (design §33).')
|
||||
def jpaPlatformSecurityTest = registerJpaPlatformLane(
|
||||
'jpaPlatformSecurityTest',
|
||||
'jpa-security',
|
||||
'Verifies runtime role privileges and search_path safety (design §36).')
|
||||
|
||||
=== tags ===
|
||||
src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/platform/HibernateCollectionFetchPaginationContractTest.java:26:@Tag("jpa-contract")
|
||||
src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/platform/PostgreSqlQueryPlanContractTest.java:27:@Tag("jpa-queryplan")
|
||||
=== collection-fetch test ===
|
||||
package dev.caskeleton.adapter.outbound.persistence.platform;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import dev.caskeleton.adapter.outbound.persistence.hibernate.HibernateStatisticsSnapshot;
|
||||
import dev.caskeleton.adapter.outbound.persistence.testkit.fetch.FetchPaginationExpectation;
|
||||
import dev.caskeleton.adapter.outbound.persistence.testkit.fetch.PagedChild;
|
||||
import dev.caskeleton.adapter.outbound.persistence.testkit.fetch.PagedParent;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* The collection-fetch pagination gate (design §26).
|
||||
*
|
||||
* <p>The failure this exists to catch is in-memory pagination: older providers fetched every
|
||||
* matching parent row and applied the page in Java, logging a warning and returning correct results
|
||||
* while reading the whole table. Correct output, unbounded work, and nothing failing anywhere.
|
||||
*
|
||||
* <p>The assertion is therefore on the <em>generated SQL</em>, not on the returned page size. The
|
||||
* page is identical either way; only the SQL says where the limit was applied.
|
||||
*/
|
||||
@Tag("jpa-contract")
|
||||
class HibernateCollectionFetchPaginationContractTest {
|
||||
|
||||
private static final int PARENTS = 50;
|
||||
private static final int CHILDREN_PER_PARENT = 4;
|
||||
private static final int PAGE_SIZE = 20;
|
||||
|
||||
private static JpaPlatformContractSupport support;
|
||||
private static JpaPlatformEntityManagerSupport jpa;
|
||||
|
||||
@BeforeAll
|
||||
static void startServerAndSeed() {
|
||||
support = JpaPlatformContractSupport.start();
|
||||
jpa = JpaPlatformEntityManagerSupport.open(support, PagedParent.class, PagedChild.class);
|
||||
jpa.inTransactionDo(
|
||||
entityManager -> {
|
||||
for (int parent = 0; parent < PARENTS; parent++) {
|
||||
var paged = new PagedParent("parent-" + parent);
|
||||
for (int child = 0; child < CHILDREN_PER_PARENT; child++) {
|
||||
paged.addChild(new PagedChild("child-" + parent + '-' + child));
|
||||
}
|
||||
entityManager.persist(paged);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void stopServer() {
|
||||
if (jpa != null) {
|
||||
jpa.close();
|
||||
}
|
||||
if (support != null) {
|
||||
support.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a paged collection fetch bounds the parent selection in SQL")
|
||||
void oneCollectionPageIsBoundedInSql() {
|
||||
var expected = FetchPaginationExpectation.hibernate74PostgreSql(PAGE_SIZE);
|
||||
|
||||
List<PagedParent> page =
|
||||
jpa.inTransaction(
|
||||
entityManager ->
|
||||
entityManager
|
||||
.createQuery(
|
||||
"select distinct p from PagedParent p left join fetch p.children"
|
||||
+ " order by p.id",
|
||||
PagedParent.class)
|
||||
.setMaxResults(PAGE_SIZE)
|
||||
.getResultList());
|
||||
|
||||
assertThat(page).hasSizeLessThanOrEqualTo(expected.maxReturnedParents());
|
||||
assertThat(expected.requiresDatabaseLimit()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("the fetched page issues one statement, not one per parent")
|
||||
void pagedFetchIssuesOneStatement() {
|
||||
HibernateStatisticsSnapshot before = jpa.statistics().snapshot();
|
||||
|
||||
jpa.inTransactionDo(
|
||||
entityManager -> {
|
||||
List<PagedParent> page =
|
||||
entityManager
|
||||
.createQuery(
|
||||
"select distinct p from PagedParent p left join fetch p.children"
|
||||
+ " order by p.id",
|
||||
PagedParent.class)
|
||||
.setMaxResults(PAGE_SIZE)
|
||||
.getResultList();
|
||||
page.forEach(parent -> assertThat(parent.children()).isNotNull());
|
||||
});
|
||||
|
||||
HibernateStatisticsSnapshot delta = jpa.statistics().snapshot().minus(before);
|
||||
assertThat(delta.preparedStatements())
|
||||
.as("a join fetch must not degrade into one statement per parent")
|
||||
.isLessThanOrEqualTo(2L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("without a fetch join the same access is an N+1")
|
||||
void withoutFetchJoinTheAccessIsAnNplusOne() {
|
||||
HibernateStatisticsSnapshot before = jpa.statistics().snapshot();
|
||||
|
||||
jpa.inTransactionDo(
|
||||
entityManager -> {
|
||||
List<PagedParent> page =
|
||||
entityManager
|
||||
.createQuery("select p from PagedParent p order by p.id", PagedParent.class)
|
||||
=== query-plan producer test ===
|
||||
package dev.caskeleton.adapter.outbound.persistence.platform;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import dev.caskeleton.adapter.outbound.persistence.testkit.queryplan.NormalizedPlan;
|
||||
import dev.caskeleton.adapter.outbound.persistence.testkit.queryplan.PostgreSqlExplainRunner;
|
||||
import dev.caskeleton.adapter.outbound.persistence.testkit.queryplan.QueryPlanAssertions;
|
||||
import dev.caskeleton.adapter.outbound.persistence.testkit.queryplan.QueryPlanExpectation;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Captures a real plan and asserts on its structure (design §33).
|
||||
*
|
||||
* <p>Structure, not cost: costs and timings differ on every execution and every machine, so a
|
||||
* snapshot including them fails for reasons that have nothing to do with the query. What is stable
|
||||
* is which node types appear, how far the planner's estimate was from reality, and whether a sort
|
||||
* spilled to disk.
|
||||
*/
|
||||
@Tag("jpa-queryplan")
|
||||
class PostgreSqlQueryPlanContractTest {
|
||||
|
||||
private static JpaPlatformContractSupport support;
|
||||
private static PostgreSqlExplainRunner runner;
|
||||
|
||||
private final QueryPlanAssertions assertions = new QueryPlanAssertions();
|
||||
|
||||
@BeforeAll
|
||||
static void startServer() throws SQLException {
|
||||
support = JpaPlatformContractSupport.start();
|
||||
runner = new PostgreSqlExplainRunner(support.dataSource());
|
||||
try (Connection connection = support.connection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
statement.execute("create table plan_row (id bigint primary key, bucket int not null)");
|
||||
statement.execute(
|
||||
"insert into plan_row(id, bucket) select generate_series(1, 5000),"
|
||||
+ " (random() * 10)::int");
|
||||
statement.execute("create index ix_plan_row_bucket on plan_row(bucket)");
|
||||
statement.execute("analyze plan_row");
|
||||
}
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void stopServer() {
|
||||
if (support != null) {
|
||||
support.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an indexed lookup produces a plan whose estimate is close to reality")
|
||||
void indexedLookupHasAccurateEstimate() {
|
||||
NormalizedPlan plan = runner.explain("select id from plan_row where id = ?", 42L);
|
||||
|
||||
assertions.assertMatches(
|
||||
plan, QueryPlanExpectation.estimateOnly(10.0d).withDiskSortForbidden());
|
||||
assertThat(plan.distinctNodeTypes()).isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a forbidden node type is reported with the whole plan")
|
||||
void forbiddenNodeTypeIsReported() {
|
||||
NormalizedPlan plan = runner.explain("select id from plan_row where bucket = ?", 3);
|
||||
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
assertions.assertMatches(
|
||||
plan,
|
||||
QueryPlanExpectation.estimateOnly(10.0d)
|
||||
.forbidding(
|
||||
"Bitmap Heap Scan", "Index Scan", "Seq Scan", "Index Only Scan")))
|
||||
.isInstanceOf(AssertionError.class)
|
||||
.hasMessageContaining("plan ");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("EXPLAIN ANALYZE refuses anything that is not a SELECT")
|
||||
void refusesNonSelect() {
|
||||
assertThatThrownBy(() -> runner.explain("update plan_row set bucket = 1"))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("only SELECT");
|
||||
}
|
||||
}
|
||||
|
||||
Exit code: 0
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
Command: ./gradlew :adapter:outbound:persistence-jpa:jpaPlatformContractTest --tests dev.caskeleton.adapter.outbound.persistence.platform.HibernateJpaBatchExecutorIntegrationTest --tests dev.caskeleton.adapter.outbound.persistence.platform.IdStrategyContractTest --tests dev.caskeleton.adapter.outbound.persistence.platform.HibernateCollectionFetchPaginationContractTest --rerun-tasks --console=plain
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template/src
|
||||
Executed at: 2026-08-29T08:24:42Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Fresh real-PostgreSQL contract execution for batching, ID strategy, and collection-fetch pagination classes. A green result proves only the assertions those tests currently contain; it does not repair the collection-fetch SQL-limit assertion gap.
|
||||
--- stdout/stderr ---
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
> Task :shared-contract:compileJava
|
||||
> Task :shared-contract:processResources
|
||||
> Task :shared-contract:classes
|
||||
> Task :shared-contract:jar
|
||||
> Task :application-core:compileJava
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes
|
||||
> Task :application-core:jar
|
||||
> Task :adapter:outbound:persistence-jpa:compileJava
|
||||
> Task :adapter:outbound:persistence-jpa:processResources
|
||||
> Task :adapter:outbound:persistence-jpa:classes
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses
|
||||
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava
|
||||
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources
|
||||
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses
|
||||
> Task :adapter:outbound:persistence-jpa:jpaPlatformContractTest
|
||||
|
||||
BUILD SUCCESSFUL in 43s
|
||||
19 actionable tasks: 19 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
Exit code: 0
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
Command: ./gradlew :adapter:outbound:persistence-jpa:jpaPlatformQueryPlanTest --rerun-tasks --console=plain
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template/src
|
||||
Executed at: 2026-08-29T08:25:53Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Fresh execution of the exact Gradle task named by the collection-fetch-pagination release registry. Test-result XML is inspected afterward to identify executed classes.
|
||||
--- stdout/stderr ---
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
> Task :shared-contract:compileJava
|
||||
> Task :shared-contract:processResources
|
||||
> Task :shared-contract:classes
|
||||
> Task :shared-contract:jar
|
||||
> Task :application-core:compileJava
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes
|
||||
> Task :application-core:jar
|
||||
> Task :adapter:outbound:persistence-jpa:compileJava
|
||||
> Task :adapter:outbound:persistence-jpa:processResources
|
||||
> Task :adapter:outbound:persistence-jpa:classes
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses
|
||||
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava
|
||||
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources
|
||||
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses
|
||||
> Task :adapter:outbound:persistence-jpa:jpaPlatformQueryPlanTest
|
||||
|
||||
BUILD SUCCESSFUL in 25s
|
||||
19 actionable tasks: 19 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
Exit code: 0
|
||||
--- executed test XML suites ---
|
||||
TEST-dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlQueryPlanContractTest.xml
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
Command: ./gradlew verifyJpaReleaseGateTasks --rerun-tasks --console=plain
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template/src
|
||||
Executed at: 2026-08-29T08:26:28Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Fresh registry-to-Gradle-task validator. It verifies absolute path, project/task existence and Test task type; it does not inspect tag/selectors or semantic coverage.
|
||||
--- stdout/stderr ---
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
|
||||
> Task :verifyJpaReleaseGateTasks
|
||||
verifyJpaReleaseGateTasks: OK — 6 gate task(s) resolve to real Test tasks.
|
||||
|
||||
BUILD SUCCESSFUL in 2s
|
||||
9 actionable tasks: 9 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
Exit code: 0
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
Command: git history for collection-fetch evidence, batch/stream/stateless hardening, keyset/sort changes
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T08:25:25Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Repository history establishes when source/test shapes entered or changed; it does not infer unstated author intent.
|
||||
--- stdout/stderr ---
|
||||
=== collection fetch test ===
|
||||
0e61f86 feat(jpa): implement the JPA relational persistence platform
|
||||
=== collection fetch gate registry ===
|
||||
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
|
||||
=== batch flush-before-clear ===
|
||||
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
|
||||
=== stream policy supplier ===
|
||||
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
|
||||
=== stateless result row cap ===
|
||||
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
|
||||
=== keyset term per-direction ===
|
||||
a24ece9 feat: web, websocket 어댑터 추가 구현
|
||||
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
|
||||
=== sort tie breaker direction ===
|
||||
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
|
||||
|
||||
Exit code: 0
|
||||
+462
@@ -0,0 +1,462 @@
|
||||
Command: javap current Spring Data JPA 4.0.7 Specification and JpaSpecificationExecutor
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T08:27:12Z
|
||||
Source revision: repository a24ece9cf797f7ea647e33bf846b115208ed1ba5; dependency org.springframework.data:spring-data-jpa:4.0.7
|
||||
Observation boundary: Inspects the resolved dependency bytecode. It proves Specification.unrestricted() is a non-null Specification whose toPredicate returns null; it does not execute a repository query.
|
||||
--- stdout/stderr ---
|
||||
Compiled from "Specification.java"
|
||||
public interface org.springframework.data.jpa.domain.Specification<T> extends java.io.Serializable {
|
||||
public static <T> org.springframework.data.jpa.domain.Specification<T> unrestricted();
|
||||
Code:
|
||||
0: invokedynamic #13, 0 // InvokeDynamic #0:toPredicate:()Lorg/springframework/data/jpa/domain/Specification;
|
||||
5: areturn
|
||||
|
||||
public static <T> org.springframework.data.jpa.domain.Specification<T> where(org.springframework.data.jpa.domain.Specification<T>);
|
||||
Code:
|
||||
0: aload_0
|
||||
1: ldc #21 // String Specification must not be null
|
||||
3: invokestatic #23 // Method org/springframework/util/Assert.notNull:(Ljava/lang/Object;Ljava/lang/String;)V
|
||||
6: aload_0
|
||||
7: areturn
|
||||
|
||||
public static <T> org.springframework.data.jpa.domain.Specification<T> where(org.springframework.data.jpa.domain.PredicateSpecification<T>);
|
||||
Code:
|
||||
0: aload_0
|
||||
1: ldc #36 // String PredicateSpecification must not be null
|
||||
3: invokestatic #23 // Method org/springframework/util/Assert.notNull:(Ljava/lang/Object;Ljava/lang/String;)V
|
||||
6: aload_0
|
||||
7: invokedynamic #38, 0 // InvokeDynamic #1:toPredicate:(Lorg/springframework/data/jpa/domain/PredicateSpecification;)Lorg/springframework/data/jpa/domain/Specification;
|
||||
12: areturn
|
||||
|
||||
public default org.springframework.data.jpa.domain.Specification<T> and(org.springframework.data.jpa.domain.Specification<T>);
|
||||
Code:
|
||||
0: aload_1
|
||||
1: ldc #49 // String Other specification must not be null
|
||||
3: invokestatic #23 // Method org/springframework/util/Assert.notNull:(Ljava/lang/Object;Ljava/lang/String;)V
|
||||
6: aload_0
|
||||
7: aload_1
|
||||
8: invokedynamic #51, 0 // InvokeDynamic #2:combine:()Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;
|
||||
13: invokestatic #55 // Method org/springframework/data/jpa/domain/SpecificationComposition.composed:(Lorg/springframework/data/jpa/domain/Specification;Lorg/springframework/data/jpa/domain/Specification;Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;)Lorg/springframework/data/jpa/domain/Specification;
|
||||
16: areturn
|
||||
|
||||
public default org.springframework.data.jpa.domain.Specification<T> and(org.springframework.data.jpa.domain.PredicateSpecification<T>);
|
||||
Code:
|
||||
0: aload_1
|
||||
1: ldc #49 // String Other specification must not be null
|
||||
3: invokestatic #23 // Method org/springframework/util/Assert.notNull:(Ljava/lang/Object;Ljava/lang/String;)V
|
||||
6: aload_0
|
||||
7: aload_1
|
||||
8: invokestatic #64 // InterfaceMethod where:(Lorg/springframework/data/jpa/domain/PredicateSpecification;)Lorg/springframework/data/jpa/domain/Specification;
|
||||
11: invokedynamic #51, 0 // InvokeDynamic #2:combine:()Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;
|
||||
16: invokestatic #55 // Method org/springframework/data/jpa/domain/SpecificationComposition.composed:(Lorg/springframework/data/jpa/domain/Specification;Lorg/springframework/data/jpa/domain/Specification;Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;)Lorg/springframework/data/jpa/domain/Specification;
|
||||
19: areturn
|
||||
|
||||
public default org.springframework.data.jpa.domain.Specification<T> or(org.springframework.data.jpa.domain.Specification<T>);
|
||||
Code:
|
||||
0: aload_1
|
||||
1: ldc #49 // String Other specification must not be null
|
||||
3: invokestatic #23 // Method org/springframework/util/Assert.notNull:(Ljava/lang/Object;Ljava/lang/String;)V
|
||||
6: aload_0
|
||||
7: aload_1
|
||||
8: invokedynamic #67, 0 // InvokeDynamic #3:combine:()Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;
|
||||
13: invokestatic #55 // Method org/springframework/data/jpa/domain/SpecificationComposition.composed:(Lorg/springframework/data/jpa/domain/Specification;Lorg/springframework/data/jpa/domain/Specification;Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;)Lorg/springframework/data/jpa/domain/Specification;
|
||||
16: areturn
|
||||
|
||||
public default org.springframework.data.jpa.domain.Specification<T> or(org.springframework.data.jpa.domain.PredicateSpecification<T>);
|
||||
Code:
|
||||
0: aload_1
|
||||
1: ldc #49 // String Other specification must not be null
|
||||
3: invokestatic #23 // Method org/springframework/util/Assert.notNull:(Ljava/lang/Object;Ljava/lang/String;)V
|
||||
6: aload_0
|
||||
7: aload_1
|
||||
8: invokestatic #64 // InterfaceMethod where:(Lorg/springframework/data/jpa/domain/PredicateSpecification;)Lorg/springframework/data/jpa/domain/Specification;
|
||||
11: invokedynamic #67, 0 // InvokeDynamic #3:combine:()Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;
|
||||
16: invokestatic #55 // Method org/springframework/data/jpa/domain/SpecificationComposition.composed:(Lorg/springframework/data/jpa/domain/Specification;Lorg/springframework/data/jpa/domain/Specification;Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;)Lorg/springframework/data/jpa/domain/Specification;
|
||||
19: areturn
|
||||
|
||||
public static <T> org.springframework.data.jpa.domain.Specification<T> not(org.springframework.data.jpa.domain.Specification<T>);
|
||||
Code:
|
||||
0: aload_0
|
||||
1: ldc #21 // String Specification must not be null
|
||||
3: invokestatic #23 // Method org/springframework/util/Assert.notNull:(Ljava/lang/Object;Ljava/lang/String;)V
|
||||
6: aload_0
|
||||
7: invokedynamic #69, 0 // InvokeDynamic #4:toPredicate:(Lorg/springframework/data/jpa/domain/Specification;)Lorg/springframework/data/jpa/domain/Specification;
|
||||
12: areturn
|
||||
|
||||
public static <T> org.springframework.data.jpa.domain.Specification<T> allOf(org.springframework.data.jpa.domain.Specification<T>...);
|
||||
Code:
|
||||
0: aload_0
|
||||
1: invokestatic #76 // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
|
||||
4: invokestatic #82 // InterfaceMethod allOf:(Ljava/lang/Iterable;)Lorg/springframework/data/jpa/domain/Specification;
|
||||
7: areturn
|
||||
|
||||
public static <T> org.springframework.data.jpa.domain.Specification<T> allOf(java.lang.Iterable<org.springframework.data.jpa.domain.Specification<T>>);
|
||||
Code:
|
||||
0: aload_0
|
||||
1: invokeinterface #88, 1 // InterfaceMethod java/lang/Iterable.spliterator:()Ljava/util/Spliterator;
|
||||
6: iconst_0
|
||||
7: invokestatic #94 // Method java/util/stream/StreamSupport.stream:(Ljava/util/Spliterator;Z)Ljava/util/stream/Stream;
|
||||
10: invokestatic #100 // InterfaceMethod unrestricted:()Lorg/springframework/data/jpa/domain/Specification;
|
||||
13: invokedynamic #102, 0 // InvokeDynamic #5:apply:()Ljava/util/function/BinaryOperator;
|
||||
18: invokeinterface #106, 3 // InterfaceMethod java/util/stream/Stream.reduce:(Ljava/lang/Object;Ljava/util/function/BinaryOperator;)Ljava/lang/Object;
|
||||
23: checkcast #1 // class org/springframework/data/jpa/domain/Specification
|
||||
26: areturn
|
||||
|
||||
public static <T> org.springframework.data.jpa.domain.Specification<T> anyOf(org.springframework.data.jpa.domain.Specification<T>...);
|
||||
Code:
|
||||
0: aload_0
|
||||
1: invokestatic #76 // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
|
||||
4: invokestatic #115 // InterfaceMethod anyOf:(Ljava/lang/Iterable;)Lorg/springframework/data/jpa/domain/Specification;
|
||||
7: areturn
|
||||
|
||||
public static <T> org.springframework.data.jpa.domain.Specification<T> anyOf(java.lang.Iterable<org.springframework.data.jpa.domain.Specification<T>>);
|
||||
Code:
|
||||
0: aload_0
|
||||
1: invokeinterface #88, 1 // InterfaceMethod java/lang/Iterable.spliterator:()Ljava/util/Spliterator;
|
||||
6: iconst_0
|
||||
7: invokestatic #94 // Method java/util/stream/StreamSupport.stream:(Ljava/util/Spliterator;Z)Ljava/util/stream/Stream;
|
||||
10: invokestatic #100 // InterfaceMethod unrestricted:()Lorg/springframework/data/jpa/domain/Specification;
|
||||
13: invokedynamic #117, 0 // InvokeDynamic #6:apply:()Ljava/util/function/BinaryOperator;
|
||||
18: invokeinterface #106, 3 // InterfaceMethod java/util/stream/Stream.reduce:(Ljava/lang/Object;Ljava/util/function/BinaryOperator;)Ljava/lang/Object;
|
||||
23: checkcast #1 // class org/springframework/data/jpa/domain/Specification
|
||||
26: areturn
|
||||
|
||||
public abstract jakarta.persistence.criteria.Predicate toPredicate(jakarta.persistence.criteria.Root<T>, jakarta.persistence.criteria.CriteriaQuery<?>, jakarta.persistence.criteria.CriteriaBuilder);
|
||||
|
||||
private static jakarta.persistence.criteria.Predicate lambda$0(jakarta.persistence.criteria.Root, jakarta.persistence.criteria.CriteriaQuery, jakarta.persistence.criteria.CriteriaBuilder);
|
||||
Code:
|
||||
0: aconst_null
|
||||
1: areturn
|
||||
|
||||
private static jakarta.persistence.criteria.Predicate lambda$1(org.springframework.data.jpa.domain.PredicateSpecification, jakarta.persistence.criteria.Root, jakarta.persistence.criteria.CriteriaQuery, jakarta.persistence.criteria.CriteriaBuilder);
|
||||
Code:
|
||||
0: aload_0
|
||||
1: aload_1
|
||||
2: aload_3
|
||||
3: invokeinterface #134, 3 // InterfaceMethod org/springframework/data/jpa/domain/PredicateSpecification.toPredicate:(Ljakarta/persistence/criteria/From;Ljakarta/persistence/criteria/CriteriaBuilder;)Ljakarta/persistence/criteria/Predicate;
|
||||
8: areturn
|
||||
|
||||
private static jakarta.persistence.criteria.Predicate lambda$6(org.springframework.data.jpa.domain.Specification, jakarta.persistence.criteria.Root, jakarta.persistence.criteria.CriteriaQuery, jakarta.persistence.criteria.CriteriaBuilder);
|
||||
Code:
|
||||
0: aload_0
|
||||
1: aload_1
|
||||
2: aload_2
|
||||
3: aload_3
|
||||
4: invokeinterface #142, 4 // InterfaceMethod toPredicate:(Ljakarta/persistence/criteria/Root;Ljakarta/persistence/criteria/CriteriaQuery;Ljakarta/persistence/criteria/CriteriaBuilder;)Ljakarta/persistence/criteria/Predicate;
|
||||
9: astore 4
|
||||
11: aload 4
|
||||
13: ifnull 27
|
||||
16: aload_3
|
||||
17: aload 4
|
||||
19: invokeinterface #144, 2 // InterfaceMethod jakarta/persistence/criteria/CriteriaBuilder.not:(Ljakarta/persistence/criteria/Expression;)Ljakarta/persistence/criteria/Predicate;
|
||||
24: goto 28
|
||||
27: aconst_null
|
||||
28: areturn
|
||||
|
||||
private static java.lang.Object $deserializeLambda$(java.lang.invoke.SerializedLambda);
|
||||
Code:
|
||||
0: aload_0
|
||||
1: invokevirtual #156 // Method java/lang/invoke/SerializedLambda.getImplMethodName:()Ljava/lang/String;
|
||||
4: astore_1
|
||||
5: iconst_m1
|
||||
6: istore_2
|
||||
7: aload_1
|
||||
8: invokevirtual #162 // Method java/lang/String.hashCode:()I
|
||||
11: lookupswitch { // 5
|
||||
-1647345005: 60
|
||||
-1647345004: 74
|
||||
-1647344999: 144
|
||||
3555: 116
|
||||
96727: 88
|
||||
default: 155
|
||||
}
|
||||
60: aload_1
|
||||
61: ldc #168 // String lambda$0
|
||||
63: invokevirtual #169 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
|
||||
66: ifeq 155
|
||||
69: iconst_0
|
||||
70: istore_2
|
||||
71: goto 155
|
||||
74: aload_1
|
||||
75: ldc #173 // String lambda$1
|
||||
77: invokevirtual #169 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
|
||||
80: ifeq 155
|
||||
83: iconst_1
|
||||
84: istore_2
|
||||
85: goto 155
|
||||
88: aload_1
|
||||
89: ldc #174 // String and
|
||||
91: invokevirtual #169 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
|
||||
94: ifeq 102
|
||||
97: iconst_2
|
||||
98: istore_2
|
||||
99: goto 155
|
||||
102: aload_1
|
||||
103: ldc #174 // String and
|
||||
105: invokevirtual #169 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
|
||||
108: ifeq 155
|
||||
111: iconst_2
|
||||
112: istore_2
|
||||
113: goto 155
|
||||
116: aload_1
|
||||
117: ldc #175 // String or
|
||||
119: invokevirtual #169 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
|
||||
122: ifeq 130
|
||||
125: iconst_3
|
||||
126: istore_2
|
||||
127: goto 155
|
||||
130: aload_1
|
||||
131: ldc #175 // String or
|
||||
133: invokevirtual #169 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
|
||||
136: ifeq 155
|
||||
139: iconst_3
|
||||
140: istore_2
|
||||
141: goto 155
|
||||
144: aload_1
|
||||
145: ldc #176 // String lambda$6
|
||||
147: invokevirtual #169 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
|
||||
150: ifeq 155
|
||||
153: iconst_4
|
||||
154: istore_2
|
||||
155: iload_2
|
||||
156: lookupswitch { // 5
|
||||
0: 208
|
||||
1: 283
|
||||
2: 366
|
||||
3: 516
|
||||
4: 666
|
||||
default: 749
|
||||
}
|
||||
208: aload_0
|
||||
209: invokevirtual #177 // Method java/lang/invoke/SerializedLambda.getImplMethodKind:()I
|
||||
212: bipush 6
|
||||
214: if_icmpne 749
|
||||
217: aload_0
|
||||
218: invokevirtual #180 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceClass:()Ljava/lang/String;
|
||||
221: ldc #183 // String org/springframework/data/jpa/domain/Specification
|
||||
223: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
226: ifeq 749
|
||||
229: aload_0
|
||||
230: invokevirtual #185 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodName:()Ljava/lang/String;
|
||||
233: ldc #188 // String toPredicate
|
||||
235: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
238: ifeq 749
|
||||
241: aload_0
|
||||
242: invokevirtual #189 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodSignature:()Ljava/lang/String;
|
||||
245: ldc #192 // String (Ljakarta/persistence/criteria/Root;Ljakarta/persistence/criteria/CriteriaQuery;Ljakarta/persistence/criteria/CriteriaBuilder;)Ljakarta/persistence/criteria/Predicate;
|
||||
247: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
250: ifeq 749
|
||||
253: aload_0
|
||||
254: invokevirtual #193 // Method java/lang/invoke/SerializedLambda.getImplClass:()Ljava/lang/String;
|
||||
257: ldc #183 // String org/springframework/data/jpa/domain/Specification
|
||||
259: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
262: ifeq 749
|
||||
265: aload_0
|
||||
266: invokevirtual #196 // Method java/lang/invoke/SerializedLambda.getImplMethodSignature:()Ljava/lang/String;
|
||||
269: ldc #192 // String (Ljakarta/persistence/criteria/Root;Ljakarta/persistence/criteria/CriteriaQuery;Ljakarta/persistence/criteria/CriteriaBuilder;)Ljakarta/persistence/criteria/Predicate;
|
||||
271: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
274: ifeq 749
|
||||
277: invokedynamic #13, 0 // InvokeDynamic #0:toPredicate:()Lorg/springframework/data/jpa/domain/Specification;
|
||||
282: areturn
|
||||
283: aload_0
|
||||
284: invokevirtual #177 // Method java/lang/invoke/SerializedLambda.getImplMethodKind:()I
|
||||
287: bipush 6
|
||||
289: if_icmpne 749
|
||||
292: aload_0
|
||||
293: invokevirtual #180 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceClass:()Ljava/lang/String;
|
||||
296: ldc #183 // String org/springframework/data/jpa/domain/Specification
|
||||
298: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
301: ifeq 749
|
||||
304: aload_0
|
||||
305: invokevirtual #185 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodName:()Ljava/lang/String;
|
||||
308: ldc #188 // String toPredicate
|
||||
310: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
313: ifeq 749
|
||||
316: aload_0
|
||||
317: invokevirtual #189 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodSignature:()Ljava/lang/String;
|
||||
320: ldc #192 // String (Ljakarta/persistence/criteria/Root;Ljakarta/persistence/criteria/CriteriaQuery;Ljakarta/persistence/criteria/CriteriaBuilder;)Ljakarta/persistence/criteria/Predicate;
|
||||
322: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
325: ifeq 749
|
||||
328: aload_0
|
||||
329: invokevirtual #193 // Method java/lang/invoke/SerializedLambda.getImplClass:()Ljava/lang/String;
|
||||
332: ldc #183 // String org/springframework/data/jpa/domain/Specification
|
||||
334: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
337: ifeq 749
|
||||
340: aload_0
|
||||
341: invokevirtual #196 // Method java/lang/invoke/SerializedLambda.getImplMethodSignature:()Ljava/lang/String;
|
||||
344: ldc #199 // String (Lorg/springframework/data/jpa/domain/PredicateSpecification;Ljakarta/persistence/criteria/Root;Ljakarta/persistence/criteria/CriteriaQuery;Ljakarta/persistence/criteria/CriteriaBuilder;)Ljakarta/persistence/criteria/Predicate;
|
||||
346: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
349: ifeq 749
|
||||
352: aload_0
|
||||
353: iconst_0
|
||||
354: invokevirtual #200 // Method java/lang/invoke/SerializedLambda.getCapturedArg:(I)Ljava/lang/Object;
|
||||
357: checkcast #135 // class org/springframework/data/jpa/domain/PredicateSpecification
|
||||
360: invokedynamic #38, 0 // InvokeDynamic #1:toPredicate:(Lorg/springframework/data/jpa/domain/PredicateSpecification;)Lorg/springframework/data/jpa/domain/Specification;
|
||||
365: areturn
|
||||
366: aload_0
|
||||
367: invokevirtual #177 // Method java/lang/invoke/SerializedLambda.getImplMethodKind:()I
|
||||
370: bipush 9
|
||||
372: if_icmpne 441
|
||||
375: aload_0
|
||||
376: invokevirtual #180 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceClass:()Ljava/lang/String;
|
||||
379: ldc #204 // String org/springframework/data/jpa/domain/SpecificationComposition$Combiner
|
||||
381: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
384: ifeq 441
|
||||
387: aload_0
|
||||
388: invokevirtual #185 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodName:()Ljava/lang/String;
|
||||
391: ldc #206 // String combine
|
||||
393: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
396: ifeq 441
|
||||
399: aload_0
|
||||
400: invokevirtual #189 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodSignature:()Ljava/lang/String;
|
||||
403: ldc #207 // String (Ljakarta/persistence/criteria/CriteriaBuilder;Ljakarta/persistence/criteria/Predicate;Ljakarta/persistence/criteria/Predicate;)Ljakarta/persistence/criteria/Predicate;
|
||||
405: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
408: ifeq 441
|
||||
411: aload_0
|
||||
412: invokevirtual #193 // Method java/lang/invoke/SerializedLambda.getImplClass:()Ljava/lang/String;
|
||||
415: ldc #209 // String jakarta/persistence/criteria/CriteriaBuilder
|
||||
417: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
420: ifeq 441
|
||||
423: aload_0
|
||||
424: invokevirtual #196 // Method java/lang/invoke/SerializedLambda.getImplMethodSignature:()Ljava/lang/String;
|
||||
427: ldc #210 // String (Ljakarta/persistence/criteria/Expression;Ljakarta/persistence/criteria/Expression;)Ljakarta/persistence/criteria/Predicate;
|
||||
429: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
432: ifeq 441
|
||||
435: invokedynamic #51, 0 // InvokeDynamic #2:combine:()Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;
|
||||
440: areturn
|
||||
441: aload_0
|
||||
442: invokevirtual #177 // Method java/lang/invoke/SerializedLambda.getImplMethodKind:()I
|
||||
445: bipush 9
|
||||
447: if_icmpne 749
|
||||
450: aload_0
|
||||
451: invokevirtual #180 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceClass:()Ljava/lang/String;
|
||||
454: ldc #204 // String org/springframework/data/jpa/domain/SpecificationComposition$Combiner
|
||||
456: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
459: ifeq 749
|
||||
462: aload_0
|
||||
463: invokevirtual #185 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodName:()Ljava/lang/String;
|
||||
466: ldc #206 // String combine
|
||||
468: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
471: ifeq 749
|
||||
474: aload_0
|
||||
475: invokevirtual #189 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodSignature:()Ljava/lang/String;
|
||||
478: ldc #207 // String (Ljakarta/persistence/criteria/CriteriaBuilder;Ljakarta/persistence/criteria/Predicate;Ljakarta/persistence/criteria/Predicate;)Ljakarta/persistence/criteria/Predicate;
|
||||
480: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
483: ifeq 749
|
||||
486: aload_0
|
||||
487: invokevirtual #193 // Method java/lang/invoke/SerializedLambda.getImplClass:()Ljava/lang/String;
|
||||
490: ldc #209 // String jakarta/persistence/criteria/CriteriaBuilder
|
||||
492: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
495: ifeq 749
|
||||
498: aload_0
|
||||
499: invokevirtual #196 // Method java/lang/invoke/SerializedLambda.getImplMethodSignature:()Ljava/lang/String;
|
||||
502: ldc #210 // String (Ljakarta/persistence/criteria/Expression;Ljakarta/persistence/criteria/Expression;)Ljakarta/persistence/criteria/Predicate;
|
||||
504: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
507: ifeq 749
|
||||
510: invokedynamic #51, 0 // InvokeDynamic #2:combine:()Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;
|
||||
515: areturn
|
||||
516: aload_0
|
||||
517: invokevirtual #177 // Method java/lang/invoke/SerializedLambda.getImplMethodKind:()I
|
||||
520: bipush 9
|
||||
522: if_icmpne 591
|
||||
525: aload_0
|
||||
526: invokevirtual #180 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceClass:()Ljava/lang/String;
|
||||
529: ldc #204 // String org/springframework/data/jpa/domain/SpecificationComposition$Combiner
|
||||
531: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
534: ifeq 591
|
||||
537: aload_0
|
||||
538: invokevirtual #185 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodName:()Ljava/lang/String;
|
||||
541: ldc #206 // String combine
|
||||
543: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
546: ifeq 591
|
||||
549: aload_0
|
||||
550: invokevirtual #189 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodSignature:()Ljava/lang/String;
|
||||
553: ldc #207 // String (Ljakarta/persistence/criteria/CriteriaBuilder;Ljakarta/persistence/criteria/Predicate;Ljakarta/persistence/criteria/Predicate;)Ljakarta/persistence/criteria/Predicate;
|
||||
555: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
558: ifeq 591
|
||||
561: aload_0
|
||||
562: invokevirtual #193 // Method java/lang/invoke/SerializedLambda.getImplClass:()Ljava/lang/String;
|
||||
565: ldc #209 // String jakarta/persistence/criteria/CriteriaBuilder
|
||||
567: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
570: ifeq 591
|
||||
573: aload_0
|
||||
574: invokevirtual #196 // Method java/lang/invoke/SerializedLambda.getImplMethodSignature:()Ljava/lang/String;
|
||||
577: ldc #210 // String (Ljakarta/persistence/criteria/Expression;Ljakarta/persistence/criteria/Expression;)Ljakarta/persistence/criteria/Predicate;
|
||||
579: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
582: ifeq 591
|
||||
585: invokedynamic #67, 0 // InvokeDynamic #3:combine:()Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;
|
||||
590: areturn
|
||||
591: aload_0
|
||||
592: invokevirtual #177 // Method java/lang/invoke/SerializedLambda.getImplMethodKind:()I
|
||||
595: bipush 9
|
||||
597: if_icmpne 749
|
||||
600: aload_0
|
||||
601: invokevirtual #180 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceClass:()Ljava/lang/String;
|
||||
604: ldc #204 // String org/springframework/data/jpa/domain/SpecificationComposition$Combiner
|
||||
606: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
609: ifeq 749
|
||||
612: aload_0
|
||||
613: invokevirtual #185 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodName:()Ljava/lang/String;
|
||||
616: ldc #206 // String combine
|
||||
618: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
621: ifeq 749
|
||||
624: aload_0
|
||||
625: invokevirtual #189 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodSignature:()Ljava/lang/String;
|
||||
628: ldc #207 // String (Ljakarta/persistence/criteria/CriteriaBuilder;Ljakarta/persistence/criteria/Predicate;Ljakarta/persistence/criteria/Predicate;)Ljakarta/persistence/criteria/Predicate;
|
||||
630: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
633: ifeq 749
|
||||
636: aload_0
|
||||
637: invokevirtual #193 // Method java/lang/invoke/SerializedLambda.getImplClass:()Ljava/lang/String;
|
||||
640: ldc #209 // String jakarta/persistence/criteria/CriteriaBuilder
|
||||
642: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
645: ifeq 749
|
||||
648: aload_0
|
||||
649: invokevirtual #196 // Method java/lang/invoke/SerializedLambda.getImplMethodSignature:()Ljava/lang/String;
|
||||
652: ldc #210 // String (Ljakarta/persistence/criteria/Expression;Ljakarta/persistence/criteria/Expression;)Ljakarta/persistence/criteria/Predicate;
|
||||
654: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
657: ifeq 749
|
||||
660: invokedynamic #67, 0 // InvokeDynamic #3:combine:()Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;
|
||||
665: areturn
|
||||
666: aload_0
|
||||
667: invokevirtual #177 // Method java/lang/invoke/SerializedLambda.getImplMethodKind:()I
|
||||
670: bipush 6
|
||||
672: if_icmpne 749
|
||||
675: aload_0
|
||||
676: invokevirtual #180 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceClass:()Ljava/lang/String;
|
||||
679: ldc #183 // String org/springframework/data/jpa/domain/Specification
|
||||
681: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
684: ifeq 749
|
||||
687: aload_0
|
||||
688: invokevirtual #185 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodName:()Ljava/lang/String;
|
||||
691: ldc #188 // String toPredicate
|
||||
693: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
696: ifeq 749
|
||||
699: aload_0
|
||||
700: invokevirtual #189 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodSignature:()Ljava/lang/String;
|
||||
703: ldc #192 // String (Ljakarta/persistence/criteria/Root;Ljakarta/persistence/criteria/CriteriaQuery;Ljakarta/persistence/criteria/CriteriaBuilder;)Ljakarta/persistence/criteria/Predicate;
|
||||
705: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
708: ifeq 749
|
||||
711: aload_0
|
||||
712: invokevirtual #193 // Method java/lang/invoke/SerializedLambda.getImplClass:()Ljava/lang/String;
|
||||
715: ldc #183 // String org/springframework/data/jpa/domain/Specification
|
||||
717: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
720: ifeq 749
|
||||
723: aload_0
|
||||
724: invokevirtual #196 // Method java/lang/invoke/SerializedLambda.getImplMethodSignature:()Ljava/lang/String;
|
||||
727: ldc #212 // String (Lorg/springframework/data/jpa/domain/Specification;Ljakarta/persistence/criteria/Root;Ljakarta/persistence/criteria/CriteriaQuery;Ljakarta/persistence/criteria/CriteriaBuilder;)Ljakarta/persistence/criteria/Predicate;
|
||||
729: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
732: ifeq 749
|
||||
735: aload_0
|
||||
736: iconst_0
|
||||
737: invokevirtual #200 // Method java/lang/invoke/SerializedLambda.getCapturedArg:(I)Ljava/lang/Object;
|
||||
740: checkcast #1 // class org/springframework/data/jpa/domain/Specification
|
||||
743: invokedynamic #69, 0 // InvokeDynamic #4:toPredicate:(Lorg/springframework/data/jpa/domain/Specification;)Lorg/springframework/data/jpa/domain/Specification;
|
||||
748: areturn
|
||||
749: new #213 // class java/lang/IllegalArgumentException
|
||||
752: dup
|
||||
753: ldc #215 // String Invalid lambda deserialization
|
||||
755: invokespecial #217 // Method java/lang/IllegalArgumentException."<init>":(Ljava/lang/String;)V
|
||||
758: athrow
|
||||
}
|
||||
|
||||
Exit code: 0
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
Command: compile/run /tmp PostgreSqlReplaySemanticProbe against current postgresqlIntegrationTest resolved runtime classpath
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T08:40:58Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Real PostgreSQL 16 Testcontainers execution of current same-store inbox and polling-outbox adapters. It isolates replay semantics and ownership; adapters are not automatically composed in the shipped application.
|
||||
--- stdout/stderr ---
|
||||
08:40:59.312 [main] INFO org.testcontainers.DockerClientFactory -- Testcontainers version: 2.0.5
|
||||
08:40:59.481 [main] INFO org.testcontainers.dockerclient.DockerClientProviderStrategy -- Loaded org.testcontainers.dockerclient.UnixSocketClientProviderStrategy from ~/.testcontainers.properties, will try it first
|
||||
08:40:59.846 [main] INFO org.testcontainers.dockerclient.DockerClientProviderStrategy -- Found Docker environment with local Unix socket (unix:///var/run/docker.sock)
|
||||
08:40:59.860 [main] INFO org.testcontainers.DockerClientFactory -- Docker host IP address is 172.17.0.1
|
||||
08:40:59.873 [main] INFO org.testcontainers.DockerClientFactory -- Connected to docker:
|
||||
Server Version: 29.6.1
|
||||
API Version: 1.55
|
||||
Operating System: Ubuntu 24.04.4 LTS
|
||||
Total Memory: 32008 MB
|
||||
08:40:59.882 [main] INFO org.testcontainers.images.PullPolicy -- Image pull policy will be performed by: DefaultPullPolicy()
|
||||
08:40:59.884 [main] INFO org.testcontainers.utility.ImageNameSubstitutor -- Image name substitution will be performed by: DefaultImageNameSubstitutor (composite of 'ConfigurationFileImageNameSubstitutor' and 'PrefixingImageNameSubstitutor')
|
||||
08:40:59.934 [main] INFO tc.testcontainers/ryuk:0.14.0 -- Creating container for image: testcontainers/ryuk:0.14.0
|
||||
08:40:59.937 [main] INFO org.testcontainers.utility.RegistryAuthLocator -- Failure when attempting to lookup auth config. Please ignore if you don't have images in an authenticated registry. Details: (dockerImageName: testcontainers/ryuk:0.14.0, configFile: /root/.docker/config.json, configEnv: DOCKER_AUTH_CONFIG). Falling back to docker-java default behaviour. Exception message: Status 404: No config supplied. Checked in order: /root/.docker/config.json (file not found), DOCKER_AUTH_CONFIG (not set)
|
||||
08:41:00.032 [main] INFO tc.testcontainers/ryuk:0.14.0 -- Container testcontainers/ryuk:0.14.0 is starting: faa6ae58a0cc7e71cd7a48899b1577fef3241b725f28c5347a05e97b1e8ee1fe
|
||||
08:41:00.281 [main] INFO tc.testcontainers/ryuk:0.14.0 -- Container testcontainers/ryuk:0.14.0 started in PT0.347332906S
|
||||
08:41:00.285 [main] INFO org.testcontainers.utility.RyukResourceReaper -- Ryuk started - will monitor and terminate Testcontainers containers on JVM exit
|
||||
08:41:00.285 [main] INFO org.testcontainers.DockerClientFactory -- Checking the system...
|
||||
08:41:00.285 [main] INFO org.testcontainers.DockerClientFactory -- ✔︎ Docker server version should be at least 1.6.0
|
||||
08:41:00.287 [main] INFO tc.postgres:16-alpine -- Creating container for image: postgres:16-alpine
|
||||
08:41:00.336 [main] INFO tc.postgres:16-alpine -- Container postgres:16-alpine is starting: 3a87e7587a2e69ac45a40120ad47a9f1ed7739c4a733024ab6306cf5ae018e82
|
||||
08:41:01.497 [main] INFO tc.postgres:16-alpine -- Container postgres:16-alpine started in PT1.210343551S
|
||||
08:41:01.498 [main] INFO tc.postgres:16-alpine -- Container is started (JDBC URL: jdbc:postgresql://172.17.0.1:32777/test?loggerLevel=OFF)
|
||||
08:41:01.507 [main] INFO com.zaxxer.hikari.HikariDataSource -- HikariPool-1 - Starting...
|
||||
08:41:01.639 [main] INFO com.zaxxer.hikari.pool.HikariPool -- HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@452c8a40
|
||||
08:41:01.640 [main] INFO com.zaxxer.hikari.HikariDataSource -- HikariPool-1 - Start completed.
|
||||
08:41:01.855 [main] INFO org.flywaydb.core.FlywayExecutor -- Database: jdbc:postgresql://172.17.0.1:32777/test?loggerLevel=OFF (PostgreSQL 16.15)
|
||||
08:41:01.894 [main] INFO org.flywaydb.core.internal.schemahistory.JdbcTableSchemaHistory -- Schema history table "public"."flyway_schema_history" does not exist yet
|
||||
08:41:01.897 [main] INFO org.flywaydb.core.internal.command.DbValidate -- Successfully validated 9 migrations (execution time 00:00.017s)
|
||||
08:41:01.908 [main] INFO org.flywaydb.core.internal.schemahistory.JdbcTableSchemaHistory -- Creating Schema History table "public"."flyway_schema_history" ...
|
||||
08:41:01.966 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Current version of schema "public": << Empty Schema >>
|
||||
08:41:01.975 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "1 - idempotency record"
|
||||
08:41:01.997 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "3 - outbox event"
|
||||
08:41:02.010 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "4 - int lock"
|
||||
08:41:02.019 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "5 - int lock expired after"
|
||||
08:41:02.028 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "6 - capability schema registry adoption"
|
||||
08:41:02.037 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "9 - widen capability schema stream"
|
||||
08:41:02.043 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "10 - idempotency request hash varchar"
|
||||
08:41:02.060 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "11 - durable operation"
|
||||
08:41:02.075 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "12 - live event log"
|
||||
08:41:02.086 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Successfully applied 9 migrations to schema "public", now at version v12 (execution time 00:00.051s)
|
||||
08:41:02.112 [main] INFO org.flywaydb.core.FlywayExecutor -- Database: jdbc:postgresql://172.17.0.1:32777/test?loggerLevel=OFF (PostgreSQL 16.15)
|
||||
08:41:02.117 [main] INFO org.flywaydb.core.internal.schemahistory.JdbcTableSchemaHistory -- Creating Schema History table "public"."flyway_jpa_core_history" with baseline ...
|
||||
08:41:02.123 [main] INFO org.flywaydb.core.internal.command.DbBaseline -- Successfully baselined schema with version: 0
|
||||
08:41:02.134 [main] INFO org.flywaydb.core.internal.command.DbValidate -- Successfully validated 3 migrations (execution time 00:00.003s)
|
||||
08:41:02.137 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Current version of schema "public": 0
|
||||
08:41:02.140 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "1 - initialize or adopt"
|
||||
08:41:02.145 [main] WARN org.flywaydb.core.internal.sqlscript.DefaultSqlScriptExecutor -- DB: relation "capability_schema_registry" already exists, skipping (SQL State: 42P07 - Error Code: 0)
|
||||
08:41:02.151 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "2 - widen capability schema stream"
|
||||
08:41:02.155 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Successfully applied 2 migrations to schema "public", now at version v2 (execution time 00:00.007s)
|
||||
08:41:02.171 [main] INFO org.flywaydb.core.FlywayExecutor -- Database: jdbc:postgresql://172.17.0.1:32777/test?loggerLevel=OFF (PostgreSQL 16.15)
|
||||
08:41:02.175 [main] INFO org.flywaydb.core.internal.schemahistory.JdbcTableSchemaHistory -- Creating Schema History table "public"."flyway_jpa_inbox_history" with baseline ...
|
||||
08:41:02.181 [main] INFO org.flywaydb.core.internal.command.DbBaseline -- Successfully baselined schema with version: 0
|
||||
08:41:02.190 [main] INFO org.flywaydb.core.internal.command.DbValidate -- Successfully validated 2 migrations (execution time 00:00.002s)
|
||||
08:41:02.193 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Current version of schema "public": 0
|
||||
08:41:02.196 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "1 - initialize same store inbox"
|
||||
08:41:02.206 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Successfully applied 1 migration to schema "public", now at version v1 (execution time 00:00.007s)
|
||||
08:41:02.221 [main] INFO org.flywaydb.core.FlywayExecutor -- Database: jdbc:postgresql://172.17.0.1:32777/test?loggerLevel=OFF (PostgreSQL 16.15)
|
||||
08:41:02.225 [main] INFO org.flywaydb.core.internal.schemahistory.JdbcTableSchemaHistory -- Creating Schema History table "public"."flyway_jpa_outbox_storage_history" with baseline ...
|
||||
08:41:02.230 [main] INFO org.flywaydb.core.internal.command.DbBaseline -- Successfully baselined schema with version: 0
|
||||
08:41:02.239 [main] INFO org.flywaydb.core.internal.command.DbValidate -- Successfully validated 2 migrations (execution time 00:00.002s)
|
||||
08:41:02.241 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Current version of schema "public": 0
|
||||
08:41:02.248 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "1 - initialize or adopt"
|
||||
08:41:02.250 [main] WARN org.flywaydb.core.internal.sqlscript.DefaultSqlScriptExecutor -- DB: relation "outbox_event" already exists, skipping (SQL State: 42P07 - Error Code: 0)
|
||||
08:41:02.250 [main] WARN org.flywaydb.core.internal.sqlscript.DefaultSqlScriptExecutor -- DB: relation "ix_outbox_event_eligible" already exists, skipping (SQL State: 42P07 - Error Code: 0)
|
||||
08:41:02.251 [main] WARN org.flywaydb.core.internal.sqlscript.DefaultSqlScriptExecutor -- DB: relation "ix_outbox_event_aggregate_occurred" already exists, skipping (SQL State: 42P07 - Error Code: 0)
|
||||
08:41:02.251 [main] WARN org.flywaydb.core.internal.sqlscript.DefaultSqlScriptExecutor -- DB: relation "ix_outbox_event_published_occurred" already exists, skipping (SQL State: 42P07 - Error Code: 0)
|
||||
08:41:02.251 [main] WARN org.flywaydb.core.internal.sqlscript.DefaultSqlScriptExecutor -- DB: relation "ix_outbox_event_status_occurred" already exists, skipping (SQL State: 42P07 - Error Code: 0)
|
||||
08:41:02.278 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Successfully applied 1 migration to schema "public", now at version v1 (execution time 00:00.026s)
|
||||
08:41:02.292 [main] INFO org.flywaydb.core.FlywayExecutor -- Database: jdbc:postgresql://172.17.0.1:32777/test?loggerLevel=OFF (PostgreSQL 16.15)
|
||||
08:41:02.295 [main] INFO org.flywaydb.core.internal.schemahistory.JdbcTableSchemaHistory -- Creating Schema History table "public"."flyway_jpa_outbox_polling_history" with baseline ...
|
||||
08:41:02.301 [main] INFO org.flywaydb.core.internal.command.DbBaseline -- Successfully baselined schema with version: 0
|
||||
08:41:02.309 [main] INFO org.flywaydb.core.internal.command.DbValidate -- Successfully validated 2 migrations (execution time 00:00.002s)
|
||||
08:41:02.312 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Current version of schema "public": 0
|
||||
08:41:02.315 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "1 - initialize polling delivery v2"
|
||||
08:41:02.327 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Successfully applied 1 migration to schema "public", now at version v1 (execution time 00:00.009s)
|
||||
inboxForgedReplay.outcome=PROCESSING_STARTED
|
||||
inboxForgedReplay.returnedActualToken=true
|
||||
inboxForgedReplay.returnedForgedToken=false
|
||||
inboxForgedReplay.completeWithReturnedOwner=COMPLETED
|
||||
inboxRetention.first=RETRYABLE
|
||||
inboxRetention.secondDifferentRetention=ALREADY_APPLIED_SAME_OPERATION
|
||||
inboxRetention.remainingHours=1.000
|
||||
outboxRetry.first=RETRY_SCHEDULED
|
||||
outboxRetry.secondDifferentSchedule=ALREADY_APPLIED_SAME_OPERATION
|
||||
outboxRetry.storedEqualsFirst=true
|
||||
outboxRetry.storedEqualsSecond=false
|
||||
08:41:02.439 [main] INFO com.zaxxer.hikari.HikariDataSource -- HikariPool-1 - Shutdown initiated...
|
||||
08:41:02.441 [main] INFO com.zaxxer.hikari.HikariDataSource -- HikariPool-1 - Shutdown completed.
|
||||
|
||||
Exit code: 0
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package dev.caskeleton.adapter.outbound.persistence.readiness;
|
||||
|
||||
import dev.caskeleton.adapter.outbound.persistence.postgresql.inbox.PostgreSqlSameStoreInboxAdapter;
|
||||
import dev.caskeleton.adapter.outbound.persistence.postgresql.outbox.PostgreSqlImmutableOutboxAppendAdapter;
|
||||
import dev.caskeleton.adapter.outbound.persistence.postgresql.outbox.PostgreSqlPollingDeliveryAdapter;
|
||||
import dev.caskeleton.application.inbox.*;
|
||||
import dev.caskeleton.application.outbox.v2.*;
|
||||
import dev.caskeleton.application.transaction.OperationId;
|
||||
import java.time.*;
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
public class PostgreSqlReplaySemanticProbe {
|
||||
public static void main(String[] args) {
|
||||
try (PostgreSqlReadinessSupport postgres = PostgreSqlReadinessSupport.start()) {
|
||||
migrate(postgres, "classpath:db/migration/postgresql", "flyway_schema_history");
|
||||
migrateIndependent(postgres, "classpath:db/migration/jpa/core", "flyway_jpa_core_history", "probe-core");
|
||||
migrateIndependent(postgres, "classpath:db/migration/jpa/inbox", "flyway_jpa_inbox_history", "probe-inbox");
|
||||
migrateIndependent(postgres, "classpath:db/migration/jpa/outbox-storage", "flyway_jpa_outbox_storage_history", "probe-outbox-storage");
|
||||
migrateIndependent(postgres, "classpath:db/migration/jpa/outbox-polling", "flyway_jpa_outbox_polling_history", "probe-outbox-polling");
|
||||
|
||||
JdbcTemplate jdbc = new JdbcTemplate(postgres.dataSource());
|
||||
TransactionTemplate tx = new TransactionTemplate(new DataSourceTransactionManager(postgres.dataSource()));
|
||||
jdbc.update("update capability_schema_registry set lifecycle_state='ACTIVE' where capability_id in ('jpa-inbox-same-store-v1','jpa-outbox-storage-v2','jpa-outbox-polling-delivery-v2')");
|
||||
|
||||
probeInboxForgedReplay(postgres, jdbc, tx);
|
||||
probeInboxRetention(postgres, jdbc, tx);
|
||||
probeOutboxRetrySchedule(postgres, jdbc, tx);
|
||||
}
|
||||
}
|
||||
|
||||
private static void probeInboxForgedReplay(PostgreSqlReadinessSupport postgres, JdbcTemplate jdbc, TransactionTemplate tx) {
|
||||
PostgreSqlSameStoreInboxAdapter inbox = new PostgreSqlSameStoreInboxAdapter(postgres.dataSource());
|
||||
InboxScopeDigest scope = new InboxScopeDigest("c".repeat(64));
|
||||
InboxClaimAttempt attempt = inbox.newClaimAttempt(new OperationId("claim-owner-probe"));
|
||||
InboxOwner claimed = tx.execute(ignored -> ((InboxClaimOutcome.Acquired) inbox.claim(new InboxClaimRequest(scope, "d".repeat(64), attempt, Duration.ofSeconds(30), Duration.ofDays(7)))).owner());
|
||||
OperationId startOp = new OperationId("start-owner-probe");
|
||||
InboxOwner actual = tx.execute(ignored -> inbox.markProcessing(claimed, startOp).owner().orElseThrow());
|
||||
|
||||
InboxOwner forged = new InboxOwner(scope, "forged-owner-token", actual.attempt(), actual.stateRevision(), actual.claimOperationId());
|
||||
InboxOwnerTransition replay = tx.execute(ignored -> inbox.markProcessing(forged, startOp));
|
||||
InboxOwner leaked = replay.owner().orElse(null);
|
||||
System.out.println("inboxForgedReplay.outcome=" + replay.outcome());
|
||||
System.out.println("inboxForgedReplay.returnedActualToken=" + (leaked != null && leaked.ownerToken().equals(actual.ownerToken())));
|
||||
System.out.println("inboxForgedReplay.returnedForgedToken=" + (leaked != null && leaked.ownerToken().equals(forged.ownerToken())));
|
||||
if (leaked != null) {
|
||||
var complete = tx.execute(ignored -> inbox.complete(leaked, new OperationId("complete-via-leaked-owner")));
|
||||
System.out.println("inboxForgedReplay.completeWithReturnedOwner=" + complete);
|
||||
}
|
||||
}
|
||||
|
||||
private static void probeInboxRetention(PostgreSqlReadinessSupport postgres, JdbcTemplate jdbc, TransactionTemplate tx) {
|
||||
PostgreSqlSameStoreInboxAdapter inbox = new PostgreSqlSameStoreInboxAdapter(postgres.dataSource());
|
||||
InboxScopeDigest scope = new InboxScopeDigest("e".repeat(64));
|
||||
InboxClaimAttempt attempt = inbox.newClaimAttempt(new OperationId("claim-retention-probe"));
|
||||
InboxOwner claimed = tx.execute(ignored -> ((InboxClaimOutcome.Acquired) inbox.claim(new InboxClaimRequest(scope, "f".repeat(64), attempt, Duration.ofSeconds(30), Duration.ofDays(7)))).owner());
|
||||
InboxOwner processing = tx.execute(ignored -> inbox.markProcessing(claimed, new OperationId("start-retention-probe")).owner().orElseThrow());
|
||||
OperationId failOp = new OperationId("retry-retention-probe");
|
||||
var first = tx.execute(ignored -> inbox.markRetryable(processing, Duration.ofHours(1), failOp));
|
||||
var second = tx.execute(ignored -> inbox.markRetryable(processing, Duration.ofHours(9), failOp));
|
||||
Double remainingHours = jdbc.queryForObject("select extract(epoch from (retention_until - clock_timestamp())) / 3600.0 from inbox_record_v1 where scope_hash=?", Double.class, scope.value());
|
||||
System.out.println("inboxRetention.first=" + first);
|
||||
System.out.println("inboxRetention.secondDifferentRetention=" + second);
|
||||
System.out.printf("inboxRetention.remainingHours=%.3f%n", remainingHours == null ? -1.0 : remainingHours);
|
||||
}
|
||||
|
||||
private static void probeOutboxRetrySchedule(PostgreSqlReadinessSupport postgres, JdbcTemplate jdbc, TransactionTemplate tx) {
|
||||
PostgreSqlImmutableOutboxAppendAdapter append = new PostgreSqlImmutableOutboxAppendAdapter(postgres.dataSource());
|
||||
PostgreSqlPollingDeliveryAdapter delivery = new PostgreSqlPollingDeliveryAdapter(postgres.dataSource());
|
||||
tx.executeWithoutResult(ignored -> cutover(jdbc));
|
||||
tx.executeWithoutResult(ignored -> append.append(new NewOutboxEventV2(
|
||||
"event-retry-semantic-probe", "WorkLog", "work-log-probe", 1, 0,
|
||||
"WorkLogChanged", 1, "portfolio.events", "work-log-probe", "application/json",
|
||||
"correlation-probe", null, Instant.parse("2026-08-29T00:00:00Z"), "{\"v\":1}")));
|
||||
ClaimedOutboxDelivery claimed = tx.execute(ignored -> delivery.claimBatch(new OutboxDeliveryClaimRequest("portfolio.events", "probe-relay", 1, Duration.ofSeconds(30)))).getFirst();
|
||||
OutboxDeliveryTransition transition = new OutboxDeliveryTransition(claimed.owner(), new OperationId("retry-schedule-probe"));
|
||||
Instant firstAt = Instant.now().plus(Duration.ofHours(1));
|
||||
Instant secondAt = firstAt.plus(Duration.ofHours(8));
|
||||
var first = tx.execute(ignored -> delivery.markRetryable(transition, firstAt, "BROKER.TEMP"));
|
||||
var second = tx.execute(ignored -> delivery.markRetryable(transition, secondAt, "BROKER.TEMP"));
|
||||
OffsetDateTime stored = jdbc.queryForObject("select next_attempt_at from outbox_delivery_v2 where retention_bucket=? and event_id=? and destination=?", OffsetDateTime.class, claimed.owner().retentionBucket(), claimed.owner().eventId(), claimed.owner().destination());
|
||||
System.out.println("outboxRetry.first=" + first);
|
||||
System.out.println("outboxRetry.secondDifferentSchedule=" + second);
|
||||
System.out.println("outboxRetry.storedEqualsFirst=" + (stored != null && Math.abs(Duration.between(firstAt, stored.toInstant()).toMillis()) < 10));
|
||||
System.out.println("outboxRetry.storedEqualsSecond=" + (stored != null && Math.abs(Duration.between(secondAt, stored.toInstant()).toMillis()) < 10));
|
||||
}
|
||||
|
||||
private static void cutover(JdbcTemplate jdbc) {
|
||||
jdbc.queryForObject("select active_epoch from outbox_publication_control_v2 where scope_id='PRIMARY' for update", Long.class);
|
||||
jdbc.update("insert into outbox_publication_cutover_v2(scope_id,active_epoch,previous_epoch,transition_kind,active_authority,legacy_row_count,legacy_pending_count,legacy_digest,schema_manifest_id,external_manifest_id,activated_at) values ('PRIMARY',2,1,'CUTOVER','POLLING_V2',0,0,?,'jpa-outbox-storage-v2-schema-revision-2',null,clock_timestamp())", "0".repeat(64));
|
||||
jdbc.update("update outbox_publication_control_v2 set active_epoch=2,active_authority='POLLING_V2',revision=1,updated_at=clock_timestamp() where scope_id='PRIMARY' and active_epoch=1");
|
||||
}
|
||||
|
||||
private static void migrate(PostgreSqlReadinessSupport postgres, String location, String history) {
|
||||
Flyway.configure().dataSource(postgres.dataSource()).locations(location).table(history).baselineOnMigrate(false).outOfOrder(false).load().migrate();
|
||||
}
|
||||
|
||||
private static void migrateIndependent(PostgreSqlReadinessSupport postgres, String location, String history, String desc) {
|
||||
Flyway f = Flyway.configure().dataSource(postgres.dataSource()).locations(location).table(history).baselineVersion("0").baselineDescription(desc).baselineOnMigrate(false).outOfOrder(false).load();
|
||||
f.baseline();
|
||||
f.migrate();
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
Command: java source-file probe of current PostgreSqlExceptionTranslator with SQLException SQLSTATE 40003
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T08:41:02Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: Direct vendor-translator classification only; no real commit/network failure.
|
||||
--- stdout/stderr ---
|
||||
type=JpaPersistenceException
|
||||
category=UNKNOWN
|
||||
sqlState=40003
|
||||
completionUnknown=false
|
||||
retryable=false
|
||||
|
||||
Exit code: 0
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import dev.caskeleton.adapter.outbound.persistence.api.PersistenceOperationName;
|
||||
import dev.caskeleton.adapter.outbound.persistence.api.error.ConstraintCode;
|
||||
import dev.caskeleton.adapter.outbound.persistence.postgresql.error.PostgreSqlExceptionTranslator;
|
||||
import java.sql.SQLException;
|
||||
import java.time.Duration;
|
||||
|
||||
public class PostgreSql40003Probe {
|
||||
public static void main(String[] args) {
|
||||
var translator = PostgreSqlExceptionTranslator.with(name -> new ConstraintCode("unknown.constraint"));
|
||||
var ex = translator.translate(
|
||||
new SQLException("completion unknown", "40003"),
|
||||
new PersistenceOperationName("probe.40003"), 1, Duration.ZERO, null);
|
||||
System.out.println("type=" + ex.getClass().getSimpleName());
|
||||
System.out.println("category=" + ex.category());
|
||||
System.out.println("sqlState=" + ex.context().sqlState());
|
||||
System.out.println("completionUnknown=" + ex.completionUnknown());
|
||||
System.out.println("retryable=" + ex.retryable());
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
Command: current PostgreSQL vendor denominator/blob/reachability snapshot
|
||||
Working directory: /shared/codebase/clean-architecture-backend-template
|
||||
Executed at: 2026-08-29T08:41:14Z
|
||||
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Observation boundary: 55 production Java + 9 dedicated unit Java + 9 vendor SQL migrations; static production reachability does not prove reflection/external consumers.
|
||||
--- stdout/stderr ---
|
||||
src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql files= 55 java= 55 sql= 0
|
||||
src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql files= 9 java= 9 sql= 0
|
||||
src/main/resources/db/migration/postgresql files= 9 java= 0 sql= 9
|
||||
=== tracked blobs ===
|
||||
e210c55ff2fc7104baafb410518f00d87ff61992 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlIdempotencyClaimRepository.java
|
||||
31a15ceab20d3f917f2245528622d767890c5fa7 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlLocalTimeoutConfigurer.java
|
||||
7ccf5807823732f307b4b78c182464d39a1ab3b0 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlOutboxClaimRepository.java
|
||||
a8d4f2ff53969521d8ab421d9e4199479c8c83a7 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlPersistenceConfig.java
|
||||
f44cea4ab24938198ebd20fc6df25518cf167621 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlSqlStateErrorMapping.java
|
||||
1921e42c5540fc320be91ed71e34d1e292cfca2a src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/array/PostgreSqlArraySupport.java
|
||||
ab72eea155bd448f8256d41f2a4647b405f05394 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintCatalog.java
|
||||
93d5ee3a7fb1dae72ce2404a3435f7402784ebc4 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java
|
||||
0c58eb0767e9b41edb060e0073bf7fb75006d118 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/BoundedCopyInputStream.java
|
||||
4281c693611a6e2f96ce4b5f8943dfbc2556b086 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/CopyAdminCapability.java
|
||||
9e9a897662c5aec230a8e2887d8101265ee32a1c src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/CopyFormat.java
|
||||
e0ca01fceacf915699de854fc425cc2dd5efc45c src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/CopyLimits.java
|
||||
103ed96e08bcafbaddf98cfe13f24d7e0abde162 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/CopyOperationName.java
|
||||
b1bd50a35d9f99d7dc5b6ec3c51c1e0e573b1650 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/CopyResult.java
|
||||
79faba1dfaf12885740a5d3739295020ec38e024 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/PostgreSqlCopyLoader.java
|
||||
caca77feff6b5a2ac3a4d478e42989525c2b4185 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/RegisteredCopyStatement.java
|
||||
fa83d4d721b0875bfb5b904ae010d99d79c660a2 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/RegisteredPostgreSqlCopyLoader.java
|
||||
ca9bfd8ff01dbec0a50c6126a21b70047646e230 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/ConstraintCatalog.java
|
||||
af979cf3dda8e13050c2298cb64986edd16524eb src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
|
||||
24afb0af6c08d914549210af1e5f8ef25ab0dd80 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlFailureClassifier.java
|
||||
bc1e253f5f3ab3799c3320d05d1dee43d89b4d12 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlServerErrorFields.java
|
||||
74675093b7635518e9416b170103cc84e5cfa58d src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlState.java
|
||||
f574bd966aaa62b393ef67b365a832d300d55efc src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyCapabilityGuard.java
|
||||
99688068acee94501b7ebbf4ecf1a9d67dc66018 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyClaimGateway.java
|
||||
f1c5a225ae14283e4d0063372328fac53563d6df src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyDigestPolicy.java
|
||||
b7409d07805e4fc59f831df1109a1f4fd2eeed6c src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyRecordRow.java
|
||||
d4d24a87f6c22e00d3050648914fc2f5d64aa33d src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyRowMapper.java
|
||||
07571331ec0ba0f07661188404b56613e39d577c src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyTransitionGateway.java
|
||||
dad9804ef50eb85adb15005dffeb659443696056 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java
|
||||
13986367170c46671dfd5359ad26cee6681eeca2 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/inbox/PostgreSqlSameStoreInboxAdapter.java
|
||||
83f4f8f808166ee38641af73750b9494940d0469 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/json/JsonDocument.java
|
||||
f4b157f1754ec7d83b01ef0f10959ace5587ac03 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/json/JsonDocumentCodec.java
|
||||
4c5784b9fb827c101d513856309ac8116028edde src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/json/JsonPathName.java
|
||||
f19caae234b6c4207a1e1720ed2bd1f439b0379a src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/json/PostgreSqlJsonQuerySupport.java
|
||||
d4844280cebcf21136bd73ff0ef05f1610c1df93 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/LockWaitObservation.java
|
||||
56e76d1578fbec8c5610488cbbe391a45898f0e9 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlLockExceptionTranslator.java
|
||||
1494b12062d5196956a2b5326c99c318f1837eab src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlLockOptions.java
|
||||
b123cbfcf55988d84073f2c0e3e5a46a6fe191b7 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlWorkClaimExecutor.java
|
||||
43939171c67bd9aab00fbdff8851eeda5a054e1a src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/WorkClaim.java
|
||||
d0b0de2159adf2cdb64b38659b2e38b135b7995f src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/WorkClaimExecutor.java
|
||||
6fbd709b5fd83a86116550340e9916dd9d1cb92e src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/WorkQueueDefinition.java
|
||||
bbebbb03d6a8bd188914512b294c99ec9c35ec16 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/WorkQueueName.java
|
||||
6ad95156dca8483bf6466791cbb3cd707f3a53dc src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlImmutableOutboxAppendAdapter.java
|
||||
fef15d75ab780049138215967704e2f4951130ec src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java
|
||||
501432fcd4eb03a3fc3e378326ffd36b3858bda9 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/range/PgRange.java
|
||||
010a9cf71a3b058799e543abf87c40357211b864 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/range/PgRangeCodec.java
|
||||
a8b05b0349b6bb3f123ab7eb202b13c1a2d66349 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/range/PgRangeJdbcType.java
|
||||
84dbb0b9bca3d00a730b8c9929112716e5df1e75 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/range/PostgreSqlRangeQuerySupport.java
|
||||
4497dd0e3a9ac6b151692796328cac187ed27989 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/write/NativeWriteName.java
|
||||
d31c9342970bdb681916da3cfe334b08ae5afda1 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/write/PostgreSqlUpsertExecutor.java
|
||||
145baeaaaf124ee84d4d543bf525e68a07d49422 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/write/RegisteredPostgreSqlUpsertExecutor.java
|
||||
b307afb6da93242a22963aab4ffed159f4990e26 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/write/RegisteredUpsertStatement.java
|
||||
b58a3688d624fc11d1cf835d58f9bc9a43025b8c src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/write/UpsertConflictTarget.java
|
||||
7ed6a25ba22e71487893f8c71176aee8c7950084 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/write/UpsertResult.java
|
||||
daf7e6191903183de0105396919dd9ae459278b2 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/write/WriteDisposition.java
|
||||
5c00fe377d8dd53bd63d0f65945fcbaa332c5ab1 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V10__idempotency_request_hash_varchar.sql
|
||||
a2058af000d86b2d5161b4dc271492d4912e9e13 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V11__durable_operation.sql
|
||||
9f3a4b53f07839e96b8f22a8a05c6080d9ebc4b7 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V12__live_event_log.sql
|
||||
5976eeae3efedaa7d1cd81c7ea350f2bffc02741 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V1__idempotency_record.sql
|
||||
c35fd5b60340a3add2badb4d11a9d18e9df4323e src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V3__outbox_event.sql
|
||||
c24dea51d458bb69ef0d93b52d0c90dcf5901eb3 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V4__int_lock.sql
|
||||
0901128b59aa9dd6c3731e0a5a1660b589bd0724 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V5__int_lock_expired_after.sql
|
||||
c72e38025f312565542199eebd99013197e194d5 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V6__capability_schema_registry_adoption.sql
|
||||
22e1e0e7b5c3dbc51d1af4aed6a9f93f444307c3 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V9__widen_capability_schema_stream.sql
|
||||
ab1792e640935b97872387f60c37d0ff1e67ccc3 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlLocalTimeoutConfigurerTest.java
|
||||
d81b2f3a90c20bec84ab8036716126d72a5a58d8 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlSqlStateErrorMappingTest.java
|
||||
f56a0ea4ca19b6baa1fa3223f72521346be3e7c9 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlVendorFailureTranslatorRegistrationTest.java
|
||||
aa73ae39894fda06274357dafb9dec4357494bf4 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintCatalogTest.java
|
||||
cf49c214796b840914d15ef7eb55ad44c293d07a src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlFailureClassifierTest.java
|
||||
b1d4eb97523b66b04f78d8ab225d3ef9d76f3e13 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyDigestPolicyTest.java
|
||||
02427be4d365aa6ab9dcdf4160a978f33cbcf172 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/OwnerSafeIdempotencyPreconditionTest.java
|
||||
e1839ffd13c95bbe65d1ac950e1ed751ded6b80c src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlLockOptionsTest.java
|
||||
4059e40802a63d2577c75b78407f4609cc70e98e src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/range/PgRangeTest.java
|
||||
=== key production construction ===
|
||||
--- PostgreSqlOwnerSafeIdempotencyStore
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java:61:public class PostgreSqlOwnerSafeIdempotencyStore implements IdempotencyStorePortV2 {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java:84: public PostgreSqlOwnerSafeIdempotencyStore(JdbcOperations jdbc) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java:88: PostgreSqlOwnerSafeIdempotencyStore(JdbcOperations jdbc, SecureRandom secureRandom) {
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/idempotency/PostgreSqlIdempotencyProviderConfig.java:3:import dev.caskeleton.adapter.outbound.persistence.postgresql.idempotency.PostgreSqlOwnerSafeIdempotencyStore;
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/idempotency/PostgreSqlIdempotencyProviderConfig.java:60: return new PostgreSqlOwnerSafeIdempotencyStore(jdbc);
|
||||
--- PostgreSqlSameStoreInboxAdapter
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/inbox/PostgreSqlSameStoreInboxAdapter.java:49:public class PostgreSqlSameStoreInboxAdapter implements InboxStorePort {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/inbox/PostgreSqlSameStoreInboxAdapter.java:204: public PostgreSqlSameStoreInboxAdapter(DataSource dataSource) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/inbox/PostgreSqlSameStoreInboxAdapter.java:208: PostgreSqlSameStoreInboxAdapter(
|
||||
--- PostgreSqlPollingDeliveryAdapter
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java:44:public class PostgreSqlPollingDeliveryAdapter implements OutboxPollingDeliveryPortV2 {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java:285: public PostgreSqlPollingDeliveryAdapter(DataSource dataSource) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java:289: PostgreSqlPollingDeliveryAdapter(
|
||||
--- PostgreSqlImmutableOutboxAppendAdapter
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlImmutableOutboxAppendAdapter.java:45:public class PostgreSqlImmutableOutboxAppendAdapter implements OutboxAppendPortV2 {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlImmutableOutboxAppendAdapter.java:153: public PostgreSqlImmutableOutboxAppendAdapter(DataSource dataSource) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlImmutableOutboxAppendAdapter.java:157: PostgreSqlImmutableOutboxAppendAdapter(DataSource dataSource, JdbcOperations jdbc) {
|
||||
--- RegisteredPostgreSqlCopyLoader
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/RegisteredPostgreSqlCopyLoader.java:30:public final class RegisteredPostgreSqlCopyLoader implements PostgreSqlCopyLoader {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/RegisteredPostgreSqlCopyLoader.java:41: public RegisteredPostgreSqlCopyLoader(
|
||||
=== current git ===
|
||||
a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
|
||||
Exit code: 0
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
Command: compile current PgRange/PgRangeCodec with isolated RangeProbe and execute
|
||||
Working revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Executed at: 2026-08-29T09:48:56Z
|
||||
Observation boundary: pure codec self-round-trip; no database/provider involved.
|
||||
comma.literal=["left,inside","right")
|
||||
comma.roundTrip=true
|
||||
comma.lower=left,inside
|
||||
quote.literal=["left\"inside","right")
|
||||
quote.error=IllegalArgumentException:postgresql range literal has no endpoint separator
|
||||
quote-comma.literal=["left\",inside","right")
|
||||
quote-comma.error=IllegalArgumentException:range lower bound exceeds upper bound
|
||||
backslash-quote-comma.literal=["left\\\",inside","right")
|
||||
backslash-quote-comma.error=IllegalArgumentException:range lower bound exceeds upper bound
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package dev.caskeleton.adapter.outbound.persistence.postgresql.range;
|
||||
public final class RangeProbe {
|
||||
public static void main(String[] args) {
|
||||
var codec = new PgRangeCodec<String>(s -> s, s -> s);
|
||||
var cases = new String[][] {
|
||||
{"comma", "left,inside", "right"},
|
||||
{"quote", "left\"inside", "right"},
|
||||
{"quote-comma", "left\",inside", "right"},
|
||||
{"backslash-quote-comma", "left\\\",inside", "right"}
|
||||
};
|
||||
for (var c : cases) {
|
||||
var range = PgRange.closedOpen(c[1], c[2]);
|
||||
String literal = codec.format(range);
|
||||
try {
|
||||
var parsed = codec.parse(literal);
|
||||
System.out.println(c[0] + ".literal=" + literal);
|
||||
System.out.println(c[0] + ".roundTrip=" + parsed.equals(range));
|
||||
System.out.println(c[0] + ".lower=" + parsed.lower().orElse("<none>"));
|
||||
} catch (Throwable t) {
|
||||
System.out.println(c[0] + ".literal=" + literal);
|
||||
System.out.println(c[0] + ".error=" + t.getClass().getSimpleName() + ":" + t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
Command: temporary analysis probes inside PostgreSqlIdempotencyIntegrationTest; run exact postgresqlIdempotencyIntegrationTest lane
|
||||
Working revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Executed at: 2026-08-29T09:50:51Z
|
||||
Observation boundary: real PostgreSQL 16 Testcontainers, production PostgreSqlOwnerSafeIdempotencyStore; temporary tests restored after run.
|
||||
--- observations ---
|
||||
expiredInspect.outcome=COMPLETED_REPLAY
|
||||
expiredInspect.replayUntil=2026-08-29T09:50:47.103976Z
|
||||
expiredInspect.claimAfterExpiry=TakenOverClaimed
|
||||
completeReplayTtl.first=COMPLETED
|
||||
completeReplayTtl.secondDifferentTtl=ALREADY_COMPLETED_SAME_RESULT
|
||||
completeReplayTtl.storedSeconds=3600
|
||||
--- gradle tail ---
|
||||
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava
|
||||
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses
|
||||
> Task :adapter:outbound:persistence-jpa:postgresqlIdempotencyIntegrationTest
|
||||
|
||||
BUILD SUCCESSFUL in 19s
|
||||
19 actionable tasks: 2 executed, 17 up-to-date
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
// Injected temporarily into PostgreSqlIdempotencyIntegrationTest for analysis only.
|
||||
@Test
|
||||
void analysisProbeCompleteReplayTtlSemantics() {
|
||||
IdempotencyOwner executing = claimAndStart("claim-complete-ttl-probe", "start-complete-ttl-probe");
|
||||
OperationId operation = new OperationId("complete-ttl-probe");
|
||||
StoredResponse response = new StoredResponse("same-response");
|
||||
IdempotencyCompleteOutcome first = transactions.execute(
|
||||
ignored -> store.complete(executing, response, Duration.ofHours(1), operation));
|
||||
IdempotencyCompleteOutcome differentTtl = transactions.execute(
|
||||
ignored -> store.complete(executing, response, Duration.ofHours(9), operation));
|
||||
Long storedSeconds = jdbc.queryForObject(
|
||||
"select extract(epoch from (replay_until - updated_at))::bigint from idempotency_record where scope_hash = ?",
|
||||
Long.class, SCOPE.digest());
|
||||
System.out.println("completeReplayTtl.first=" + first);
|
||||
System.out.println("completeReplayTtl.secondDifferentTtl=" + differentTtl);
|
||||
System.out.println("completeReplayTtl.storedSeconds=" + storedSeconds);
|
||||
}
|
||||
|
||||
@Test
|
||||
void analysisProbeExpiredCompletedInspection() {
|
||||
IdempotencyClaimAttempt attempt = store.newClaimAttempt(new OperationId("claim-expired-inspect"));
|
||||
IdempotencyClaimOutcome claim = transactions.execute(ignored -> store.claim(request(attempt, Duration.ofSeconds(5))));
|
||||
IdempotencyOwner claimed = ((IdempotencyClaimOutcome.Acquired) claim).owner();
|
||||
IdempotencyOwner executing = transactions.execute(ignored ->
|
||||
store.markExecutionStarted(claimed, new OperationId("start-expired-inspect")).owner().orElseThrow());
|
||||
transactions.execute(ignored -> store.complete(
|
||||
executing, new StoredResponse("expired-response"), Duration.ofMillis(25), new OperationId("complete-expired-inspect")));
|
||||
jdbc.queryForObject("select pg_sleep(0.05)", Object.class);
|
||||
var inspection = store.inspect(new IdempotencyInspectionRequest(SCOPE, FINGERPRINT, attempt));
|
||||
IdempotencyClaimOutcome newClaim = transactions.execute(ignored -> store.claim(request(
|
||||
store.newClaimAttempt(new OperationId("claim-after-expired-inspect")), Duration.ofSeconds(5))));
|
||||
System.out.println("expiredInspect.outcome=" + inspection.outcome());
|
||||
System.out.println("expiredInspect.replayUntil=" + inspection.replayUntil().orElse(null));
|
||||
System.out.println("expiredInspect.claimAfterExpiry=" + newClaim.getClass().getSimpleName());
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
Command: ./gradlew :adapter:outbound:persistence-jpa:test <all 9 dedicated postgresql unit test classes> --rerun-tasks
|
||||
Working revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Executed at: 2026-08-29T09:48:57Z
|
||||
Classes:
|
||||
dev.caskeleton.adapter.outbound.persistence.postgresql.PostgreSqlLocalTimeoutConfigurerTest
|
||||
dev.caskeleton.adapter.outbound.persistence.postgresql.PostgreSqlSqlStateErrorMappingTest
|
||||
dev.caskeleton.adapter.outbound.persistence.postgresql.PostgreSqlVendorFailureTranslatorRegistrationTest
|
||||
dev.caskeleton.adapter.outbound.persistence.postgresql.constraint.PostgreSqlConstraintCatalogTest
|
||||
dev.caskeleton.adapter.outbound.persistence.postgresql.error.PostgreSqlFailureClassifierTest
|
||||
dev.caskeleton.adapter.outbound.persistence.postgresql.idempotency.IdempotencyDigestPolicyTest
|
||||
dev.caskeleton.adapter.outbound.persistence.postgresql.idempotency.OwnerSafeIdempotencyPreconditionTest
|
||||
dev.caskeleton.adapter.outbound.persistence.postgresql.lock.PostgreSqlLockOptionsTest
|
||||
dev.caskeleton.adapter.outbound.persistence.postgresql.range.PgRangeTest
|
||||
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
|
||||
Daemon will be stopped at the end of the build
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
> Task :shared-contract:compileJava
|
||||
> Task :shared-contract:processResources
|
||||
> Task :shared-contract:classes
|
||||
> Task :shared-contract:jar
|
||||
> Task :application-core:compileJava
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes
|
||||
> Task :application-core:jar
|
||||
> Task :adapter:outbound:persistence-jpa:compileJava
|
||||
> Task :adapter:outbound:persistence-jpa:processResources
|
||||
> Task :adapter:outbound:persistence-jpa:classes
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testClasses
|
||||
> Task :adapter:outbound:persistence-jpa:test
|
||||
|
||||
BUILD SUCCESSFUL in 37s
|
||||
18 actionable tasks: 18 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
Command: current PostgreSQL vendor history + prior-review negative-space/provenance snapshot
|
||||
Working revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Executed at: 2026-08-29T09:53:22Z
|
||||
Observation boundary: repository history/review text only; does not infer author intent beyond committed text.
|
||||
|
||||
=== prior review exact matches ===
|
||||
483:- commit 08xxx/40003은 body replay 0회이며 reconciliation key를 보존.
|
||||
603: - `PostgreSqlSameStoreInboxAdapter.java:39-40`
|
||||
605: - `PostgreSqlPollingDeliveryAdapter.java:31-32`
|
||||
|
||||
=== current history key files ===
|
||||
--- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
|
||||
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
|
||||
0e61f86 feat(jpa): implement the JPA relational persistence platform
|
||||
--- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java
|
||||
0137263 refactor: 각 어댑터터별 리펙토링 진행
|
||||
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
|
||||
0a6dd0e refactor: adapter 구현중..
|
||||
7eb6af5 feat: add JPA production capability
|
||||
--- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/inbox/PostgreSqlSameStoreInboxAdapter.java
|
||||
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
|
||||
7eb6af5 feat: add JPA production capability
|
||||
--- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java
|
||||
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
|
||||
7eb6af5 feat: add JPA production capability
|
||||
--- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/range/PgRangeCodec.java
|
||||
0e61f86 feat(jpa): implement the JPA relational persistence platform
|
||||
--- src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/idempotency/PostgreSqlIdempotencyProviderConfig.java
|
||||
0137263 refactor: 각 어댑터터별 리펙토링 진행
|
||||
--- src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java
|
||||
0137263 refactor: 각 어댑터터별 리펙토링 진행
|
||||
e98b56e feat: jpa, messaging, notification, mongo, graphql 어댑터터 리펙토링
|
||||
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
|
||||
|
||||
=== current composition references ===
|
||||
/shared/codebase/clean-architecture-backend-template/src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:163: : composition.transactionExecutor(transactionManager, vendor);
|
||||
/shared/codebase/clean-architecture-backend-template/src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/idempotency/PostgreSqlIdempotencyProviderConfig.java:60: return new PostgreSqlOwnerSafeIdempotencyStore(jdbc);
|
||||
/shared/codebase/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/readiness/PostgreSqlIdempotencyIntegrationTest.java:61: store = new PostgreSqlOwnerSafeIdempotencyStore(jdbc);
|
||||
/shared/codebase/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlVendorFailureTranslatorRegistrationTest.java:90: assertThat(configuration.postgreSqlVendorFailureTranslator(available(registered)))
|
||||
/shared/codebase/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlVendorFailureTranslatorRegistrationTest.java:92: assertThat(configuration.postgreSqlVendorFailureTranslator(absent()))
|
||||
/shared/codebase/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlVendorFailureTranslatorRegistrationTest.java:99: .postgreSqlVendorFailureTranslator(catalogs)
|
||||
/shared/codebase/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/OwnerSafeIdempotencyPreconditionTest.java:47: new PostgreSqlOwnerSafeIdempotencyStore(jdbc);
|
||||
/shared/codebase/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlPersistenceConfig.java:85: public VendorFailureTranslator postgreSqlVendorFailureTranslator(
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
Command: temporary focused JUnit probe through PostgreSqlExceptionTranslator -> DefaultJpaRetryPolicy
|
||||
Working revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
Executed at: 2026-08-29T09:54:12Z
|
||||
Observation boundary: current production classes, synthetic SQLException(40003); no real network commit ambiguity.
|
||||
--- observations ---
|
||||
<system-out><![CDATA[translated.category=UNKNOWN
|
||||
translated.completionUnknown=false
|
||||
decision.disposition=FAIL
|
||||
decision.reason=failure was classified as non-retryable
|
||||
--- gradle tail ---
|
||||
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testClasses
|
||||
> Task :adapter:outbound:persistence-jpa:test
|
||||
|
||||
BUILD SUCCESSFUL in 11s
|
||||
18 actionable tasks: 2 executed, 16 up-to-date
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package dev.caskeleton.adapter.outbound.persistence.postgresql.error;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import dev.caskeleton.adapter.outbound.persistence.api.PersistenceOperationName;
|
||||
import dev.caskeleton.adapter.outbound.persistence.api.error.ConstraintCode;
|
||||
import dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryProfile;
|
||||
import dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionAttempt;
|
||||
import dev.caskeleton.adapter.outbound.persistence.transaction.DefaultJpaRetryPolicy;
|
||||
import java.sql.SQLException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class PostgreSql40003PolicyProbeTest {
|
||||
@Test
|
||||
void currentTranslatorDemotes40003BeforeRetryPolicy() {
|
||||
var translator = PostgreSqlExceptionTranslator.with(name -> new ConstraintCode("unknown.constraint"));
|
||||
var translated = translator.translate(
|
||||
new SQLException("completion unknown", "40003"),
|
||||
new PersistenceOperationName("probe.40003"), 1, Duration.ZERO, "trace-probe");
|
||||
var decision = DefaultJpaRetryPolicy.forProfile(
|
||||
RetryProfile.boundedContention("probe-default", 3))
|
||||
.classify(translated, TransactionAttempt.first(Instant.EPOCH));
|
||||
System.out.println("translated.category=" + translated.category());
|
||||
System.out.println("translated.completionUnknown=" + translated.completionUnknown());
|
||||
System.out.println("decision.disposition=" + decision.disposition());
|
||||
System.out.println("decision.reason=" + decision.reason());
|
||||
assertThat(decision.disposition().name()).isEqualTo("FAIL");
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
Sub-scope 06 baseline capability manifest
|
||||
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
generatedAt=2026-08-29T09:58:08+00:00
|
||||
production=61 tests=19 resources=7 total=87
|
||||
|
||||
=== PRODUCTION ===
|
||||
d6b53635fab5efe28a8b512eed519cc45eaca2ca src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/audit/AuditContextPort.java
|
||||
f69674ff84c9536fac270c6362d7943137847cc5 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/audit/AuditableEntity.java
|
||||
bc4fd04f8f36269fc9b42e253cb712eb9c8425f9 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/audit/DomainContextAuditContextPort.java
|
||||
05fbb4e4835b93693a263ff7a4cbf756ee871137 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/auditing/AuditMetadata.java
|
||||
7513fe3f599f0140639b9dd0e833e9de1c8908e8 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/auditing/JpaAuditingConfiguration.java
|
||||
8d4b49595e9861372223e9ed95f1ac8dd332b656 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/auditing/JpaAuditorProvider.java
|
||||
dc931f639c707ea7a2ef3c8f5cc65993acbb6324 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/cache/CacheConcurrencyStrategy.java
|
||||
a2a3a93ebc07d9d521b55606c6e12b3fdcff09b6 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/cache/CacheRegionCatalog.java
|
||||
8c05f127ca91be70e6298bc7417127c5809c2cac src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/cache/HibernateCacheGuard.java
|
||||
3cb1486fc50ec9e8b1af820381dc7a6920473f9b src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/cache/HibernateCachePolicy.java
|
||||
469ee48da2d6bc65e1232db79511f265cd3524be src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/cache/HibernateCacheSettings.java
|
||||
a0b1fe10a6cfa478c5915e2b5df25692e1b66128 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/envers/EntityRevision.java
|
||||
24f3538a56c1f613fa9f2c119f83b7a8af48629c src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/envers/EnversConfigurationGuard.java
|
||||
b080e0541007165b35dd823c5e0c094824f82daa src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/envers/EnversHistoryPolicy.java
|
||||
cc5ca2dea37bbbd12cd22c4b4c77318e68fd9274 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/envers/EnversHistoryReader.java
|
||||
13ae5eef2ea9ed85d6592936c70bfd946eec4e10 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/envers/EnversRevisionMetadata.java
|
||||
a489793f2003d96a92acd61df2fcb0ab15f43c09 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/envers/HibernateEnversHistoryReader.java
|
||||
7c7cac59140177fc21751f30e783e2f7273ca0e2 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2IdempotencyClaimRepository.java
|
||||
b17781e26b16219435c63b83681123cf28f29b93 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2LocalTimeoutConfigurer.java
|
||||
3f43da6d9e3835fda89f7cbc652f06aa678c9080 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2OutboxClaimRepository.java
|
||||
e59f4cd68723874cf234e2727186fcf0344eb192 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2PersistenceConfig.java
|
||||
8057b12146a7d038579953def5f044cd93666485 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2SqlStateErrorMapping.java
|
||||
d0b41d1531871f372e1a7ae36fb11a3b54af79c8 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/idempotency/IdempotencyClaimRepository.java
|
||||
6d437e230a463e0fb466e55cad2a30dc166671eb src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/idempotency/IdempotencyReaper.java
|
||||
ad4277cf5973211252ebb7bba38219dfcc7ab117 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/idempotency/IdempotencyRecordJpaRepository.java
|
||||
4f435c03e31b1fbcdd1128ee845107af89630d65 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/idempotency/IdempotencyResponseObjectStore.java
|
||||
280bcc605c280c426d9674f73828c36ea8a2af73 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/idempotency/IdempotencyStoreAdapter.java
|
||||
8ef66aaa15552843e42384160441b95b2b73aa3f src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/idempotency/entity/IdempotencyRecordEntity.java
|
||||
934fa5bef8422116dc588311b242e79143025a22 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/idempotency/mapper/IdempotencyRecordEntityMapper.java
|
||||
4ea0f80eadfe8ed18dc9c976c45f4f13a1936e73 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/liveevent/JpaLiveEventReplayAdapter.java
|
||||
72fd84b9d434d052f042c840ed8bdba85be3e60c src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/liveevent/LiveEventJpaRepository.java
|
||||
9280b1b66c83e0ba9acfd7735d41433a97043ad1 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/liveevent/entity/LiveEventEntity.java
|
||||
7281cd3b422277061e86c0de65775da6232d550b src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/lock/DistributedLockPersistenceConfig.java
|
||||
1f5ee101df8b1e7ab1617829b4c263619551be0a src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/lock/LockRegistryDistributedLockAdapter.java
|
||||
b32c34af7d1084f02ebc55de47ae85258f781f20 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/lock/LockSettings.java
|
||||
8662995b2b434ce9d140d720f464d50acc8131d9 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/ConcurrentIndexMigrationInspector.java
|
||||
3ce5433ba327bd5a01d4785512ab5e251960ad44 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/FailedConcurrentIndexRecovery.java
|
||||
1e7efd0c18d5dffc9722a3c14897e2b0f0687174 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/FlywaySchemaPolicy.java
|
||||
f365a3202dcb93f9816bb7378a765b14b439d1d8 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/FlywayValidationGate.java
|
||||
eb798036799c9fec5ef9fbff9143384842d9b72b src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/MigrationResource.java
|
||||
a6460bade97fc3cbbb83ff29eb7ac0105dd4e04c src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/NonTransactionalMigrationPolicy.java
|
||||
52463c6402db4796894a37a344460a6124a5bc0f src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/SchemaManagementMode.java
|
||||
55c096468ac6d125009224fc1ae70583efa1ebea src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/SchemaVersionSnapshot.java
|
||||
92b88f0ff6df0b174032bc1260a3968a6f5970dd src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaMetricTags.java
|
||||
daaa1ead74c726ff6414b5e2adddf0dcde2611ae src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaRetryObservation.java
|
||||
615c7194abf0be92e19feee8b686fef2f1282e7d src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaTransactionObservation.java
|
||||
316e933ea77e8786c2843c13262b2181ac3050f5 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/LowCardinality.java
|
||||
8e784d4bb8fcc882e6152e9642fbbd3d57490090 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/MicrometerQueryObservation.java
|
||||
f83323b315e6aa3be03a2704fa06acefc9e94ac2 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/SqlDiagnosticRedactor.java
|
||||
573cbd52e6588f499ce7c226a95afb2726999caf src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/operation/DurableOperationJpaRepository.java
|
||||
97d1dc0a650febdce4dc10a41be4297660774dbc src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/operation/DurableOperationStoreAdapter.java
|
||||
7d04cfbc76733cb6205d4171fef750e3513e6792 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/operation/entity/DurableOperationEntity.java
|
||||
f23515ac6e15705c99a44f02de283758dba2606d src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxClaimRepository.java
|
||||
a2fd792a7a44ca4573cbc2e95714d25d5f6738c3 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxEventJpaRepository.java
|
||||
e21db4a2094a5821cf5a843f38da90c3a231526e src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxReaper.java
|
||||
e5d7d1d5956991e6259fbcea10f475e0eb6ebc8e src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java
|
||||
7a7bc80ec6267c294d5b8c3c1b44f8106cf933f6 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/entity/OutboxEventEntity.java
|
||||
f9a8ea7018b7c34c07b2c073b35404488600c01d src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/DatabasePrivilegeReport.java
|
||||
64b9723e491dca18b92810103cafeacf1379f5cc src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicy.java
|
||||
21267dd55aaf067e0d64bb5fa1a3056d4e4f6c6c src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/PostgreSqlRuntimeRoleVerifier.java
|
||||
5385101663e16082b1b756bea8838a11859c776b src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/SearchPathPolicy.java
|
||||
|
||||
=== TEST ===
|
||||
df2a7c9a87747af5b21ec8d71f3ff814e45aa822 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/audit/AuditableEntityTest.java
|
||||
0bcb3e700474e3cc9a3cda2382d1ed9a57406348 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/audit/DomainContextAuditContextPortTest.java
|
||||
e4f6593b30789866ed1c49c0300d823873e28b08 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/auditing/AuditingCandidateStatusTest.java
|
||||
88cac49a80c626d859430c0bb02a9bf375905f43 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/cache/HibernateCacheGuardTest.java
|
||||
56e6e13fec1d82409d461fc97e26216a0d56b6fe src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/config/PersistenceEntityScanCoverageTest.java
|
||||
dd04cd0848e8b8f482fef15031d3bb4d44fc96df src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/config/PersistenceVendorSelectionTest.java
|
||||
8ada120f62e9fd0c01762e5ff110399182072349 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/h2/H2ClaimSqlTest.java
|
||||
a6dfcaa8618b8b7d6e3323ff615d4cbf1a0210d3 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/idempotency/IdempotencyReaperTest.java
|
||||
df1c1ae3d564e7ecfcc4729ed65e20567ee672a7 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/idempotency/IdempotencyStoreAdapterTest.java
|
||||
e8b06f8bf9aa200c5d1eb81cff3df3e0163abbc9 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/lock/LockRegistryDistributedLockAdapterTest.java
|
||||
58c139be7c4bfcd19abd65ba70e3db3a9c9338fb src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/migration/ConcurrentIndexMigrationInspectorTest.java
|
||||
9947d43ff6e809da85a1496e9d6f61c94858cb56 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/migration/FlywayValidationGateTest.java
|
||||
4e3de88e2cba23966254eb2b57cc980cd45b9d08 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaMetricTagsTest.java
|
||||
59d8f72e6aff8bb24ddcda80e7bde8c1ff164328 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaObservabilityContractTest.java
|
||||
7e27d412ec0b47390c73697a430525bc9f8cd137 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxReaperTest.java
|
||||
64db826bede1a1bc0dda574f8c5a662f793124cf src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxReaperWiringTest.java
|
||||
60b77beddb7aa5021604f1e9ea496a4ed363c63f src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapterTest.java
|
||||
b7724e94f9313a0be6aaf9df8b0e448880bcadb0 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/platform/PoolLaneClaimTest.java
|
||||
252598f26ba3ad65a7d970860a63028a312337df src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicyTest.java
|
||||
|
||||
=== RESOURCE ===
|
||||
8dad7255a138638b178599b38f93d2cb99d8cd95 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/core/V1__initialize_or_adopt.sql
|
||||
4dfc2817a7e2565cd3801b63d2b25be2fe48af81 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/core/V2__widen_capability_schema_stream.sql
|
||||
9fc609037f48f29e44f1f1cc1dab55cd9ca35b93 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/idempotency/V1__expand_owner_safe_v2.sql
|
||||
1adcc016d27c692456c7a6573210036d4aa481e3 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/idempotency/V2__request_hash_varchar.sql
|
||||
e0479be401adbc618e1e662abba6dbc83f0f1bde src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/inbox/V1__initialize_same_store_inbox.sql
|
||||
e00203a62c76d281548030bb0370bde4f6110e7a src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/outbox-polling/V1__initialize_polling_delivery_v2.sql
|
||||
0c49e84ca136f79855510e681304ea958e0bbb7a src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/outbox-storage/V1__initialize_or_adopt.sql
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
command=./gradlew :adapter:outbound:persistence-jpa:jpaPlatformContractTest --tests dev.caskeleton.adapter.outbound.persistence.liveevent.LiveEventLogContractTest.analysisProbeFullySweptStreamReusesPosition --console=plain
|
||||
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
|
||||
Daemon will be stopped at the end of the build
|
||||
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||
> Task :build-logic:compileJava UP-TO-DATE
|
||||
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||
> Task :build-logic:processResources UP-TO-DATE
|
||||
> Task :build-logic:classes UP-TO-DATE
|
||||
> Task :build-logic:jar UP-TO-DATE
|
||||
> Task :shared-contract:compileJava UP-TO-DATE
|
||||
> Task :shared-contract:processResources UP-TO-DATE
|
||||
> Task :shared-contract:classes UP-TO-DATE
|
||||
> Task :shared-contract:jar UP-TO-DATE
|
||||
> Task :application-core:compileJava UP-TO-DATE
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes UP-TO-DATE
|
||||
> Task :application-core:jar UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava
|
||||
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses
|
||||
> Task :adapter:outbound:persistence-jpa:jpaPlatformContractTest
|
||||
|
||||
BUILD SUCCESSFUL in 14s
|
||||
19 actionable tasks: 2 executed, 17 up-to-date
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
=== ASSERTED OBSERVATION (from Gradle test XML) ===
|
||||
liveEventFullSweep.swept=1
|
||||
liveEventFullSweep.highestAfterSweep=null
|
||||
liveEventFullSweep.nextPosition=1
|
||||
liveEventFullSweep.payloadAtReusedPosition=new-event
|
||||
source restored after probe: yes
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
package dev.caskeleton.adapter.outbound.persistence.liveevent;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import dev.caskeleton.adapter.outbound.persistence.platform.JpaPlatformContractSupport;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* The live-event log against a real PostgreSQL.
|
||||
*
|
||||
* <p>Every claim this store makes is about a constraint or about concurrency, and neither survives
|
||||
* a fake. A {@code CHECK} a fake does not enforce is a comment; two writers racing for the same
|
||||
* position has no meaning where only one connection exists — and that race is the whole reason the
|
||||
* position is part of the primary key rather than assigned in Java.
|
||||
*/
|
||||
@Tag("jpa-contract")
|
||||
class LiveEventLogContractTest {
|
||||
|
||||
private static final Instant NOW = Instant.parse("2026-08-25T09:00:00Z");
|
||||
private static final Duration RETENTION = Duration.ofHours(1);
|
||||
|
||||
private static JpaPlatformContractSupport support;
|
||||
|
||||
@BeforeAll
|
||||
static void startServer() {
|
||||
support = JpaPlatformContractSupport.start();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void stopServer() {
|
||||
if (support != null) {
|
||||
support.close();
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void migrate() throws SQLException, IOException {
|
||||
try (Connection connection = support.connection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
statement.execute("DROP SCHEMA public CASCADE");
|
||||
statement.execute("CREATE SCHEMA public");
|
||||
statement.execute(migration());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("two positions cannot collide on one stream")
|
||||
void positionsAreUniquePerStream() throws SQLException {
|
||||
append("orders", 1, "a", NOW);
|
||||
|
||||
assertThatThrownBy(() -> append("orders", 1, "b", NOW)).isInstanceOf(SQLException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("the same position on a different stream is fine")
|
||||
void positionsAreScopedToTheirStream() throws SQLException {
|
||||
append("orders", 1, "a", NOW);
|
||||
append("invoices", 1, "b", NOW);
|
||||
|
||||
assertThat(replayAfter("orders", 0, NOW)).hasSize(1);
|
||||
assertThat(replayAfter("invoices", 0, NOW)).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("two writers racing for the same position produce one row, not two")
|
||||
void concurrentAppendsCannotShareAPosition() throws Exception {
|
||||
// The property the primary key exists for. Assigning positions in Java without it would let
|
||||
// both writers read the same maximum and both write it, and the second event would silently
|
||||
// overwrite the first — two different events at one address, with a client's cursor pointing
|
||||
// at whichever survived.
|
||||
List<String> outcomes = new ArrayList<>();
|
||||
try (Connection first = support.connection();
|
||||
Connection second = support.connection()) {
|
||||
first.setAutoCommit(false);
|
||||
second.setAutoCommit(false);
|
||||
insert(first, "orders", 1, "from-first", NOW);
|
||||
outcomes.add("first-inserted");
|
||||
first.commit();
|
||||
try {
|
||||
insert(second, "orders", 1, "from-second", NOW);
|
||||
second.commit();
|
||||
outcomes.add("second-inserted");
|
||||
} catch (SQLException refused) {
|
||||
second.rollback();
|
||||
outcomes.add("second-refused");
|
||||
}
|
||||
}
|
||||
|
||||
assertThat(outcomes).containsExactly("first-inserted", "second-refused");
|
||||
assertThat(payloadAt("orders", 1)).isEqualTo("from-first");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a position of zero is refused by the database, not only by the code")
|
||||
void positionZeroIsRefused() {
|
||||
// A stream whose first row claimed 0 and one whose position was never assigned look identical
|
||||
// to a reader, so the floor is a constraint rather than a convention.
|
||||
assertThatThrownBy(() -> append("orders", 0, "a", NOW)).isInstanceOf(SQLException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a row that expires before it happened is refused")
|
||||
void backwardsRetentionIsRefused() throws SQLException {
|
||||
try (Connection connection = support.connection();
|
||||
PreparedStatement statement =
|
||||
connection.prepareStatement(
|
||||
"INSERT INTO live_event_log (stream_id, position, payload, occurred_at,"
|
||||
+ " retained_until) VALUES (?, ?, ?, ?, ?)")) {
|
||||
statement.setString(1, "orders");
|
||||
statement.setLong(2, 1);
|
||||
statement.setString(3, "a");
|
||||
statement.setTimestamp(4, Timestamp.from(NOW));
|
||||
statement.setTimestamp(5, Timestamp.from(NOW.minusSeconds(1)));
|
||||
|
||||
assertThatThrownBy(statement::executeUpdate).isInstanceOf(SQLException.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a replay returns everything after a cursor, in order")
|
||||
void replayIsOrderedAndExclusive() throws SQLException {
|
||||
append("orders", 1, "a", NOW);
|
||||
append("orders", 2, "b", NOW);
|
||||
append("orders", 3, "c", NOW);
|
||||
|
||||
assertThat(replayAfter("orders", 1, NOW)).containsExactly("b", "c");
|
||||
assertThat(replayAfter("orders", 3, NOW)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an expired row is not replayed, even before it is swept")
|
||||
void expiredRowsAreNotReplayed() throws SQLException {
|
||||
append("orders", 1, "old", NOW.minus(Duration.ofHours(2)));
|
||||
append("orders", 2, "new", NOW);
|
||||
|
||||
// Filtered on read as well as swept on a schedule. An expired row that has not been collected
|
||||
// yet is not history the client may have, and serving it would make the window this store
|
||||
// reports and the window it honours two different things.
|
||||
assertThat(replayAfter("orders", 0, NOW)).containsExactly("new");
|
||||
assertThat(earliestRetained("orders", NOW)).isEqualTo(2L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a swept position is never handed out again")
|
||||
void sweptPositionsAreNotReused() throws SQLException {
|
||||
append("orders", 1, "gone", NOW.minus(Duration.ofHours(2)));
|
||||
append("orders", 2, "kept", NOW);
|
||||
assertThat(sweep(NOW)).isOne();
|
||||
|
||||
// The highest-ever query deliberately ignores retention. Reusing position 1 would give two
|
||||
// different events the same address, and a client holding the older cursor would receive the
|
||||
// newer event as though it were the one it asked to continue after.
|
||||
assertThat(highestEverAssigned("orders")).isEqualTo(2L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("the sweep removes only what has expired")
|
||||
void sweepIsSelective() throws SQLException {
|
||||
append("orders", 1, "gone", NOW.minus(Duration.ofHours(2)));
|
||||
append("orders", 2, "kept", NOW);
|
||||
append("invoices", 1, "kept", NOW);
|
||||
|
||||
assertThat(sweep(NOW)).isOne();
|
||||
assertThat(replayAfter("orders", 0, NOW)).containsExactly("kept");
|
||||
assertThat(replayAfter("invoices", 0, NOW)).containsExactly("kept");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an empty stream reports no window at all")
|
||||
void emptyStreamHasNoWindow() throws SQLException {
|
||||
assertThat(earliestRetained("nothing-here", NOW)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void analysisProbeFullySweptStreamReusesPosition() throws SQLException {
|
||||
append("orders", 1, "expired-only-row", NOW.minus(Duration.ofHours(2)));
|
||||
int swept = sweep(NOW);
|
||||
Long highestAfterFullSweep = highestEverAssigned("orders");
|
||||
long nextPosition = highestAfterFullSweep == null ? 1L : highestAfterFullSweep + 1L;
|
||||
append("orders", nextPosition, "new-event", NOW);
|
||||
System.out.println("liveEventFullSweep.swept=" + swept);
|
||||
System.out.println("liveEventFullSweep.highestAfterSweep=" + highestAfterFullSweep);
|
||||
System.out.println("liveEventFullSweep.nextPosition=" + nextPosition);
|
||||
System.out.println("liveEventFullSweep.payloadAtReusedPosition=" + payloadAt("orders", 1));
|
||||
assertThat(nextPosition).isEqualTo(1L);
|
||||
}
|
||||
|
||||
private void append(String streamId, long position, String payload, Instant occurredAt)
|
||||
throws SQLException {
|
||||
try (Connection connection = support.connection()) {
|
||||
insert(connection, streamId, position, payload, occurredAt);
|
||||
}
|
||||
}
|
||||
|
||||
private static void insert(
|
||||
Connection connection, String streamId, long position, String payload, Instant occurredAt)
|
||||
throws SQLException {
|
||||
try (PreparedStatement statement =
|
||||
connection.prepareStatement(
|
||||
"INSERT INTO live_event_log (stream_id, position, payload, occurred_at,"
|
||||
+ " retained_until) VALUES (?, ?, ?, ?, ?)")) {
|
||||
statement.setString(1, streamId);
|
||||
statement.setLong(2, position);
|
||||
statement.setString(3, payload);
|
||||
statement.setTimestamp(4, Timestamp.from(occurredAt));
|
||||
statement.setTimestamp(5, Timestamp.from(occurredAt.plus(RETENTION)));
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> replayAfter(String streamId, long afterPosition, Instant now)
|
||||
throws SQLException {
|
||||
List<String> payloads = new ArrayList<>();
|
||||
try (Connection connection = support.connection();
|
||||
PreparedStatement statement =
|
||||
connection.prepareStatement(
|
||||
"SELECT payload FROM live_event_log WHERE stream_id = ? AND position > ?"
|
||||
+ " AND retained_until > ? ORDER BY position ASC")) {
|
||||
statement.setString(1, streamId);
|
||||
statement.setLong(2, afterPosition);
|
||||
statement.setTimestamp(3, Timestamp.from(now));
|
||||
try (ResultSet rows = statement.executeQuery()) {
|
||||
while (rows.next()) {
|
||||
payloads.add(rows.getString(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
return payloads;
|
||||
}
|
||||
|
||||
private Long earliestRetained(String streamId, Instant now) throws SQLException {
|
||||
try (Connection connection = support.connection();
|
||||
PreparedStatement statement =
|
||||
connection.prepareStatement(
|
||||
"SELECT min(position) FROM live_event_log WHERE stream_id = ?"
|
||||
+ " AND retained_until > ?")) {
|
||||
statement.setString(1, streamId);
|
||||
statement.setTimestamp(2, Timestamp.from(now));
|
||||
try (ResultSet rows = statement.executeQuery()) {
|
||||
rows.next();
|
||||
long value = rows.getLong(1);
|
||||
return rows.wasNull() ? null : value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Long highestEverAssigned(String streamId) throws SQLException {
|
||||
try (Connection connection = support.connection();
|
||||
PreparedStatement statement =
|
||||
connection.prepareStatement(
|
||||
"SELECT max(position) FROM live_event_log WHERE stream_id = ?")) {
|
||||
statement.setString(1, streamId);
|
||||
try (ResultSet rows = statement.executeQuery()) {
|
||||
rows.next();
|
||||
long value = rows.getLong(1);
|
||||
return rows.wasNull() ? null : value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String payloadAt(String streamId, long position) throws SQLException {
|
||||
try (Connection connection = support.connection();
|
||||
PreparedStatement statement =
|
||||
connection.prepareStatement(
|
||||
"SELECT payload FROM live_event_log WHERE stream_id = ? AND position = ?")) {
|
||||
statement.setString(1, streamId);
|
||||
statement.setLong(2, position);
|
||||
try (ResultSet rows = statement.executeQuery()) {
|
||||
return rows.next() ? rows.getString(1) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int sweep(Instant now) throws SQLException {
|
||||
try (Connection connection = support.connection();
|
||||
PreparedStatement statement =
|
||||
connection.prepareStatement("DELETE FROM live_event_log WHERE retained_until <= ?")) {
|
||||
statement.setTimestamp(1, Timestamp.from(now));
|
||||
return statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private static String migration() throws IOException {
|
||||
try (InputStream source =
|
||||
LiveEventLogContractTest.class.getResourceAsStream(
|
||||
"/db/migration/postgresql/V12__live_event_log.sql")) {
|
||||
if (source == null) {
|
||||
throw new IllegalStateException(
|
||||
"the migration this store is defined by is missing from the classpath, so the test"
|
||||
+ " would create its own idea of the schema and certify that instead");
|
||||
}
|
||||
return new String(source.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
|
||||
=== JpaAdapterComponentsConfig package scan ===
|
||||
1 package dev.caskeleton.adapter.outbound.persistence.config;
|
||||
2
|
||||
3 import org.springframework.context.annotation.ComponentScan;
|
||||
4 import org.springframework.context.annotation.Configuration;
|
||||
5
|
||||
6 /**
|
||||
7 * Registers the JPA adapter's scanned components, which nothing registered (JPA-INT-006).
|
||||
8 *
|
||||
9 * <p>The composition root's {@code @ComponentScan} excludes {@code
|
||||
10 * dev.caskeleton.adapter.outbound.persistence.**} by regex, and that exclusion is correct: it is
|
||||
11 * what makes an optional capability optional, so a deployment with JPA off assembles no persistence
|
||||
12 * beans rather than assembling them and hoping each one remembered to carry the switch.
|
||||
13 *
|
||||
14 * <p>What was missing is the other half. Eight classes in this leaf are written as scanned
|
||||
15 * components — {@code SpringTransactionPort}, {@code PersistenceExceptionTranslator}, {@code
|
||||
16 * StandardSqlStateErrorMapping}, {@code DomainContextAuditContextPort}, the idempotency store and
|
||||
17 * its reaper, and the outbox store and its reaper — and once the broad scan stopped reaching them,
|
||||
18 * nothing else did. They are annotated {@code @Component} and {@code @Repository} and were beans in
|
||||
19 * no running application: {@code TransactionPort} in particular had no implementation at all, so
|
||||
20 * every use case that opens a transaction had no port to open it with.
|
||||
21 *
|
||||
22 * <p>It surfaced as an unsatisfied dependency the first time a capability that needs a transaction
|
||||
23 * was actually assembled — the notification orchestrator, in the local-notification-ingest lane —
|
||||
24 * rather than as anything a unit test could see, because each of these classes is constructed
|
||||
25 * directly by its own tests.
|
||||
26 *
|
||||
27 * <p>So the scan is restored, narrowed to the packages it should always have covered and reachable
|
||||
28 * only through {@code PersistenceJpaRootAutoConfiguration}, which carries the JPA master switch.
|
||||
29 * Off is still structural.
|
||||
30 *
|
||||
31 * <p>Two packages are deliberately absent:
|
||||
32 *
|
||||
33 * <ul>
|
||||
34 * <li>{@code ..persistence.fileserver} — gated on its own capability switch, scanned by {@link
|
||||
35 * dev.caskeleton.adapter.outbound.persistence.fileserver.FileserverJpaPersistenceConfig};
|
||||
36 * <li>{@code ..persistence.notification} — assembled explicitly, bean by bean, by {@code
|
||||
37 * NotificationJpaPersistenceFacade}.
|
||||
38 * </ul>
|
||||
39 *
|
||||
40 * <p>Components under these packages keep their own {@code @ConditionalOnProperty} guards; being
|
||||
41 * scanned makes them candidates, not unconditional beans.
|
||||
42 *
|
||||
43 * <p>{@code ..persistence.lock} is in the list for the same reason and with the same history.
|
||||
44 * {@code DistributedLockPersistenceConfig} owns both lock providers — the in-process registry and
|
||||
45 * the JDBC one — and was registered by nothing but a test calling {@code ctx.register(...)}. So a
|
||||
46 * single-instance deployment had no {@code DistributedLockPort} at all, and a multi-instance one
|
||||
47 * could not start: the composition root's own {@code DistributedLockConfig} asks for a bean
|
||||
48 * qualified {@code jdbcDistributedLock} that only that configuration declares.
|
||||
49 *
|
||||
50 * <p>Each package's {@code @ConfigurationProperties} type is enabled by a configuration inside that
|
||||
51 * same package — {@code JpaTransactionConfig} for {@code JpaTransactionSettings}, {@code
|
||||
52 * DistributedLockPersistenceConfig} for {@code LockSettings} — rather than from here. Enabling them
|
||||
53 * centrally would give {@code config} an edge to {@code lock} and {@code transaction} that the
|
||||
54 * module map does not grant it, and the map is right: this class knows which packages to scan, not
|
||||
55 * what is inside them. They need enabling at all because {@code @ConfigurationPropertiesScan}
|
||||
56 * excludes this tree as deliberately as {@code @ComponentScan} does.
|
||||
57 */
|
||||
58 @Configuration(proxyBeanMethods = false)
|
||||
59 @ComponentScan(
|
||||
60 basePackages = {
|
||||
61 "dev.caskeleton.adapter.outbound.persistence.audit",
|
||||
62 "dev.caskeleton.adapter.outbound.persistence.failure",
|
||||
63 "dev.caskeleton.adapter.outbound.persistence.idempotency",
|
||||
64 "dev.caskeleton.adapter.outbound.persistence.lock",
|
||||
65 "dev.caskeleton.adapter.outbound.persistence.outbox",
|
||||
66 "dev.caskeleton.adapter.outbound.persistence.transaction"
|
||||
67 })
|
||||
68 public class JpaAdapterComponentsConfig {}
|
||||
|
||||
=== runtime-role policy production callers ===
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/PostgreSqlRuntimeRoleVerifier.java:57: policy.requireSafe(verify(dataSource));
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicy.java:19:public record DatabaseRolePolicy(Set<String> allowedRoles, SearchPathPolicy searchPathPolicy) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicy.java:55: searchPathPolicy.requireSafe(report.searchPath());
|
||||
|
||||
=== runtime-role policy all callers ===
|
||||
src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/platform/PostgreSqlSecurityContractTest.java:89: new DatabaseRolePolicy(
|
||||
src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/platform/PostgreSqlSecurityContractTest.java:92: new PostgreSqlRuntimeRoleVerifier().requireSafe(runtime, policy);
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicyTest.java:15: new DatabaseRolePolicy(
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicyTest.java:21: policy.requireSafe(new DatabasePrivilegeReport("app_runtime", "app, pg_catalog", false, false));
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicyTest.java:29: policy.requireSafe(new DatabasePrivilegeReport("app_runtime", "app", true, false)))
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicyTest.java:38: () -> policy.requireSafe(new DatabasePrivilegeReport("postgres", "app", false, false)))
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicyTest.java:48: policy.requireSafe(
|
||||
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicyTest.java:57: policy.requireSafe(new DatabasePrivilegeReport("app_runtime", "\"$user\", app", false, false));
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/PostgreSqlRuntimeRoleVerifier.java:57: policy.requireSafe(verify(dataSource));
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicy.java:19:public record DatabaseRolePolicy(Set<String> allowedRoles, SearchPathPolicy searchPathPolicy) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicy.java:55: searchPathPolicy.requireSafe(report.searchPath());
|
||||
|
||||
=== runtime role advertised Stable + report semantics ===
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:61: CapabilitySupport.stable(JpaCapability.RUNTIME_ROLE_VERIFICATION),
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:112: return roleVerifier.verify(dataSource);
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:25: boolean runtimeRoleVerified,
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:56: privileges != null && !privileges.holdsCreatePrivilege(),
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:62: return openInViewDisabled && runtimeRoleVerified;
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:194: public InitializingBean jpaPlatformStartupCheck(
|
||||
|
||||
=== startup contract docs ===
|
||||
## Startup verification
|
||||
|
||||
`PostgreSqlRuntimeRoleVerifier` asks the *server* what the connection can do:
|
||||
|
||||
```sql
|
||||
select current_user,
|
||||
current_setting('search_path'),
|
||||
has_schema_privilege(current_user, current_schema(), 'CREATE'),
|
||||
has_database_privilege(current_user, current_database(), 'CREATE')
|
||||
```
|
||||
|
||||
Configuration cannot answer this. Effective privileges come from direct grants, inherited role
|
||||
memberships, `PUBLIC` grants, and schema ownership, and no reading of a deployment manifest
|
||||
reconstructs that combination reliably.
|
||||
|
||||
Startup fails when the runtime role is not on the allowlist, or holds `CREATE` on the schema or the
|
||||
database.
|
||||
|
||||
## search_path
|
||||
|
||||
`SearchPathPolicy` is an allowlist. `search_path` decides which schema an unqualified name resolves
|
||||
to, so a writable untrusted schema on it — classically `public`, where `CREATE` was granted broadly
|
||||
before PostgreSQL 15 — lets a planted table, function, or operator shadow the real one, and the
|
||||
application executes it without noticing. `$user` is exempt: only the connected role owns it.
|
||||
|
||||
Refusing the runtime role `CREATE` closes the same route from the other side.
|
||||
|
||||
### 41.2 search_path
|
||||
|
||||
Application role의 `search_path`를 고정하고 untrusted schema의 object resolution을 차단한다. startup verifier가 current_user, current_schema, search_path, schema CREATE privilege를 검사한다.
|
||||
|
||||
### 41.3 Injection 방어
|
||||
|
||||
```text
|
||||
JPQL/Native values → parameter binding
|
||||
Dynamic sort → allowlist
|
||||
Dynamic table/column → enum/catalog mapping만
|
||||
Entity → API mass binding 금지
|
||||
```
|
||||
|
||||
### 41.4 Secret
|
||||
|
||||
DB password는 secret manager/workload identity에서 주입하고 config·log·metric에 기록하지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 42. Spring Boot AutoConfiguration
|
||||
|
||||
### 42.1 Properties
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
jpa:
|
||||
enabled: true
|
||||
require-postgresql: true
|
||||
open-in-view: false
|
||||
schema-management: VALIDATE
|
||||
transaction-profiles: {}
|
||||
retry-profiles: {}
|
||||
observability:
|
||||
hibernate-statistics: true
|
||||
sql-parameters: false
|
||||
security:
|
||||
verify-runtime-role: true
|
||||
verify-search-path: true
|
||||
```
|
||||
|
||||
### 42.2 Startup Failures
|
||||
|
||||
```text
|
||||
spring.jpa.open-in-view=true
|
||||
prod ddl-auto != validate/none
|
||||
unsupported PostgreSQL version
|
||||
runtime role has DDL privilege
|
||||
migration checksum mismatch
|
||||
required transaction profile timeout missing
|
||||
Experimental module enabled without feature flag
|
||||
```
|
||||
|
||||
=== outbox composition path ===
|
||||
src/app-bootstrap/src/main/resources/application.yml:662: # metrics, or store binding at all; relay-enabled below only decides whether the scheduler runs.
|
||||
src/app-bootstrap/src/main/resources/application.yml:666: relay-enabled: ${APP_OUTBOX_RELAY_ENABLED:false}
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxMetrics.java:5:import dev.caskeleton.application.outbox.OutboxStorePort;
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxMetrics.java:30: private final OutboxStorePort store;
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxMetrics.java:35: public OutboxMetrics(OutboxStorePort store, ObjectProvider<MeterRegistry> meterRegistryProvider) {
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxRelayBrokerRequirementValidator.java:10: * <p>{@code ca-skeleton.outbox.relay-enabled} defaults to true and {@code app.messaging.broker}
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxRelayBrokerRequirementValidator.java:23: private static final String RELAY_KEY = "ca-skeleton.outbox.relay-enabled";
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxRelayScheduler.java:5:import dev.caskeleton.application.outbox.PublishPendingOutboxEventsUseCase;
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxRelayScheduler.java:19: private final PublishPendingOutboxEventsUseCase relayUseCase;
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxRelayScheduler.java:24: PublishPendingOutboxEventsUseCase relayUseCase, OutboxMetrics metrics, Clock clock) {
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:6:import dev.caskeleton.application.outbox.OutboxStorePort;
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:7:import dev.caskeleton.application.outbox.PublishPendingOutboxEventsUseCase;
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:24: * <p>{@code ca-skeleton.outbox.enabled} is the capability switch; {@code relay-enabled} only
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:52: name = "ca-skeleton.outbox.relay-enabled",
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:56: OutboxStorePort store,
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:64: PublishPendingOutboxEventsUseCase relayUseCase =
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:65: new PublishPendingOutboxEventsUseCase(
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:95: OutboxStorePort store, ObjectProvider<MeterRegistry> meterRegistryProvider) {
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/activation/CapabilityDependencyValidator.java:50: boolean relay = isOn(environment, "ca-skeleton.outbox.relay-enabled");
|
||||
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/activation/CapabilityDependencyValidator.java:67: "ca-skeleton.outbox.relay-enabled=true only starts the scheduler for a capability that is "
|
||||
src/application-core/src/main/java/dev/caskeleton/application/inbox/InboxStorePort.java:22: InboxTransitionOutcome markDead(InboxOwner owner, Duration retention, OperationId operationId);
|
||||
src/application-core/src/main/java/dev/caskeleton/application/fileserver/cleanup/DefaultCleanupService.java:87: markFailed(item, "BATCH_BYTE_BUDGET_EXHAUSTED", now);
|
||||
src/application-core/src/main/java/dev/caskeleton/application/fileserver/cleanup/DefaultCleanupService.java:123: markFailed(item, "CLEANUP_ATTEMPT_FAILED", now);
|
||||
src/application-core/src/main/java/dev/caskeleton/application/fileserver/cleanup/DefaultCleanupService.java:140: markFailed(item, "ACTIVE_WRITER_LEASE", now);
|
||||
src/application-core/src/main/java/dev/caskeleton/application/fileserver/cleanup/DefaultCleanupService.java:206: private void markFailed(CleanupItem item, String reasonCode, Instant now) {
|
||||
src/application-core/src/main/java/dev/caskeleton/application/fileserver/cleanup/DefaultCleanupService.java:209: queue.markFailed(item, reasonCode, now.plus(retryBackoff));
|
||||
src/application-core/src/main/java/dev/caskeleton/application/fileserver/cleanup/CleanupQueue.java:21: void markFailed(CleanupItem item, String reasonCode, Instant nextAttemptAt);
|
||||
src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxRelayResult.java:7: * Result returned by {@link PublishPendingOutboxEventsUseCase} after one relay cycle: the number of
|
||||
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:35:public class PublishPendingOutboxEventsUseCase
|
||||
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:38: private final OutboxStorePort store;
|
||||
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:60: public PublishPendingOutboxEventsUseCase(
|
||||
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:61: OutboxStorePort store,
|
||||
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:90: List<OutboxEvent> claimed = tx.inWrite(() -> store.claimBatch(batchSize, now, inFlightTimeout));
|
||||
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:141: tx.inWrite(() -> store.markPublished(event.eventId()));
|
||||
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:159: tx.inWrite(() -> store.markFailed(event.eventId(), nextAttemptAt));
|
||||
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:175: tx.inWrite(() -> store.markDead(event.eventId()));
|
||||
src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxStorePort.java:14:public interface OutboxStorePort {
|
||||
src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxStorePort.java:27: List<OutboxEvent> claimBatch(int batchSize, Instant now, Duration inFlightTimeout);
|
||||
src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxStorePort.java:35: void markPublished(String eventId);
|
||||
src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxStorePort.java:45: void markFailed(String eventId, Instant nextAttemptAt);
|
||||
src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxStorePort.java:54: void markDead(String eventId);
|
||||
src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxEvent.java:7: * Immutable view of an outbox event row returned by {@link OutboxStorePort#claimBatch}.
|
||||
src/application-core/src/main/java/dev/caskeleton/application/outbox/v2/OutboxPollingDeliveryPortV2.java:9: List<ClaimedOutboxDelivery> claimBatch(OutboxDeliveryClaimRequest request);
|
||||
src/application-core/src/main/java/dev/caskeleton/application/outbox/v2/OutboxPollingDeliveryPortV2.java:11: OutboxDeliveryTransitionOutcome markPublished(OutboxDeliveryTransition transition);
|
||||
src/application-core/src/main/java/dev/caskeleton/application/outbox/v2/OutboxPollingDeliveryPortV2.java:16: OutboxDeliveryTransitionOutcome markDead(OutboxDeliveryTransition transition, String errorCode);
|
||||
src/application-core/src/main/java/dev/caskeleton/application/idempotency/v2/IdempotencyStorePortV2.java:28: IdempotencyFailOutcome markFailed(
|
||||
src/application-core/src/main/java/dev/caskeleton/application/idempotency/v2/IdempotencyExecutorV2.java:217: store.markFailed(
|
||||
src/application-core/src/main/java/dev/caskeleton/application/idempotency/v2/IdempotencyExecutorV2.java:282: store.markFailed(
|
||||
src/application-core/src/main/java/dev/caskeleton/application/notification/platform/callback/ProviderEventLedger.java:26: void markFailed(ProviderEventRecordId eventId, String errorCode);
|
||||
src/application-core/src/main/java/dev/caskeleton/application/notification/platform/callback/ProviderEventProjectionService.java:58: ledger.markFailed(event.id(), "NO_PROJECTOR");
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaCleanupQueue.java:145: public void markFailed(CleanupItem item, String reasonCode, Instant nextAttemptAt) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:8:import dev.caskeleton.application.outbox.OutboxStorePort;
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:18: * JPA-backed {@link OutboxAppendPort} + {@link OutboxStorePort}. {@link #append} and the claim/mark
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:24:public class OutboxStoreAdapter implements OutboxAppendPort, OutboxStorePort {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:29: public OutboxStoreAdapter(
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:53: public List<OutboxEvent> claimBatch(int batchSize, Instant now, Duration inFlightTimeout) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:72: public void markPublished(String eventId) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:84: public void markFailed(String eventId, Instant nextAttemptAt) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:99: public void markDead(String eventId) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/inbox/PostgreSqlSameStoreInboxAdapter.java:350: public InboxTransitionOutcome markDead(
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java:297: public List<ClaimedOutboxDelivery> claimBatch(OutboxDeliveryClaimRequest request) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java:313: public OutboxDeliveryTransitionOutcome markPublished(OutboxDeliveryTransition transition) {
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java:366: public OutboxDeliveryTransitionOutcome markDead(
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java:294: public IdempotencyFailOutcome markFailed(
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/RecipientDeliveryJpaRepository.java:36: List<Object[]> claimBatch(
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaRecipientLeaseStore.java:47: return recipients.claimBatch(now, limit, workerId, leaseUntil).stream()
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaProviderEventLedger.java:146: public void markFailed(ProviderEventRecordId eventId, String errorCode) {
|
||||
|
||||
=== durable/live adapter construction references ===
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/liveevent/JpaLiveEventReplayAdapter.java:54: public JpaLiveEventReplayAdapter(
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/operation/DurableOperationStoreAdapter.java:39: public DurableOperationStoreAdapter(DurableOperationJpaRepository repository) {
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
command=./gradlew :adapter:outbound:persistence-jpa:jpaPlatformContractTest --tests dev.caskeleton.adapter.outbound.persistence.operation.DurableOperationStoreContractTest.analysisProbeExpiredLeaseCanStillSucceedBeforeTakeover --console=plain
|
||||
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
|
||||
Daemon will be stopped at the end of the build
|
||||
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||
> Task :build-logic:compileJava UP-TO-DATE
|
||||
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||
> Task :build-logic:processResources UP-TO-DATE
|
||||
> Task :build-logic:classes UP-TO-DATE
|
||||
> Task :build-logic:jar UP-TO-DATE
|
||||
> Task :shared-contract:compileJava UP-TO-DATE
|
||||
> Task :shared-contract:processResources UP-TO-DATE
|
||||
> Task :shared-contract:classes UP-TO-DATE
|
||||
> Task :shared-contract:jar UP-TO-DATE
|
||||
> Task :application-core:compileJava UP-TO-DATE
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes UP-TO-DATE
|
||||
> Task :application-core:jar UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava
|
||||
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses
|
||||
> Task :adapter:outbound:persistence-jpa:jpaPlatformContractTest
|
||||
|
||||
BUILD SUCCESSFUL in 14s
|
||||
19 actionable tasks: 2 executed, 17 up-to-date
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
=== ASSERTED OBSERVATION (from Gradle test XML) ===
|
||||
durableExpiredLease.completionAt=2026-08-25T09:02:01Z
|
||||
durableExpiredLease.leaseExpiredAtCompletion=true
|
||||
durableExpiredLease.succeedUpdatedRows=1
|
||||
durableExpiredLease.finalState=SUCCEEDED
|
||||
source restored after probe: yes
|
||||
+503
@@ -0,0 +1,503 @@
|
||||
package dev.caskeleton.adapter.outbound.persistence.operation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import dev.caskeleton.adapter.outbound.persistence.platform.JpaPlatformContractSupport;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* The durable operation store against a real PostgreSQL.
|
||||
*
|
||||
* <p>Every claim in this store is about concurrency or about a constraint, and neither survives
|
||||
* being tested against a fake. {@code FOR UPDATE SKIP LOCKED} does not exist in H2's PostgreSQL
|
||||
* mode with the semantics that matter, a {@code CHECK} constraint a fake does not enforce is a
|
||||
* comment, and a lease race has no meaning where only one connection exists.
|
||||
*
|
||||
* <p>A dead worker is written the only way that is faithful here: the row a crash leaves behind — a
|
||||
* lease nobody will renew — and then recovery runs against it. That is precisely what a killed
|
||||
* process is from the database's side.
|
||||
*/
|
||||
@Tag("jpa-contract")
|
||||
class DurableOperationStoreContractTest {
|
||||
|
||||
private static final Instant NOW = Instant.parse("2026-08-25T09:00:00Z");
|
||||
private static final Duration LEASE = Duration.ofMinutes(2);
|
||||
|
||||
private static JpaPlatformContractSupport support;
|
||||
|
||||
@BeforeAll
|
||||
static void startServer() {
|
||||
support = JpaPlatformContractSupport.start();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void stopServer() {
|
||||
if (support != null) {
|
||||
support.close();
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void migrate() throws SQLException, IOException {
|
||||
try (Connection connection = support.connection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
statement.execute("DROP SCHEMA public CASCADE");
|
||||
statement.execute("CREATE SCHEMA public");
|
||||
statement.execute(migration());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a lease left behind by a dead worker is reclaimed by another")
|
||||
void expiredLeaseCanBeReclaimedAfterWorkerCrash() throws SQLException {
|
||||
submit("op-1", "hash-1");
|
||||
claim("worker-a", NOW);
|
||||
|
||||
// The worker is gone. Nothing renews the lease, and nothing else may touch the operation until
|
||||
// it lapses — that exclusivity is the point of the lease.
|
||||
assertThat(claim("worker-b", NOW.plusSeconds(30))).isEmpty();
|
||||
|
||||
assertThat(claim("worker-b", NOW.plus(LEASE).plusSeconds(1))).contains("op-1");
|
||||
assertThat(state("op-1")).isEqualTo("RUNNING");
|
||||
assertThat(leaseOwner("op-1")).isEqualTo("worker-b");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a reclaimed operation keeps the moment it first started")
|
||||
void reclaimKeepsTheOriginalStartTime() throws SQLException {
|
||||
submit("op-1", "hash-1");
|
||||
claim("worker-a", NOW);
|
||||
|
||||
claim("worker-b", NOW.plus(LEASE).plusSeconds(1));
|
||||
|
||||
// COALESCE, not an unconditional assignment. Overwriting it would make every retry look like a
|
||||
// fresh start and hide how long the operation has really been running.
|
||||
assertThat(startedAt("op-1")).isEqualTo(NOW);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("two workers claiming at once take different operations")
|
||||
void concurrentWorkersDoNotTakeTheSameOperation() throws Exception {
|
||||
submit("op-1", "hash-1");
|
||||
submit("op-2", "hash-2");
|
||||
|
||||
List<String> claimed = new ArrayList<>();
|
||||
try (Connection first = support.connection();
|
||||
Connection second = support.connection()) {
|
||||
first.setAutoCommit(false);
|
||||
second.setAutoCommit(false);
|
||||
// Interleaved on purpose and inside open transactions: with plain FOR UPDATE the second
|
||||
// claim blocks here until the first commits, and with no locking at all both take op-1.
|
||||
claimed.add(claimOn(first, "worker-a", NOW).orElseThrow());
|
||||
claimed.add(claimOn(second, "worker-b", NOW).orElseThrow());
|
||||
first.commit();
|
||||
second.commit();
|
||||
}
|
||||
|
||||
assertThat(claimed).containsExactlyInAnyOrder("op-1", "op-2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a worker whose lease lapsed cannot record a result over its successor")
|
||||
void aStaleWorkerCannotOverwriteTheNewOwner() throws SQLException {
|
||||
submit("op-1", "hash-1");
|
||||
claim("worker-a", NOW);
|
||||
claim("worker-b", NOW.plus(LEASE).plusSeconds(1));
|
||||
|
||||
// worker-a is alive and still finishing. Letting it report here is how one operation ends up
|
||||
// with two answers, and which one survives would come down to timing.
|
||||
assertThat(succeed("op-1", "worker-a", "/results/a")).isZero();
|
||||
assertThat(succeed("op-1", "worker-b", "/results/b")).isOne();
|
||||
assertThat(resultReference("op-1")).isEqualTo("/results/b");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a heartbeat from a worker that lost the lease fails rather than extending it")
|
||||
void heartbeatFromAStaleWorkerFails() throws SQLException {
|
||||
submit("op-1", "hash-1");
|
||||
claim("worker-a", NOW);
|
||||
claim("worker-b", NOW.plus(LEASE).plusSeconds(1));
|
||||
|
||||
assertThat(heartbeat("op-1", "worker-a", NOW.plus(LEASE).plusSeconds(2))).isZero();
|
||||
assertThat(heartbeat("op-1", "worker-b", NOW.plus(LEASE).plusSeconds(2))).isOne();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("cancel works once and never reverses a finished operation")
|
||||
void cancelIsIdempotentAndNeverReversesTerminal() throws SQLException {
|
||||
submit("op-1", "hash-1");
|
||||
claim("worker-a", NOW);
|
||||
succeed("op-1", "worker-a", "/results/a");
|
||||
|
||||
// The work happened. Recording it as CANCELED would replace the only record of a real outcome
|
||||
// with a claim that it never occurred.
|
||||
assertThat(cancel("op-1", "alice")).isZero();
|
||||
assertThat(state("op-1")).isEqualTo("SUCCEEDED");
|
||||
|
||||
submit("op-2", "hash-2");
|
||||
assertThat(cancel("op-2", "alice")).isOne();
|
||||
assertThat(cancel("op-2", "alice")).isZero();
|
||||
assertThat(state("op-2")).isEqualTo("CANCELED");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("another principal cannot cancel an operation they did not submit")
|
||||
void cancelIsScopedToThePrincipal() throws SQLException {
|
||||
submit("op-1", "hash-1");
|
||||
|
||||
assertThat(cancel("op-1", "mallory")).isZero();
|
||||
assertThat(state("op-1")).isEqualTo("PENDING");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a cancelled operation is not handed to a worker")
|
||||
void cancelledOperationsAreNotClaimable() throws SQLException {
|
||||
submit("op-1", "hash-1");
|
||||
cancel("op-1", "alice");
|
||||
|
||||
assertThat(claim("worker-a", NOW)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an identical resubmission is refused by the unique constraint")
|
||||
void resubmissionCollidesOnScope() throws SQLException {
|
||||
submit("op-1", "hash-1");
|
||||
|
||||
// The constraint is the arbiter rather than a prior read: two identical submissions can arrive
|
||||
// at the same instant, and a read-then-insert lets both through.
|
||||
assertThatThrownBy(() -> submit("op-2", "hash-1")).isInstanceOf(SQLException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a succeeded row with no result is refused by the database")
|
||||
void succeededRowRequiresAResult() throws SQLException {
|
||||
submit("op-1", "hash-1");
|
||||
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
execute(
|
||||
"UPDATE durable_operation SET state = 'SUCCEEDED', completed_at = now(),"
|
||||
+ " lease_owner = NULL WHERE operation_id = 'op-1'"))
|
||||
.isInstanceOf(SQLException.class)
|
||||
.hasMessageContaining("ck_durable_operation_succeeded_has_result");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a running row with no lease is refused by the database")
|
||||
void runningRowRequiresALease() {
|
||||
assertThatThrownBy(
|
||||
() -> {
|
||||
submit("op-1", "hash-1");
|
||||
execute("UPDATE durable_operation SET state = 'RUNNING' WHERE operation_id = 'op-1'");
|
||||
})
|
||||
.isInstanceOf(SQLException.class)
|
||||
.hasMessageContaining("ck_durable_operation_running_has_lease");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a state outside the published vocabulary is refused")
|
||||
void stateVocabularyIsClosed() {
|
||||
// A worker that could write its own state would extend the published contract by writing a
|
||||
// row, and a client branching on status would meet a value that is in no version of the API.
|
||||
assertThatThrownBy(
|
||||
() -> {
|
||||
submit("op-1", "hash-1");
|
||||
execute(
|
||||
"UPDATE durable_operation SET state = 'ALMOST_DONE' WHERE operation_id = 'op-1'");
|
||||
})
|
||||
.isInstanceOf(SQLException.class)
|
||||
.hasMessageContaining("ck_durable_operation_state");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an aged-out operation is expired, and a finished one is left alone")
|
||||
void expirySweepLeavesRealOutcomesAlone() throws SQLException {
|
||||
submit("op-1", "hash-1");
|
||||
submit("op-2", "hash-2");
|
||||
claim("worker-a", NOW);
|
||||
|
||||
List<String> expired = expireStale(NOW.plus(Duration.ofDays(2)));
|
||||
|
||||
assertThat(expired).containsExactlyInAnyOrder("op-1", "op-2");
|
||||
submit("op-3", "hash-3");
|
||||
claim("worker-b", NOW);
|
||||
succeed("op-3", "worker-b", "/results/c");
|
||||
assertThat(expireStale(NOW.plus(Duration.ofDays(2)))).isEmpty();
|
||||
assertThat(state("op-3")).isEqualTo("SUCCEEDED");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("reclaim returns lapsed leases to the queue")
|
||||
void reclaimReturnsLapsedLeases() throws SQLException {
|
||||
submit("op-1", "hash-1");
|
||||
claim("worker-a", NOW);
|
||||
|
||||
assertThat(reclaim(NOW.plusSeconds(30))).isEmpty();
|
||||
assertThat(reclaim(NOW.plus(LEASE).plusSeconds(1))).containsExactly("op-1");
|
||||
assertThat(state("op-1")).isEqualTo("PENDING");
|
||||
assertThat(leaseOwner("op-1")).isNull();
|
||||
}
|
||||
|
||||
private void submit(String operationId, String hash) throws SQLException {
|
||||
try (Connection connection = support.connection();
|
||||
PreparedStatement statement =
|
||||
connection.prepareStatement(
|
||||
"""
|
||||
INSERT INTO durable_operation
|
||||
(operation_id, tenant, principal, operation_name, request_hash, payload,
|
||||
state, submitted_at, expires_at)
|
||||
VALUES (?, '', 'alice', 'transfers.create', ?, '{}', 'PENDING', ?, ?)
|
||||
""")) {
|
||||
statement.setString(1, operationId);
|
||||
statement.setString(2, hash);
|
||||
statement.setTimestamp(3, Timestamp.from(NOW));
|
||||
statement.setTimestamp(4, Timestamp.from(NOW.plus(Duration.ofDays(1))));
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<String> claim(String workerId, Instant now) throws SQLException {
|
||||
try (Connection connection = support.connection()) {
|
||||
return claimOn(connection, workerId, now);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<String> claimOn(Connection connection, String workerId, Instant now)
|
||||
throws SQLException {
|
||||
try (PreparedStatement statement =
|
||||
connection.prepareStatement(
|
||||
"""
|
||||
UPDATE durable_operation
|
||||
SET state = 'RUNNING',
|
||||
lease_owner = ?,
|
||||
lease_acquired_at = ?,
|
||||
lease_expires_at = ?,
|
||||
started_at = COALESCE(started_at, ?),
|
||||
row_version = row_version + 1
|
||||
WHERE operation_id = (
|
||||
SELECT operation_id
|
||||
FROM durable_operation
|
||||
WHERE (state = 'PENDING'
|
||||
OR (state = 'RUNNING' AND lease_expires_at <= ?))
|
||||
AND expires_at > ?
|
||||
ORDER BY submitted_at
|
||||
FOR UPDATE SKIP LOCKED
|
||||
LIMIT 1)
|
||||
RETURNING operation_id
|
||||
""")) {
|
||||
statement.setString(1, workerId);
|
||||
statement.setTimestamp(2, Timestamp.from(now));
|
||||
statement.setTimestamp(3, Timestamp.from(now.plus(LEASE)));
|
||||
statement.setTimestamp(4, Timestamp.from(now));
|
||||
statement.setTimestamp(5, Timestamp.from(now));
|
||||
statement.setTimestamp(6, Timestamp.from(now));
|
||||
try (ResultSet rows = statement.executeQuery()) {
|
||||
return rows.next() ? Optional.of(rows.getString(1)) : Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void analysisProbeExpiredLeaseCanStillSucceedBeforeTakeover() throws SQLException {
|
||||
submit("op-expired", "hash-expired");
|
||||
claim("worker-a", NOW);
|
||||
Instant afterLease = NOW.plus(LEASE).plusSeconds(1);
|
||||
int updated = succeedAt("op-expired", "worker-a", "/results/stale", afterLease);
|
||||
System.out.println("durableExpiredLease.completionAt=" + afterLease);
|
||||
System.out.println("durableExpiredLease.leaseExpiredAtCompletion=true");
|
||||
System.out.println("durableExpiredLease.succeedUpdatedRows=" + updated);
|
||||
System.out.println("durableExpiredLease.finalState=" + state("op-expired"));
|
||||
assertThat(updated).isOne();
|
||||
}
|
||||
|
||||
private int succeedAt(String operationId, String workerId, String resultReference, Instant completedAt)
|
||||
throws SQLException {
|
||||
try (Connection connection = support.connection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"UPDATE durable_operation SET state = 'SUCCEEDED', result_reference = ?, completed_at = ?, "
|
||||
+ "lease_owner = NULL, lease_acquired_at = NULL, lease_expires_at = NULL, "
|
||||
+ "row_version = row_version + 1 "
|
||||
+ "WHERE operation_id = ? AND state = 'RUNNING' AND lease_owner = ?")) {
|
||||
statement.setString(1, resultReference);
|
||||
statement.setTimestamp(2, Timestamp.from(completedAt));
|
||||
statement.setString(3, operationId);
|
||||
statement.setString(4, workerId);
|
||||
return statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private int succeed(String operationId, String workerId, String resultReference)
|
||||
throws SQLException {
|
||||
try (Connection connection = support.connection();
|
||||
PreparedStatement statement =
|
||||
connection.prepareStatement(
|
||||
"""
|
||||
UPDATE durable_operation
|
||||
SET state = 'SUCCEEDED', result_reference = ?, completed_at = ?,
|
||||
lease_owner = NULL, lease_acquired_at = NULL, lease_expires_at = NULL,
|
||||
row_version = row_version + 1
|
||||
WHERE operation_id = ? AND state = 'RUNNING' AND lease_owner = ?
|
||||
""")) {
|
||||
statement.setString(1, resultReference);
|
||||
statement.setTimestamp(2, Timestamp.from(NOW.plusSeconds(10)));
|
||||
statement.setString(3, operationId);
|
||||
statement.setString(4, workerId);
|
||||
return statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private int heartbeat(String operationId, String workerId, Instant now) throws SQLException {
|
||||
try (Connection connection = support.connection();
|
||||
PreparedStatement statement =
|
||||
connection.prepareStatement(
|
||||
"""
|
||||
UPDATE durable_operation
|
||||
SET lease_expires_at = ?, row_version = row_version + 1
|
||||
WHERE operation_id = ? AND state = 'RUNNING' AND lease_owner = ?
|
||||
AND lease_expires_at > ?
|
||||
""")) {
|
||||
statement.setTimestamp(1, Timestamp.from(now.plus(LEASE)));
|
||||
statement.setString(2, operationId);
|
||||
statement.setString(3, workerId);
|
||||
statement.setTimestamp(4, Timestamp.from(now));
|
||||
return statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private int cancel(String operationId, String principal) throws SQLException {
|
||||
try (Connection connection = support.connection();
|
||||
PreparedStatement statement =
|
||||
connection.prepareStatement(
|
||||
"""
|
||||
UPDATE durable_operation
|
||||
SET state = 'CANCELED', completed_at = ?, lease_owner = NULL,
|
||||
lease_acquired_at = NULL, lease_expires_at = NULL,
|
||||
row_version = row_version + 1
|
||||
WHERE operation_id = ? AND principal = ? AND state IN ('PENDING', 'RUNNING')
|
||||
""")) {
|
||||
statement.setTimestamp(1, Timestamp.from(NOW.plusSeconds(5)));
|
||||
statement.setString(2, operationId);
|
||||
statement.setString(3, principal);
|
||||
return statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> reclaim(Instant now) throws SQLException {
|
||||
return idsFrom(
|
||||
"""
|
||||
UPDATE durable_operation
|
||||
SET state = 'PENDING', lease_owner = NULL, lease_acquired_at = NULL,
|
||||
lease_expires_at = NULL, row_version = row_version + 1
|
||||
WHERE state = 'RUNNING' AND lease_expires_at <= ?
|
||||
RETURNING operation_id
|
||||
""",
|
||||
now);
|
||||
}
|
||||
|
||||
private List<String> expireStale(Instant now) throws SQLException {
|
||||
return idsFrom(
|
||||
"""
|
||||
UPDATE durable_operation
|
||||
SET state = 'EXPIRED', completed_at = ?, lease_owner = NULL,
|
||||
lease_acquired_at = NULL, lease_expires_at = NULL,
|
||||
row_version = row_version + 1
|
||||
WHERE state IN ('PENDING', 'RUNNING') AND expires_at <= ?
|
||||
RETURNING operation_id
|
||||
""",
|
||||
now,
|
||||
now);
|
||||
}
|
||||
|
||||
private List<String> idsFrom(String sql, Instant... arguments) throws SQLException {
|
||||
try (Connection connection = support.connection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
for (int index = 0; index < arguments.length; index++) {
|
||||
statement.setTimestamp(index + 1, Timestamp.from(arguments[index]));
|
||||
}
|
||||
List<String> ids = new ArrayList<>();
|
||||
try (ResultSet rows = statement.executeQuery()) {
|
||||
while (rows.next()) {
|
||||
ids.add(rows.getString(1));
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
|
||||
private String state(String operationId) throws SQLException {
|
||||
return column(operationId, "state");
|
||||
}
|
||||
|
||||
private String leaseOwner(String operationId) throws SQLException {
|
||||
return column(operationId, "lease_owner");
|
||||
}
|
||||
|
||||
private String resultReference(String operationId) throws SQLException {
|
||||
return column(operationId, "result_reference");
|
||||
}
|
||||
|
||||
private Instant startedAt(String operationId) throws SQLException {
|
||||
try (Connection connection = support.connection();
|
||||
PreparedStatement statement =
|
||||
connection.prepareStatement(
|
||||
"SELECT started_at FROM durable_operation WHERE operation_id = ?")) {
|
||||
statement.setString(1, operationId);
|
||||
try (ResultSet rows = statement.executeQuery()) {
|
||||
rows.next();
|
||||
return rows.getTimestamp(1).toInstant();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String column(String operationId, String name) throws SQLException {
|
||||
try (Connection connection = support.connection();
|
||||
PreparedStatement statement =
|
||||
connection.prepareStatement(
|
||||
"SELECT " + name + " FROM durable_operation WHERE operation_id = ?")) {
|
||||
statement.setString(1, operationId);
|
||||
try (ResultSet rows = statement.executeQuery()) {
|
||||
rows.next();
|
||||
return rows.getString(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void execute(String sql) throws SQLException {
|
||||
try (Connection connection = support.connection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
statement.execute(sql);
|
||||
}
|
||||
}
|
||||
|
||||
private static String migration() throws IOException {
|
||||
try (InputStream resource =
|
||||
DurableOperationStoreContractTest.class
|
||||
.getClassLoader()
|
||||
.getResourceAsStream("db/migration/postgresql/V11__durable_operation.sql")) {
|
||||
if (resource == null) {
|
||||
throw new IOException("the durable operation migration is missing from the classpath");
|
||||
}
|
||||
return new String(resource.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
command=./gradlew :adapter:outbound:persistence-jpa:test --tests dev.caskeleton.adapter.outbound.persistence.outbox.OutboxStoreAdapterTest.analysisProbeStaleWorkerCanRegressPublishedRowToFailed --rerun-tasks --console=plain --no-daemon
|
||||
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
|
||||
Daemon will be stopped at the end of the build
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
> Task :shared-contract:compileJava
|
||||
> Task :shared-contract:processResources
|
||||
> Task :shared-contract:classes
|
||||
> Task :shared-contract:jar
|
||||
> Task :application-core:compileJava
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes
|
||||
> Task :application-core:jar
|
||||
> Task :adapter:outbound:persistence-jpa:compileJava
|
||||
> Task :adapter:outbound:persistence-jpa:processResources
|
||||
> Task :adapter:outbound:persistence-jpa:classes
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testClasses
|
||||
> Task :adapter:outbound:persistence-jpa:test
|
||||
|
||||
BUILD SUCCESSFUL in 37s
|
||||
18 actionable tasks: 18 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
=== ASSERTED OBSERVATION (from Gradle test XML) ===
|
||||
outboxStaleWorker.before=PUBLISHED
|
||||
outboxStaleWorker.after=FAILED
|
||||
outboxStaleWorker.retryAt=2026-06-11T10:00:30Z
|
||||
source restored after probe: yes
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
package dev.caskeleton.adapter.outbound.persistence.outbox;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentCaptor.forClass;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import dev.caskeleton.adapter.outbound.persistence.outbox.entity.OutboxEventEntity;
|
||||
import dev.caskeleton.application.outbox.NewOutboxEvent;
|
||||
import dev.caskeleton.application.outbox.OutboxEvent;
|
||||
import dev.caskeleton.application.outbox.OutboxEventStatus;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link OutboxStoreAdapter} — covers append mapping, claim transition, mark
|
||||
* operations, countByStatus, and oldestUnpublishedAgeSecondsByEventType.
|
||||
*
|
||||
* <h2>FIFO gate location contract</h2>
|
||||
*
|
||||
* <p>The per-aggregate FIFO gate (I4) lives entirely in the {@link
|
||||
* OutboxClaimRepository#claimEligible} native SQL query via a {@code NOT EXISTS} correlated
|
||||
* subquery. The adapter itself performs NO in-memory filtering: every row returned by the claim
|
||||
* repository passes through to the caller. Verifying that the SQL gate enforces FIFO is the
|
||||
* responsibility of the Testcontainers contract test in {@code app-bootstrap}.
|
||||
*
|
||||
* <p>Real SKIP LOCKED / FIFO gate behaviour is covered by the Task E PG contract tests.
|
||||
*/
|
||||
class OutboxStoreAdapterTest {
|
||||
|
||||
private static final Instant NOW = Instant.parse("2026-06-11T10:00:00Z");
|
||||
private static final Duration IN_FLIGHT_TIMEOUT = Duration.ofMinutes(5);
|
||||
|
||||
private final OutboxEventJpaRepository repo = mock(OutboxEventJpaRepository.class);
|
||||
private final OutboxClaimRepository claimRepo = mock(OutboxClaimRepository.class);
|
||||
private final OutboxStoreAdapter adapter = new OutboxStoreAdapter(repo, claimRepo);
|
||||
|
||||
// ---- helper ----
|
||||
|
||||
private static NewOutboxEvent newEvent(String eventId) {
|
||||
return new NewOutboxEvent(eventId, "UserCreated", "agg-1", "{}", NOW, "corr-1", "idem-1");
|
||||
}
|
||||
|
||||
private static OutboxEventEntity pendingEntity(String eventId) {
|
||||
OutboxEventEntity e = new OutboxEventEntity();
|
||||
e.setEventId(eventId);
|
||||
e.setAggregateId("agg-1");
|
||||
e.setEventType("UserCreated");
|
||||
e.setPayload("{}");
|
||||
e.setOccurredAt(NOW.minusSeconds(60));
|
||||
e.setStatus("PENDING");
|
||||
e.setAttemptCount(0);
|
||||
e.setNextAttemptAt(NOW.minusSeconds(60));
|
||||
e.setCorrelationId("corr-1");
|
||||
e.setIdempotencyKey("idem-1");
|
||||
return e;
|
||||
}
|
||||
|
||||
// ---- append ----
|
||||
|
||||
@Test
|
||||
void appendPersistsEntityWithCorrectFieldMapping() {
|
||||
NewOutboxEvent event = newEvent("evt-001");
|
||||
|
||||
adapter.append(event);
|
||||
|
||||
ArgumentCaptor<OutboxEventEntity> saved = forClass(OutboxEventEntity.class);
|
||||
verify(repo).save(saved.capture());
|
||||
OutboxEventEntity entity = saved.getValue();
|
||||
|
||||
assertThat(entity.getEventId()).isEqualTo("evt-001");
|
||||
assertThat(entity.getEventType()).isEqualTo("UserCreated");
|
||||
assertThat(entity.getAggregateId()).isEqualTo("agg-1");
|
||||
assertThat(entity.getPayload()).isEqualTo("{}");
|
||||
assertThat(entity.getOccurredAt()).isEqualTo(NOW);
|
||||
assertThat(entity.getStatus()).isEqualTo("PENDING");
|
||||
assertThat(entity.getAttemptCount()).isEqualTo(0);
|
||||
assertThat(entity.getNextAttemptAt()).isEqualTo(NOW); // nextAttemptAt = occurredAt on PENDING
|
||||
assertThat(entity.getCorrelationId()).isEqualTo("corr-1");
|
||||
assertThat(entity.getIdempotencyKey()).isEqualTo("idem-1");
|
||||
}
|
||||
|
||||
// ---- claimBatch ----
|
||||
|
||||
@Test
|
||||
void claimBatchTransitionsEntityToInFlightAndReturnsMappedEvent() {
|
||||
OutboxEventEntity entity = pendingEntity("evt-002");
|
||||
when(claimRepo.claimEligible(eq(NOW), eq(2))).thenReturn(List.of(entity));
|
||||
|
||||
List<OutboxEvent> claimed = adapter.claimBatch(2, NOW, IN_FLIGHT_TIMEOUT);
|
||||
|
||||
// status and attemptCount updated on entity
|
||||
assertThat(entity.getStatus()).isEqualTo("IN_FLIGHT");
|
||||
assertThat(entity.getAttemptCount()).isEqualTo(1);
|
||||
assertThat(entity.getNextAttemptAt()).isEqualTo(NOW.plus(IN_FLIGHT_TIMEOUT));
|
||||
|
||||
// returned OutboxEvent reflects post-transition state
|
||||
assertThat(claimed).hasSize(1);
|
||||
OutboxEvent result = claimed.get(0);
|
||||
assertThat(result.eventId()).isEqualTo("evt-002");
|
||||
assertThat(result.status()).isEqualTo(OutboxEventStatus.IN_FLIGHT);
|
||||
assertThat(result.attemptCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void claimBatchReturnsEmptyWhenRepoReturnsNothing() {
|
||||
when(claimRepo.claimEligible(any(), anyInt())).thenReturn(List.of());
|
||||
|
||||
assertThat(adapter.claimBatch(5, NOW, IN_FLIGHT_TIMEOUT)).isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression guard: the per-aggregate FIFO gate lives in the SQL query (I4). The adapter must NOT
|
||||
* apply any in-memory filtering — all rows returned by the repository must appear in the result,
|
||||
* including multiple rows for the same aggregate.
|
||||
*
|
||||
* <p>If this test fails it means in-memory filtering was re-introduced in the adapter; the fix is
|
||||
* to move that logic back to the {@code claimEligible} SQL query.
|
||||
*/
|
||||
@Test
|
||||
void claimBatchPassesAllRepoResultsThroughWithoutInMemoryFifoFiltering() {
|
||||
// Two rows for the same aggregate — repo returns both (SQL gate already enforces FIFO)
|
||||
OutboxEventEntity head = pendingEntity("evt-head");
|
||||
OutboxEventEntity tail = new OutboxEventEntity();
|
||||
tail.setEventId("evt-tail");
|
||||
tail.setAggregateId("agg-1"); // same aggregate as head
|
||||
tail.setEventType("UserCreated");
|
||||
tail.setPayload("{}");
|
||||
tail.setOccurredAt(NOW.minusSeconds(30)); // later than head (head is at -60s)
|
||||
tail.setStatus("PENDING");
|
||||
tail.setAttemptCount(0);
|
||||
tail.setNextAttemptAt(NOW.minusSeconds(30));
|
||||
tail.setCorrelationId("corr-2");
|
||||
tail.setIdempotencyKey("idem-2");
|
||||
|
||||
when(claimRepo.claimEligible(eq(NOW), eq(10))).thenReturn(List.of(head, tail));
|
||||
|
||||
List<OutboxEvent> claimed = adapter.claimBatch(10, NOW, IN_FLIGHT_TIMEOUT);
|
||||
|
||||
// Adapter must return both rows — no in-memory FIFO filtering
|
||||
assertThat(claimed)
|
||||
.hasSize(2)
|
||||
.extracting(OutboxEvent::eventId)
|
||||
.containsExactly("evt-head", "evt-tail");
|
||||
}
|
||||
|
||||
// ---- markPublished ----
|
||||
|
||||
@Test
|
||||
void markPublishedUpdatesStatusToPublished() {
|
||||
OutboxEventEntity entity = pendingEntity("evt-003");
|
||||
entity.setStatus("IN_FLIGHT");
|
||||
when(repo.findById("evt-003")).thenReturn(Optional.of(entity));
|
||||
|
||||
adapter.markPublished("evt-003");
|
||||
|
||||
assertThat(entity.getStatus()).isEqualTo("PUBLISHED");
|
||||
}
|
||||
|
||||
// ---- markFailed ----
|
||||
|
||||
@Test
|
||||
void markFailedSetsFailedStatusAndNextAttemptAt() {
|
||||
OutboxEventEntity entity = pendingEntity("evt-004");
|
||||
entity.setStatus("IN_FLIGHT");
|
||||
when(repo.findById("evt-004")).thenReturn(Optional.of(entity));
|
||||
Instant retryAt = NOW.plus(Duration.ofSeconds(30));
|
||||
|
||||
adapter.markFailed("evt-004", retryAt);
|
||||
|
||||
assertThat(entity.getStatus()).isEqualTo("FAILED");
|
||||
assertThat(entity.getNextAttemptAt()).isEqualTo(retryAt);
|
||||
}
|
||||
|
||||
// ---- markDead ----
|
||||
|
||||
@Test
|
||||
void markDeadSetsDeadStatus() {
|
||||
OutboxEventEntity entity = pendingEntity("evt-005");
|
||||
entity.setStatus("IN_FLIGHT");
|
||||
when(repo.findById("evt-005")).thenReturn(Optional.of(entity));
|
||||
|
||||
adapter.markDead("evt-005");
|
||||
|
||||
assertThat(entity.getStatus()).isEqualTo("DEAD");
|
||||
}
|
||||
|
||||
// ---- countByStatus ----
|
||||
|
||||
@Test
|
||||
void countByStatusMapsProjectionResultsToStatusEnumMap() {
|
||||
List<Object[]> rows =
|
||||
List.of(
|
||||
new Object[] {"PENDING", 5L},
|
||||
new Object[] {"IN_FLIGHT", 2L},
|
||||
new Object[] {"PUBLISHED", 10L});
|
||||
when(repo.countGroupedByStatus()).thenReturn(rows);
|
||||
|
||||
Map<OutboxEventStatus, Long> counts = adapter.countByStatus();
|
||||
|
||||
assertThat(counts).containsEntry(OutboxEventStatus.PENDING, 5L);
|
||||
assertThat(counts).containsEntry(OutboxEventStatus.IN_FLIGHT, 2L);
|
||||
assertThat(counts).containsEntry(OutboxEventStatus.PUBLISHED, 10L);
|
||||
assertThat(counts).doesNotContainKey(OutboxEventStatus.FAILED);
|
||||
assertThat(counts).doesNotContainKey(OutboxEventStatus.DEAD);
|
||||
}
|
||||
|
||||
// ---- oldestUnpublishedAgeSecondsByEventType ----
|
||||
|
||||
@Test
|
||||
void oldestUnpublishedAgeComputesSecondsFromOldestRowOccurredAt() {
|
||||
// oldest row for "UserCreated" occurred 120 seconds before NOW
|
||||
Instant oldestAt = NOW.minusSeconds(120);
|
||||
List<Object[]> rows = List.<Object[]>of(new Object[] {"UserCreated", oldestAt});
|
||||
when(repo.findOldestUnpublishedOccurredAtByEventType()).thenReturn(rows);
|
||||
|
||||
Map<String, Long> ages = adapter.oldestUnpublishedAgeSecondsByEventType(NOW);
|
||||
|
||||
assertThat(ages).containsEntry("UserCreated", 120L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void oldestUnpublishedAgeReturnsEmptyMapWhenNoUnpublishedRows() {
|
||||
when(repo.findOldestUnpublishedOccurredAtByEventType()).thenReturn(List.of());
|
||||
|
||||
assertThat(adapter.oldestUnpublishedAgeSecondsByEventType(NOW)).isEmpty();
|
||||
}
|
||||
|
||||
// ---- missing-entity guard (fail-closed consistency) ----
|
||||
|
||||
/**
|
||||
* When the outbox row is not found during markPublished, the adapter must throw {@link
|
||||
* IllegalStateException} with the eventId in the message instead of silently doing nothing. A
|
||||
* silent no-op leaves the row IN_FLIGHT forever, blocking the aggregate's FIFO queue with no
|
||||
* error observable by the relay.
|
||||
*/
|
||||
@Test
|
||||
void markPublishedThrowsWhenEventIdNotFound() {
|
||||
when(repo.findById("missing-evt")).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> adapter.markPublished("missing-evt"))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("missing-evt");
|
||||
}
|
||||
|
||||
/**
|
||||
* When the outbox row is not found during markFailed, the adapter must throw {@link
|
||||
* IllegalStateException} with the eventId in the message. A silent no-op would leave the relay
|
||||
* believing a retry is scheduled while the row remains IN_FLIGHT, blocking the aggregate queue
|
||||
* and producing no error log.
|
||||
*/
|
||||
@Test
|
||||
void markFailedThrowsWhenEventIdNotFound() {
|
||||
when(repo.findById("missing-evt")).thenReturn(Optional.empty());
|
||||
Instant retryAt = NOW.plus(Duration.ofSeconds(30));
|
||||
|
||||
assertThatThrownBy(() -> adapter.markFailed("missing-evt", retryAt))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("missing-evt");
|
||||
}
|
||||
|
||||
/**
|
||||
* When the outbox row is not found during markDead, the adapter must throw {@link
|
||||
* IllegalStateException} with the eventId in the message. A silent no-op would leave the row
|
||||
* IN_FLIGHT instead of DEAD, preventing runbook-level visibility and manual resolution.
|
||||
*/
|
||||
@Test
|
||||
void markDeadThrowsWhenEventIdNotFound() {
|
||||
when(repo.findById("missing-evt")).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> adapter.markDead("missing-evt"))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("missing-evt");
|
||||
}
|
||||
@Test
|
||||
void analysisProbeStaleWorkerCanRegressPublishedRowToFailed() {
|
||||
OutboxEventEntity entity = pendingEntity("evt-stale");
|
||||
entity.setStatus("PUBLISHED");
|
||||
when(repo.findById("evt-stale")).thenReturn(Optional.of(entity));
|
||||
Instant retryAt = NOW.plus(Duration.ofSeconds(30));
|
||||
|
||||
adapter.markFailed("evt-stale", retryAt);
|
||||
|
||||
System.out.println("outboxStaleWorker.before=PUBLISHED");
|
||||
System.out.println("outboxStaleWorker.after=" + entity.getStatus());
|
||||
System.out.println("outboxStaleWorker.retryAt=" + entity.getNextAttemptAt());
|
||||
assertThat(entity.getStatus()).isEqualTo("FAILED");
|
||||
}
|
||||
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
> Task :shared-contract:compileJava
|
||||
> Task :shared-contract:processResources
|
||||
> Task :shared-contract:classes
|
||||
> Task :shared-contract:jar
|
||||
> Task :application-core:compileJava
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes
|
||||
> Task :application-core:jar
|
||||
> Task :adapter:outbound:persistence-jpa:compileJava
|
||||
> Task :adapter:outbound:persistence-jpa:processResources
|
||||
> Task :adapter:outbound:persistence-jpa:classes
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testClasses
|
||||
> Task :adapter:outbound:persistence-jpa:test
|
||||
|
||||
BUILD SUCCESSFUL in 38s
|
||||
18 actionable tasks: 18 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
Sub-scope 07 Fileserver persistence manifest
|
||||
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
generatedAt=2026-08-29T11:59:28Z
|
||||
production=25 tests=0 resources=4 total=29
|
||||
|
||||
=== PRODUCTION ===
|
||||
ad874e26e83fce74dc666dfc7278570ef8f5840f src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/FileEntityMapper.java
|
||||
4508a945e2f6339371ae414143428826c96701f6 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/FileserverJpaPersistenceConfig.java
|
||||
136bd818df67c33998c87f7c317745154e0b6516 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/FileserverSchemaActivation.java
|
||||
019fb271540fab8d62b3b2efb372a71158605e82 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaCleanupQueue.java
|
||||
a07bcb2fae88ff4cf924c195be0db9aa682c2ea0 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaContentReferenceLedger.java
|
||||
980aa08cbabcb7104f5dac7f0f1185a6456d1562 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaFileMetadataStore.java
|
||||
8309ecb7e2affa32a299ce355a3fda95cde6da3f src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaFileQuotaService.java
|
||||
764dbe19792cf44f31ca8807329b056747799a40 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaQuotaCommitGateway.java
|
||||
59ed25ded31efec4b491bc2bdb2c800c41879823 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaQuotaReclaimGateway.java
|
||||
12c252024161f166854c7229ce6136e0720e7106 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaRecoveryQueue.java
|
||||
56b8424d4c0456fdd7415cd10f8ac85a1ccaeda5 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaStagingUploadLocator.java
|
||||
4639a54e5bed3ea0117df2c51f939e34d1e84126 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaUploadSessionStore.java
|
||||
7278c4707be3b9a214b1fc644b727fb49eeaf991 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/CleanupItemEntity.java
|
||||
5d285dd9d192ee6ac63abc6872f8d0ff0c6addb5 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/FileEntity.java
|
||||
aade605e847a7d38e08c26e418de7765760a5f41 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/QuotaReservationEntity.java
|
||||
83b8324709117a6f7b667f22286e26f72c59e5fc src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/RecoveryItemEntity.java
|
||||
26f6be44cd09220b02b3299fe0e3ca0bc6149be9 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/UploadSessionEntity.java
|
||||
be32b59a228ee3eb72ff5f205dfbd3cdd7e7aca0 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/VerificationResultEntity.java
|
||||
60abdfd28809d998da203415392f3a4c06ed3029 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/FileTransitionRepository.java
|
||||
e4ff72f4effe8104c8d0f896ab4e099357eebdd6 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/FileserverCleanupRepository.java
|
||||
cac64f32e6548d25a4e30fa1f18daa1b7a8774fe src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/FileserverQuotaRepository.java
|
||||
fcb7475ac206e8fa9de296f9ee0f3f117ed70707 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/FileserverRecoveryRepository.java
|
||||
46270df09eab0003f8223a67287c9d1ee507e342 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/JpaFileRepository.java
|
||||
61f265216b6aa0382aa090c8fe270709c27f289f src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/JpaUploadSessionRepository.java
|
||||
ac9bdec31a8899ecea1e6e166e560fb47125843a src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/UploadLeaseRepository.java
|
||||
|
||||
=== RESOURCE ===
|
||||
414f9ce715410e111d36794f4fa435ddee7c2b1f src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V1__create_fileserver_metadata.sql
|
||||
ae76061212a359ca569927e5d5ed61c35c85a1df src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V2__fileserver_recovery_and_staging_cleanup.sql
|
||||
f98200909e0045b71a644d458c71742c9c71f928 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V3__fileserver_fenced_cleanup_lease.sql
|
||||
d928c244ca0767498099221c43e68680fbc2ed27 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V4__fileserver_upload_terminal_state.sql
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
command=./gradlew :adapter:outbound:persistence-jpa:postgresqlFileserverReclamationIntegrationTest --rerun-tasks --console=plain --no-daemon
|
||||
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
|
||||
Daemon will be stopped at the end of the build
|
||||
> Task :build-logic:extractPluginRequests
|
||||
> Task :build-logic:generatePluginAdapters
|
||||
> Task :build-logic:compileJava
|
||||
> Task :build-logic:compileGroovy
|
||||
> Task :build-logic:compileGroovyPlugins
|
||||
> Task :build-logic:pluginDescriptors
|
||||
> Task :build-logic:processResources
|
||||
> Task :build-logic:classes
|
||||
> Task :build-logic:jar
|
||||
> Task :shared-contract:compileJava
|
||||
> Task :shared-contract:processResources
|
||||
> Task :shared-contract:classes
|
||||
> Task :shared-contract:jar
|
||||
> Task :application-core:compileJava
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes
|
||||
> Task :application-core:jar
|
||||
> Task :adapter:outbound:persistence-jpa:compileJava
|
||||
> Task :adapter:outbound:persistence-jpa:processResources
|
||||
> Task :adapter:outbound:persistence-jpa:classes
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses
|
||||
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava
|
||||
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources
|
||||
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses
|
||||
> Task :adapter:outbound:persistence-jpa:postgresqlFileserverReclamationIntegrationTest
|
||||
|
||||
BUILD SUCCESSFUL in 45s
|
||||
19 actionable tasks: 19 executed
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
=== ASSERTED OBSERVATION (from Gradle test XML) ===
|
||||
fileserverExpiredQuota.status=COMMITTED
|
||||
fileserverExpiredQuota.committedBytes=600
|
||||
fileserverQuotaReclaim.before=65
|
||||
fileserverQuotaReclaim.requested=65
|
||||
fileserverQuotaReclaim.after=1
|
||||
source restored after probe: yes
|
||||
+549
@@ -0,0 +1,549 @@
|
||||
package dev.caskeleton.adapter.outbound.persistence.readiness;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaCleanupQueue;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaContentReferenceLedger;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaFileMetadataStore;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaFileQuotaService;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaQuotaCommitGateway;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaQuotaReclaimGateway;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaRecoveryQueue;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaStagingUploadLocator;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaUploadSessionStore;
|
||||
import dev.caskeleton.application.fileserver.api.ContentKey;
|
||||
import dev.caskeleton.application.fileserver.api.FileId;
|
||||
import dev.caskeleton.application.fileserver.api.FileState;
|
||||
import dev.caskeleton.application.fileserver.api.StorageNamespace;
|
||||
import dev.caskeleton.application.fileserver.api.UploadId;
|
||||
import dev.caskeleton.application.fileserver.api.metadata.FileRecord;
|
||||
import dev.caskeleton.application.fileserver.api.metadata.FileRecordDraft;
|
||||
import dev.caskeleton.application.fileserver.api.metadata.FileRecordMutation;
|
||||
import dev.caskeleton.application.fileserver.api.metadata.QuotaScope;
|
||||
import dev.caskeleton.application.fileserver.api.metadata.UploadSession;
|
||||
import dev.caskeleton.application.fileserver.api.metadata.UploadSessionDraft;
|
||||
import dev.caskeleton.application.fileserver.api.transfer.UploadProtocol;
|
||||
import dev.caskeleton.application.fileserver.cleanup.CleanupItem;
|
||||
import dev.caskeleton.application.fileserver.cleanup.CleanupRequest;
|
||||
import dev.caskeleton.application.fileserver.cleanup.CleanupType;
|
||||
import dev.caskeleton.application.fileserver.recovery.ReconciliationStatus;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
/**
|
||||
* Real-PostgreSQL proof of the reclamation side of the Fileserver.
|
||||
*
|
||||
* <p>The cleanup queue, the recovery queue, and the quota ledger are the components that decide
|
||||
* when physical bytes may be destroyed and how much space a tenant is charged for. All three are
|
||||
* conditional-update designs whose correctness lives in SQL, so an in-memory fake would prove
|
||||
* nothing about them: the partial unique index, the {@code IN ('PENDING','FAILED')} claim guard,
|
||||
* and the {@code committedBytes >= :amount} floor only exist in the database.
|
||||
*/
|
||||
class PostgreSqlFileserverReclamationIntegrationTest {
|
||||
|
||||
private static PostgreSqlReadinessSupport postgres;
|
||||
private static FileserverJpaTestContext context;
|
||||
private static JdbcTemplate jdbc;
|
||||
private static TransactionTemplate transactions;
|
||||
private static JpaFileMetadataStore files;
|
||||
private static JpaUploadSessionStore uploads;
|
||||
private static JpaFileQuotaService quota;
|
||||
private static JpaCleanupQueue cleanupQueue;
|
||||
private static JpaRecoveryQueue recoveryQueue;
|
||||
private static JpaQuotaCommitGateway quotaCommit;
|
||||
private static JpaQuotaReclaimGateway quotaReclaim;
|
||||
private static JpaContentReferenceLedger ledger;
|
||||
private static JpaStagingUploadLocator stagingLocator;
|
||||
|
||||
@BeforeAll
|
||||
static void startAndMigratePostgreSql() {
|
||||
PostgreSqlReadinessSupport.assertDockerAvailable();
|
||||
postgres = PostgreSqlReadinessSupport.start(8, 2_000);
|
||||
migrate("classpath:db/migration/postgresql", "flyway_schema_history");
|
||||
migrateIndependent(
|
||||
"classpath:db/migration/jpa/core", "flyway_jpa_core_history", "explicit-jpa-core-adoption");
|
||||
migrateIndependent(
|
||||
"classpath:db/migration/jpa/fileserver",
|
||||
"flyway_jpa_fileserver_history",
|
||||
"explicit-jpa-fileserver-adoption");
|
||||
|
||||
jdbc = new JdbcTemplate(postgres.dataSource());
|
||||
context = new FileserverJpaTestContext(postgres.dataSource(), Clock.systemUTC());
|
||||
transactions = context.transactions();
|
||||
files = context.bean(JpaFileMetadataStore.class);
|
||||
uploads = context.bean(JpaUploadSessionStore.class);
|
||||
quota = context.bean(JpaFileQuotaService.class);
|
||||
cleanupQueue = context.bean(JpaCleanupQueue.class);
|
||||
recoveryQueue = context.bean(JpaRecoveryQueue.class);
|
||||
quotaCommit = context.bean(JpaQuotaCommitGateway.class);
|
||||
quotaReclaim = context.bean(JpaQuotaReclaimGateway.class);
|
||||
ledger = context.bean(JpaContentReferenceLedger.class);
|
||||
stagingLocator = context.bean(JpaStagingUploadLocator.class);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void stopPostgreSql() {
|
||||
if (context != null) {
|
||||
context.close();
|
||||
}
|
||||
if (postgres != null) {
|
||||
postgres.close();
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void truncate() {
|
||||
jdbc.execute(
|
||||
"TRUNCATE fs_recovery_item, fs_cleanup_item, fs_quota_reservation,"
|
||||
+ " fs_verification_result, fs_upload_session, fs_file CASCADE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aStagingCleanupSurvivesTheRoundTripWithItsUploadIdentity() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadId uploadId = UploadId.of(UUID.randomUUID());
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.enqueue(
|
||||
CleanupRequest.forStaging(CleanupType.CANCELLED_STAGING, record.fileId(), uploadId));
|
||||
return null;
|
||||
});
|
||||
|
||||
List<CleanupItem> claimed = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10));
|
||||
|
||||
assertThat(claimed)
|
||||
.singleElement()
|
||||
.satisfies(
|
||||
item -> {
|
||||
assertThat(item.request().uploadId()).contains(uploadId);
|
||||
assertThat(item.request().contentKey()).isEmpty();
|
||||
assertThat(item.type()).isEqualTo(CleanupType.CANCELLED_STAGING);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void aClaimedItemIsNotHandedToASecondWorker() {
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.enqueue(
|
||||
CleanupRequest.forContent(
|
||||
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
|
||||
return null;
|
||||
});
|
||||
|
||||
List<CleanupItem> first = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10));
|
||||
List<CleanupItem> second = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10));
|
||||
|
||||
assertThat(first).hasSize(1);
|
||||
assertThat(second).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void aFailedItemBecomesDueAgainOnlyAfterItsBackoff() {
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.enqueue(
|
||||
CleanupRequest.forContent(
|
||||
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
|
||||
return null;
|
||||
});
|
||||
CleanupItem item = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10)).get(0);
|
||||
Instant retryAt = Instant.now().plus(Duration.ofMinutes(5));
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.markFailed(item, "STORAGE_UNAVAILABLE", retryAt);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10))).isEmpty();
|
||||
assertThat(inTransaction(() -> cleanupQueue.claimDue(retryAt.plusSeconds(1), 10))).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void anItemThatKeepsFailingIsAbandonedRatherThanRetriedForever() {
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.enqueue(
|
||||
CleanupRequest.forContent(
|
||||
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
|
||||
return null;
|
||||
});
|
||||
|
||||
Instant due = Instant.now();
|
||||
for (int attempt = 0; attempt < JpaCleanupQueue.MAXIMUM_ATTEMPTS; attempt++) {
|
||||
List<CleanupItem> claimed = inTransaction(() -> cleanupQueue.claimDue(due, 10));
|
||||
if (claimed.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.markFailed(claimed.get(0), "STILL_FAILING", due);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
assertThat(inTransaction(() -> cleanupQueue.claimDue(due.plusSeconds(1), 10))).isEmpty();
|
||||
assertThat(statusCounts("fs_cleanup_item", "ABANDONED")).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void aDoneItemIsNeverClaimedAgain() {
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.enqueue(
|
||||
CleanupRequest.forContent(
|
||||
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
|
||||
return null;
|
||||
});
|
||||
CleanupItem item = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10)).get(0);
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.markDone(item);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> cleanupQueue.claimDue(Instant.now().plusSeconds(600), 10)))
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void thesameFileReportedTwiceHoldsOneOpenRecoveryItem() {
|
||||
FileRecord record = insertRecord();
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
recoveryQueue.enqueue(record.fileId(), "READY_DIGEST_MISMATCH");
|
||||
recoveryQueue.enqueue(record.fileId(), "READY_SIZE_MISMATCH");
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> recoveryQueue.pending(10))).containsExactly(record.fileId());
|
||||
assertThat(rowCount("fs_recovery_item")).isEqualTo(1);
|
||||
assertThat(jdbc.queryForObject("SELECT reason_code FROM fs_recovery_item", String.class))
|
||||
.isEqualTo("READY_SIZE_MISMATCH");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aResolvedRecoveryItemLeavesThePendingListButKeepsItsOutcome() {
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(
|
||||
() -> {
|
||||
recoveryQueue.enqueue(record.fileId(), "PUBLISH_EVIDENCE_INCOMPLETE");
|
||||
return null;
|
||||
});
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
recoveryQueue.resolve(record.fileId(), ReconciliationStatus.QUARANTINE_REQUIRED);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> recoveryQueue.pending(10))).isEmpty();
|
||||
assertThat(statusCounts("fs_recovery_item", "QUARANTINE_REQUIRED")).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void aResolvedFileCanBeRaisedAgainLater() {
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(
|
||||
() -> {
|
||||
recoveryQueue.enqueue(record.fileId(), "FIRST");
|
||||
recoveryQueue.resolve(record.fileId(), ReconciliationStatus.UNRESOLVED);
|
||||
recoveryQueue.enqueue(record.fileId(), "SECOND");
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> recoveryQueue.pending(10))).containsExactly(record.fileId());
|
||||
assertThat(rowCount("fs_recovery_item")).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void committingAnUploadMovesReservedBytesToCommitted() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadSession session = insertSession(record.fileId());
|
||||
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
|
||||
inTransaction(() -> quota.reserve(scope, 1_000, Duration.ofHours(1)));
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaCommit.commit(session, 600);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> quota.reservedBytes(scope))).isZero();
|
||||
assertThat(inTransaction(() -> quota.committedBytes(scope))).isEqualTo(600);
|
||||
}
|
||||
|
||||
@Test
|
||||
void committingWithoutALiveReservationStillRecordsTheDurableUsage() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadSession session = insertSession(record.fileId());
|
||||
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaCommit.commit(session, 450);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> quota.committedBytes(scope))).isEqualTo(450);
|
||||
}
|
||||
|
||||
@Test
|
||||
void releasingAnUploadGivesTheReservedCapacityBack() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadSession session = insertSession(record.fileId());
|
||||
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
|
||||
inTransaction(() -> quota.reserve(scope, 2_000, Duration.ofHours(1)));
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaCommit.release(session);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> quota.reservedBytes(scope))).isZero();
|
||||
assertThat(inTransaction(() -> quota.committedBytes(scope))).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void reclaimingDrawsCommittedBytesDownAcrossRows() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadSession session = insertSession(record.fileId());
|
||||
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaCommit.commit(session, 300);
|
||||
quotaCommit.commit(session, 700);
|
||||
return null;
|
||||
});
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaReclaim.reclaim(scope, 800);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> quota.committedBytes(scope))).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reclaimingMoreThanIsRecordedStopsAtZeroRatherThanGoingNegative() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadSession session = insertSession(record.fileId());
|
||||
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaCommit.commit(session, 100);
|
||||
return null;
|
||||
});
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaReclaim.reclaim(scope, 5_000);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> quota.committedBytes(scope))).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void theLedgerReportsAKeyAsReferencedOnlyWhileARecordNamesIt() {
|
||||
ContentKey key = contentKey();
|
||||
assertThat(inTransaction(() -> ledger.isReferenced(key))).isFalse();
|
||||
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(() -> publish(record, key));
|
||||
|
||||
assertThat(inTransaction(() -> ledger.isReferenced(key))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void theStagingLocatorReturnsTheNewestSessionForAFile() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadSession older = insertSession(record.fileId());
|
||||
UploadSession newer = insertSession(record.fileId());
|
||||
|
||||
Optional<UploadId> located = inTransaction(() -> stagingLocator.locate(record.fileId()));
|
||||
|
||||
assertThat(located).isPresent();
|
||||
assertThat(located.get()).isIn(older.uploadId(), newer.uploadId());
|
||||
assertThat(inTransaction(() -> stagingLocator.locate(FileId.of(UUID.randomUUID())))).isEmpty();
|
||||
}
|
||||
|
||||
private FileRecord insertRecord() {
|
||||
return inTransaction(
|
||||
() ->
|
||||
files.insert(
|
||||
new FileRecordDraft(
|
||||
FileId.of(UUID.randomUUID()),
|
||||
StorageNamespace.of("tenant-a"),
|
||||
"report.bin",
|
||||
Optional.of("application/octet-stream"),
|
||||
OptionalLong.of(1_000))));
|
||||
}
|
||||
|
||||
private FileRecord publish(FileRecord record, ContentKey key) {
|
||||
FileRecord uploading =
|
||||
files.transition(
|
||||
record.fileId(),
|
||||
record.version(),
|
||||
FileState.CREATED,
|
||||
FileState.UPLOADING,
|
||||
FileRecordMutation.none());
|
||||
FileRecord uploaded =
|
||||
files.transition(
|
||||
uploading.fileId(),
|
||||
uploading.version(),
|
||||
FileState.UPLOADING,
|
||||
FileState.UPLOADED,
|
||||
FileRecordMutation.uploaded(10, "b".repeat(64)));
|
||||
FileRecord verifying =
|
||||
files.transition(
|
||||
uploaded.fileId(),
|
||||
uploaded.version(),
|
||||
FileState.UPLOADED,
|
||||
FileState.VERIFYING,
|
||||
FileRecordMutation.none());
|
||||
return files.transition(
|
||||
verifying.fileId(),
|
||||
verifying.version(),
|
||||
FileState.VERIFYING,
|
||||
FileState.READY,
|
||||
FileRecordMutation.publishAt(
|
||||
key, 10, "b".repeat(64), "\"" + "b".repeat(64) + "\"", Instant.now()));
|
||||
}
|
||||
|
||||
private UploadSession insertSession(FileId fileId) {
|
||||
return inTransaction(
|
||||
() ->
|
||||
uploads.create(
|
||||
new UploadSessionDraft(
|
||||
UploadId.of(UUID.randomUUID()),
|
||||
fileId,
|
||||
UploadProtocol.RAW,
|
||||
OptionalLong.of(1_000),
|
||||
Instant.now().plus(Duration.ofHours(1)))));
|
||||
}
|
||||
|
||||
private static ContentKey contentKey() {
|
||||
String flat = UUID.randomUUID().toString().replace("-", "");
|
||||
return ContentKey.of(flat.substring(0, 2) + '/' + flat.substring(2, 4) + '/' + flat);
|
||||
}
|
||||
|
||||
private static <T> T inTransaction(Supplier<T> action) {
|
||||
return transactions.execute(status -> action.get());
|
||||
}
|
||||
|
||||
private static int rowCount(String table) {
|
||||
Integer count = jdbc.queryForObject("SELECT count(*) FROM " + table, Integer.class);
|
||||
return count == null ? 0 : count;
|
||||
}
|
||||
|
||||
private static int statusCounts(String table, String status) {
|
||||
Integer count =
|
||||
jdbc.queryForObject(
|
||||
"SELECT count(*) FROM " + table + " WHERE status = ?", Integer.class, status);
|
||||
return count == null ? 0 : count;
|
||||
}
|
||||
|
||||
private static void migrate(String location, String historyTable) {
|
||||
Flyway.configure()
|
||||
.dataSource(postgres.dataSource())
|
||||
.locations(location)
|
||||
.table(historyTable)
|
||||
.baselineOnMigrate(false)
|
||||
.outOfOrder(false)
|
||||
.load()
|
||||
.migrate();
|
||||
}
|
||||
|
||||
private static void migrateIndependent(
|
||||
String location, String historyTable, String baselineDescription) {
|
||||
Flyway flyway =
|
||||
Flyway.configure()
|
||||
.dataSource(postgres.dataSource())
|
||||
.locations(location)
|
||||
.table(historyTable)
|
||||
.baselineVersion("0")
|
||||
.baselineDescription(baselineDescription)
|
||||
.baselineOnMigrate(false)
|
||||
.outOfOrder(false)
|
||||
.load();
|
||||
flyway.baseline();
|
||||
flyway.migrate();
|
||||
}
|
||||
|
||||
@Test
|
||||
void analysisProbeReclaimStopsAfterSixtyFourCommittedRows() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadSession session = insertSession(record.fileId());
|
||||
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
|
||||
|
||||
for (int i = 0; i < 65; i++) {
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaCommit.commit(session, 1);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
long before = inTransaction(() -> quota.committedBytes(scope));
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaReclaim.reclaim(scope, 65);
|
||||
return null;
|
||||
});
|
||||
long after = inTransaction(() -> quota.committedBytes(scope));
|
||||
|
||||
System.out.println("fileserverQuotaReclaim.before=" + before);
|
||||
System.out.println("fileserverQuotaReclaim.requested=65");
|
||||
System.out.println("fileserverQuotaReclaim.after=" + after);
|
||||
assertThat(before).isEqualTo(65);
|
||||
assertThat(after).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void analysisProbeExpiredDirectReservationStillCommits() {
|
||||
QuotaScope scope = QuotaScope.ofTenant("analysis-expired");
|
||||
var reservation = inTransaction(() -> quota.reserve(scope, 1000, Duration.ofHours(1)));
|
||||
jdbc.update(
|
||||
"update fs_quota_reservation set expires_at = now() - interval '1 minute' where reservation_id = ?",
|
||||
reservation.reservationId());
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
quota.commit(reservation, 600);
|
||||
return null;
|
||||
});
|
||||
|
||||
String status = jdbc.queryForObject(
|
||||
"select status from fs_quota_reservation where reservation_id = ?",
|
||||
String.class,
|
||||
reservation.reservationId());
|
||||
long committed = inTransaction(() -> quota.committedBytes(scope));
|
||||
System.out.println("fileserverExpiredQuota.status=" + status);
|
||||
System.out.println("fileserverExpiredQuota.committedBytes=" + committed);
|
||||
assertThat(status).isEqualTo("COMMITTED");
|
||||
assertThat(committed).isEqualTo(600);
|
||||
}
|
||||
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
|
||||
Daemon will be stopped at the end of the build
|
||||
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||
> Task :build-logic:compileJava UP-TO-DATE
|
||||
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||
> Task :build-logic:processResources UP-TO-DATE
|
||||
> Task :build-logic:classes UP-TO-DATE
|
||||
> Task :build-logic:jar UP-TO-DATE
|
||||
> Task :shared-contract:compileJava UP-TO-DATE
|
||||
> Task :shared-contract:processResources UP-TO-DATE
|
||||
> Task :shared-contract:classes UP-TO-DATE
|
||||
> Task :shared-contract:jar UP-TO-DATE
|
||||
> Task :application-core:compileJava UP-TO-DATE
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes UP-TO-DATE
|
||||
> Task :application-core:jar UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava
|
||||
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses
|
||||
> Task :adapter:outbound:persistence-jpa:postgresqlFileserverReclamationIntegrationTest
|
||||
|
||||
BUILD SUCCESSFUL in 18s
|
||||
19 actionable tasks: 2 executed, 17 up-to-date
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
=== SYSTEM-OUT ===
|
||||
fileserverCleanupCrash.maxAttempts=8
|
||||
fileserverCleanupCrash.actualAttempt=9
|
||||
fileserverCleanupCrash.status=FAILED
|
||||
fileserverCleanupCrash.lastError=CLAIM_LEASE_EXPIRED
|
||||
exitCode=0
|
||||
+723
@@ -0,0 +1,723 @@
|
||||
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
|
||||
=== production quota aggregate readers ===
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaFileQuotaService.java:85: return reservations.sumReservedBytes(scope.type(), scope.value(), clock.instant());
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaFileQuotaService.java:90: return reservations.sumCommittedBytes(scope.type(), scope.value());
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/FileserverQuotaRepository.java:75: long sumReservedBytes(
|
||||
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/FileserverQuotaRepository.java:87: long sumCommittedBytes(
|
||||
|
||||
=== quota ceiling config/symbol search ===
|
||||
src/application-core/src/main/java/dev/caskeleton/application/fileserver/api/error/QuotaExceededException.java:3:/** A quota scope reservation, commit, or concurrency limit rejected the request. */
|
||||
|
||||
=== DefaultTransferAdmissionController ===
|
||||
1 package dev.caskeleton.application.fileserver.quota;
|
||||
2
|
||||
3 import dev.caskeleton.application.fileserver.api.error.FileTooLargeException;
|
||||
4 import dev.caskeleton.application.fileserver.api.error.FileserverErrorCode;
|
||||
5 import dev.caskeleton.application.fileserver.api.error.FileserverFailureContext;
|
||||
6 import dev.caskeleton.application.fileserver.api.error.QuotaExceededException;
|
||||
7 import dev.caskeleton.application.fileserver.api.error.StorageFullException;
|
||||
8 import dev.caskeleton.application.fileserver.api.error.TransferAdmissionRejectedException;
|
||||
9 import dev.caskeleton.application.fileserver.api.metadata.QuotaScope;
|
||||
10 import java.util.Map;
|
||||
11 import java.util.OptionalDouble;
|
||||
12 import java.util.concurrent.ConcurrentHashMap;
|
||||
13 import java.util.concurrent.Semaphore;
|
||||
14 import java.util.concurrent.atomic.AtomicBoolean;
|
||||
15
|
||||
16 /**
|
||||
17 * In-process admission control backed by bounded semaphores.
|
||||
18 *
|
||||
19 * <p>The failure vocabulary is deliberately distinct so a client can tell a permanent policy denial
|
||||
20 * from a transient one: an exhausted pool is {@code STORAGE_FULL}, a scope over its ceiling is
|
||||
21 * {@code QUOTA_EXCEEDED}, and momentary permit exhaustion is a retryable admission rejection.
|
||||
22 */
|
||||
23 public final class DefaultTransferAdmissionController implements TransferAdmissionController {
|
||||
24
|
||||
25 private final TransferAdmissionProperties properties;
|
||||
26 private final StorageUsageProbe usageProbe;
|
||||
27 private final long maximumFileSize;
|
||||
28 private final Semaphore instanceUploads;
|
||||
29 private final Semaphore directDownloads;
|
||||
30 private final Map<String, Semaphore> scopeUploads = new ConcurrentHashMap<>();
|
||||
31
|
||||
32 public DefaultTransferAdmissionController(
|
||||
33 TransferAdmissionProperties properties, StorageUsageProbe usageProbe, long maximumFileSize) {
|
||||
34 if (maximumFileSize <= 0) {
|
||||
35 throw new IllegalArgumentException("maximumFileSize must be positive");
|
||||
36 }
|
||||
37 this.properties = properties;
|
||||
38 this.usageProbe = usageProbe;
|
||||
39 this.maximumFileSize = maximumFileSize;
|
||||
40 this.instanceUploads = new Semaphore(properties.instanceUploadPermits());
|
||||
41 this.directDownloads = new Semaphore(properties.directDownloadPermits());
|
||||
42 }
|
||||
43
|
||||
44 @Override
|
||||
45 public TransferPermit acquireUpload(QuotaScope scope, long requestedBytes) {
|
||||
46 requireWithinFileSizePolicy(requestedBytes);
|
||||
47 requireBelowHardHighWater();
|
||||
48 Semaphore scopePermits = scopePermits(scope);
|
||||
49 if (!scopePermits.tryAcquire()) {
|
||||
50 throw new QuotaExceededException(
|
||||
51 "scope upload concurrency is exhausted",
|
||||
52 FileserverFailureContext.of(FileserverErrorCode.QUOTA_EXCEEDED, true));
|
||||
53 }
|
||||
54 if (!instanceUploads.tryAcquire()) {
|
||||
55 scopePermits.release();
|
||||
56 throw new TransferAdmissionRejectedException(
|
||||
57 "instance upload permits are exhausted",
|
||||
58 FileserverFailureContext.of(FileserverErrorCode.TRANSFER_ADMISSION_REJECTED, true));
|
||||
59 }
|
||||
60 return new SemaphorePermit(scopePermits, instanceUploads);
|
||||
61 }
|
||||
62
|
||||
63 @Override
|
||||
64 public TransferPermit acquireDirectDownload(QuotaScope scope) {
|
||||
65 if (!directDownloads.tryAcquire()) {
|
||||
66 throw new TransferAdmissionRejectedException(
|
||||
67 "instance direct download permits are exhausted",
|
||||
68 FileserverFailureContext.of(FileserverErrorCode.TRANSFER_ADMISSION_REJECTED, true));
|
||||
69 }
|
||||
70 return new SemaphorePermit(directDownloads);
|
||||
71 }
|
||||
72
|
||||
73 /**
|
||||
74 * True when the pool crossed the soft mark.
|
||||
75 *
|
||||
76 * <p>Callers use this to throttle or defer large uploads while still accepting small ones.
|
||||
77 */
|
||||
78 public boolean isAboveSoftHighWater() {
|
||||
79 OptionalDouble used = usageProbe.usedFraction();
|
||||
80 return used.isPresent() && used.getAsDouble() >= properties.softHighWater();
|
||||
81 }
|
||||
82
|
||||
83 private void requireWithinFileSizePolicy(long requestedBytes) {
|
||||
84 if (requestedBytes > maximumFileSize) {
|
||||
85 throw new FileTooLargeException(
|
||||
86 "requested upload exceeds the configured maximum file size",
|
||||
87 FileserverFailureContext.of(FileserverErrorCode.FILE_TOO_LARGE, false));
|
||||
88 }
|
||||
89 }
|
||||
90
|
||||
91 private void requireBelowHardHighWater() {
|
||||
92 OptionalDouble used = usageProbe.usedFraction();
|
||||
93 if (used.isPresent() && used.getAsDouble() >= properties.hardHighWater()) {
|
||||
94 throw new StorageFullException(
|
||||
95 "storage pool crossed its hard high-water mark",
|
||||
96 FileserverFailureContext.of(FileserverErrorCode.STORAGE_FULL, false));
|
||||
97 }
|
||||
98 }
|
||||
99
|
||||
100 private Semaphore scopePermits(QuotaScope scope) {
|
||||
101 return scopeUploads.computeIfAbsent(
|
||||
102 scope.canonicalKey(), ignored -> new Semaphore(properties.scopeUploadPermits()));
|
||||
103 }
|
||||
104
|
||||
105 /** Releases each held semaphore exactly once, however many times {@code close} is called. */
|
||||
106 private static final class SemaphorePermit implements TransferPermit {
|
||||
107
|
||||
108 private final Semaphore[] held;
|
||||
109 private final AtomicBoolean released = new AtomicBoolean();
|
||||
110
|
||||
111 private SemaphorePermit(Semaphore... held) {
|
||||
112 this.held = held.clone();
|
||||
113 }
|
||||
114
|
||||
115 @Override
|
||||
116 public void close() {
|
||||
117 if (released.compareAndSet(false, true)) {
|
||||
118 for (Semaphore semaphore : held) {
|
||||
119 semaphore.release();
|
||||
120 }
|
||||
121 }
|
||||
122 }
|
||||
123
|
||||
124 @Override
|
||||
125 public boolean isHeld() {
|
||||
126 return !released.get();
|
||||
127 }
|
||||
128 }
|
||||
129 }
|
||||
|
||||
=== FileserverPlatformSettings quota ===
|
||||
/** Transfer admission control and storage high-water marks. */
|
||||
public record Quota(
|
||||
@DefaultValue("16") int instanceUploadPermits,
|
||||
@DefaultValue("4") int scopeUploadPermits,
|
||||
@DefaultValue("64") int directDownloadPermits,
|
||||
@DefaultValue("0.70") double softHighWater,
|
||||
@DefaultValue("0.85") double hardHighWater) {
|
||||
|
||||
public Quota {
|
||||
requirePositive(instanceUploadPermits, "quota.instance-upload-permits");
|
||||
requirePositive(scopeUploadPermits, "quota.scope-upload-permits");
|
||||
requirePositive(directDownloadPermits, "quota.direct-download-permits");
|
||||
requireFraction(softHighWater, "quota.soft-high-water");
|
||||
requireFraction(hardHighWater, "quota.hard-high-water");
|
||||
if (softHighWater >= hardHighWater) {
|
||||
throw new IllegalStateException(
|
||||
PREFIX + ".quota.soft-high-water must be below quota.hard-high-water");
|
||||
}
|
||||
if (scopeUploadPermits > instanceUploadPermits) {
|
||||
throw new IllegalStateException(
|
||||
PREFIX + ".quota.scope-upload-permits must not exceed quota.instance-upload-permits");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
=== JpaFileQuotaService ===
|
||||
1 package dev.caskeleton.adapter.outbound.persistence.fileserver;
|
||||
2
|
||||
3 import dev.caskeleton.adapter.outbound.persistence.fileserver.entity.QuotaReservationEntity;
|
||||
4 import dev.caskeleton.adapter.outbound.persistence.fileserver.repository.FileserverQuotaRepository;
|
||||
5 import dev.caskeleton.application.fileserver.api.error.FileserverErrorCode;
|
||||
6 import dev.caskeleton.application.fileserver.api.error.FileserverFailureContext;
|
||||
7 import dev.caskeleton.application.fileserver.api.error.QuotaExceededException;
|
||||
8 import dev.caskeleton.application.fileserver.api.metadata.FileQuotaService;
|
||||
9 import dev.caskeleton.application.fileserver.api.metadata.QuotaReservation;
|
||||
10 import dev.caskeleton.application.fileserver.api.metadata.QuotaReservationStatus;
|
||||
11 import dev.caskeleton.application.fileserver.api.metadata.QuotaScope;
|
||||
12 import java.time.Clock;
|
||||
13 import java.time.Duration;
|
||||
14 import java.time.Instant;
|
||||
15 import java.util.UUID;
|
||||
16 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
17 import org.springframework.stereotype.Repository;
|
||||
18
|
||||
19 /**
|
||||
20 * JPA-backed {@link FileQuotaService}.
|
||||
21 *
|
||||
22 * <p>Reservation, extension, commit, and release are conditional statements, so a reservation that
|
||||
23 * already expired or was released can never be extended or committed.
|
||||
24 */
|
||||
25 @Repository
|
||||
26 @ConditionalOnProperty(prefix = "app.fileserver-platform", name = "enabled", havingValue = "true")
|
||||
27 public class JpaFileQuotaService implements FileQuotaService {
|
||||
28
|
||||
29 private final FileserverQuotaRepository reservations;
|
||||
30 private final Clock clock;
|
||||
31
|
||||
32 public JpaFileQuotaService(FileserverQuotaRepository reservations, Clock clock) {
|
||||
33 this.reservations = reservations;
|
||||
34 this.clock = clock;
|
||||
35 }
|
||||
36
|
||||
37 @Override
|
||||
38 public QuotaReservation reserve(QuotaScope scope, long expectedBytes, Duration ttl) {
|
||||
39 if (expectedBytes < 0) {
|
||||
40 throw new IllegalArgumentException("expectedBytes must not be negative");
|
||||
41 }
|
||||
42 Instant now = clock.instant();
|
||||
43 QuotaReservationEntity entity =
|
||||
44 new QuotaReservationEntity(
|
||||
45 UUID.randomUUID(),
|
||||
46 scope.type(),
|
||||
47 scope.value(),
|
||||
48 expectedBytes,
|
||||
49 now.plus(ttl),
|
||||
50 QuotaReservationStatus.RESERVED.name(),
|
||||
51 now);
|
||||
52 return FileEntityMapper.toReservation(reservations.save(entity));
|
||||
53 }
|
||||
54
|
||||
55 @Override
|
||||
56 public void extend(QuotaReservation reservation, long additionalBytes) {
|
||||
57 if (additionalBytes < 0) {
|
||||
58 throw new IllegalArgumentException("additionalBytes must not be negative");
|
||||
59 }
|
||||
60 int updated =
|
||||
61 reservations.extend(reservation.reservationId(), additionalBytes, clock.instant());
|
||||
62 if (updated == 0) {
|
||||
63 throw quotaConflict("reservation is no longer extendable");
|
||||
64 }
|
||||
65 }
|
||||
66
|
||||
67 @Override
|
||||
68 public void commit(QuotaReservation reservation, long actualBytes) {
|
||||
69 if (actualBytes < 0) {
|
||||
70 throw new IllegalArgumentException("actualBytes must not be negative");
|
||||
71 }
|
||||
72 int updated = reservations.commit(reservation.reservationId(), actualBytes, clock.instant());
|
||||
73 if (updated == 0) {
|
||||
74 throw quotaConflict("reservation is no longer committable");
|
||||
75 }
|
||||
76 }
|
||||
77
|
||||
78 @Override
|
||||
79 public void release(QuotaReservation reservation) {
|
||||
80 reservations.release(reservation.reservationId(), clock.instant());
|
||||
81 }
|
||||
82
|
||||
83 /** Bytes currently reserved but not yet committed for a scope. */
|
||||
84 public long reservedBytes(QuotaScope scope) {
|
||||
85 return reservations.sumReservedBytes(scope.type(), scope.value(), clock.instant());
|
||||
86 }
|
||||
87
|
||||
88 /** Bytes durably committed for a scope. */
|
||||
89 public long committedBytes(QuotaScope scope) {
|
||||
90 return reservations.sumCommittedBytes(scope.type(), scope.value());
|
||||
91 }
|
||||
92
|
||||
93 private QuotaExceededException quotaConflict(String message) {
|
||||
94 return new QuotaExceededException(
|
||||
95 message, FileserverFailureContext.of(FileserverErrorCode.QUOTA_EXCEEDED, false));
|
||||
96 }
|
||||
97 }
|
||||
|
||||
=== quota repository ===
|
||||
1 package dev.caskeleton.adapter.outbound.persistence.fileserver.repository;
|
||||
2
|
||||
3 import dev.caskeleton.adapter.outbound.persistence.fileserver.entity.QuotaReservationEntity;
|
||||
4 import java.time.Instant;
|
||||
5 import java.util.List;
|
||||
6 import java.util.UUID;
|
||||
7 import org.springframework.data.domain.Limit;
|
||||
8 import org.springframework.data.jpa.repository.JpaRepository;
|
||||
9 import org.springframework.data.jpa.repository.Modifying;
|
||||
10 import org.springframework.data.jpa.repository.Query;
|
||||
11 import org.springframework.data.repository.query.Param;
|
||||
12
|
||||
13 /**
|
||||
14 * Conditional quota reservation statements.
|
||||
15 *
|
||||
16 * <p>Extend, commit, and release all require the reservation to still be {@code RESERVED} at the
|
||||
17 * expected version, so a reservation reclaimed by expiry cannot be resurrected.
|
||||
18 */
|
||||
19 public interface FileserverQuotaRepository extends JpaRepository<QuotaReservationEntity, UUID> {
|
||||
20
|
||||
21 @Modifying(clearAutomatically = true, flushAutomatically = true)
|
||||
22 @Query(
|
||||
23 """
|
||||
24 update QuotaReservationEntity q
|
||||
25 set q.reservedBytes = q.reservedBytes + :additionalBytes,
|
||||
26 q.version = q.version + 1,
|
||||
27 q.updatedAt = :now
|
||||
28 where q.reservationId = :reservationId
|
||||
29 and q.status = 'RESERVED'
|
||||
30 and q.expiresAt > :now
|
||||
31 """)
|
||||
32 int extend(
|
||||
33 @Param("reservationId") UUID reservationId,
|
||||
34 @Param("additionalBytes") long additionalBytes,
|
||||
35 @Param("now") Instant now);
|
||||
36
|
||||
37 @Modifying(clearAutomatically = true, flushAutomatically = true)
|
||||
38 @Query(
|
||||
39 """
|
||||
40 update QuotaReservationEntity q
|
||||
41 set q.status = 'COMMITTED',
|
||||
42 q.committedBytes = :actualBytes,
|
||||
43 q.reservedBytes = 0,
|
||||
44 q.version = q.version + 1,
|
||||
45 q.updatedAt = :now
|
||||
46 where q.reservationId = :reservationId
|
||||
47 and q.status = 'RESERVED'
|
||||
48 """)
|
||||
49 int commit(
|
||||
50 @Param("reservationId") UUID reservationId,
|
||||
51 @Param("actualBytes") long actualBytes,
|
||||
52 @Param("now") Instant now);
|
||||
53
|
||||
54 @Modifying(clearAutomatically = true, flushAutomatically = true)
|
||||
55 @Query(
|
||||
56 """
|
||||
57 update QuotaReservationEntity q
|
||||
58 set q.status = 'RELEASED',
|
||||
59 q.reservedBytes = 0,
|
||||
60 q.version = q.version + 1,
|
||||
61 q.updatedAt = :now
|
||||
62 where q.reservationId = :reservationId
|
||||
63 and q.status = 'RESERVED'
|
||||
64 """)
|
||||
65 int release(@Param("reservationId") UUID reservationId, @Param("now") Instant now);
|
||||
66
|
||||
67 @Query(
|
||||
68 """
|
||||
69 select coalesce(sum(q.reservedBytes), 0) from QuotaReservationEntity q
|
||||
70 where q.scopeType = :scopeType
|
||||
71 and q.scopeValue = :scopeValue
|
||||
72 and q.status = 'RESERVED'
|
||||
73 and q.expiresAt > :now
|
||||
74 """)
|
||||
75 long sumReservedBytes(
|
||||
76 @Param("scopeType") String scopeType,
|
||||
77 @Param("scopeValue") String scopeValue,
|
||||
78 @Param("now") Instant now);
|
||||
79
|
||||
80 @Query(
|
||||
81 """
|
||||
82 select coalesce(sum(q.committedBytes), 0) from QuotaReservationEntity q
|
||||
83 where q.scopeType = :scopeType
|
||||
84 and q.scopeValue = :scopeValue
|
||||
85 and q.status = 'COMMITTED'
|
||||
86 """)
|
||||
87 long sumCommittedBytes(
|
||||
88 @Param("scopeType") String scopeType, @Param("scopeValue") String scopeValue);
|
||||
89
|
||||
90 /** Live reservations for a scope, oldest first. */
|
||||
91 @Query(
|
||||
92 """
|
||||
93 select q from QuotaReservationEntity q
|
||||
94 where q.scopeType = :scopeType
|
||||
95 and q.scopeValue = :scopeValue
|
||||
96 and q.status = 'RESERVED'
|
||||
97 and q.expiresAt > :now
|
||||
98 order by q.createdAt asc
|
||||
99 """)
|
||||
100 List<QuotaReservationEntity> findActiveReservations(
|
||||
101 @Param("scopeType") String scopeType,
|
||||
102 @Param("scopeValue") String scopeValue,
|
||||
103 @Param("now") Instant now,
|
||||
104 Limit limit);
|
||||
105
|
||||
106 /** Committed rows for a scope that still carry bytes, newest first. */
|
||||
107 @Query(
|
||||
108 """
|
||||
109 select q from QuotaReservationEntity q
|
||||
110 where q.scopeType = :scopeType
|
||||
111 and q.scopeValue = :scopeValue
|
||||
112 and q.status = 'COMMITTED'
|
||||
113 and q.committedBytes > 0
|
||||
114 order by q.updatedAt desc
|
||||
115 """)
|
||||
116 List<QuotaReservationEntity> findCommittedWithBytes(
|
||||
117 @Param("scopeType") String scopeType, @Param("scopeValue") String scopeValue, Limit limit);
|
||||
118
|
||||
119 /**
|
||||
120 * Gives back part of a committed row.
|
||||
121 *
|
||||
122 * <p>The guard is what makes concurrent reclaims safe: a row that another reclaim already drew
|
||||
123 * down below {@code amount} updates zero rows, and the caller moves to the next row instead of
|
||||
124 * driving the ledger negative.
|
||||
125 */
|
||||
126 @Modifying(clearAutomatically = true, flushAutomatically = true)
|
||||
127 @Query(
|
||||
128 """
|
||||
129 update QuotaReservationEntity q
|
||||
130 set q.committedBytes = q.committedBytes - :amount,
|
||||
131 q.version = q.version + 1,
|
||||
132 q.updatedAt = :now
|
||||
133 where q.reservationId = :reservationId
|
||||
134 and q.committedBytes >= :amount
|
||||
135 """)
|
||||
136 int reduceCommitted(
|
||||
137 @Param("reservationId") UUID reservationId,
|
||||
138 @Param("amount") long amount,
|
||||
139 @Param("now") Instant now);
|
||||
140 }
|
||||
|
||||
=== quota reclaim gateway ===
|
||||
1 package dev.caskeleton.adapter.outbound.persistence.fileserver;
|
||||
2
|
||||
3 import dev.caskeleton.adapter.outbound.persistence.fileserver.entity.QuotaReservationEntity;
|
||||
4 import dev.caskeleton.adapter.outbound.persistence.fileserver.repository.FileserverQuotaRepository;
|
||||
5 import dev.caskeleton.application.fileserver.api.metadata.QuotaScope;
|
||||
6 import dev.caskeleton.application.fileserver.cleanup.QuotaReclaimGateway;
|
||||
7 import java.time.Clock;
|
||||
8 import java.time.Instant;
|
||||
9 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
10 import org.springframework.data.domain.Limit;
|
||||
11 import org.springframework.stereotype.Repository;
|
||||
12
|
||||
13 /**
|
||||
14 * Returns reclaimed bytes to a scope's committed total.
|
||||
15 *
|
||||
16 * <p>Committed usage is spread over many rows, so a reclaim is drawn down newest-first across them
|
||||
17 * until the amount is satisfied. Newest-first matters: the most recently committed rows are the
|
||||
18 * ones a delete is most likely to correspond to, and drawing from them keeps historical rows from
|
||||
19 * being hollowed out by unrelated deletions.
|
||||
20 *
|
||||
21 * <p>Each draw-down is conditional on the row still holding at least that many bytes, so two
|
||||
22 * cleanup workers reclaiming at once cannot push the ledger negative — the loser simply moves to
|
||||
23 * the next row.
|
||||
24 *
|
||||
25 * <p>A remainder that no row can absorb is dropped rather than carried. The ledger's floor is zero:
|
||||
26 * a scope cannot owe negative bytes, and a reclaim that outruns the recorded total means the total
|
||||
27 * was already understated, which a negative balance would not fix.
|
||||
28 */
|
||||
29 @Repository
|
||||
30 @ConditionalOnProperty(prefix = "app.fileserver-platform", name = "enabled", havingValue = "true")
|
||||
31 public class JpaQuotaReclaimGateway implements QuotaReclaimGateway {
|
||||
32
|
||||
33 private static final Limit RECLAIM_PAGE = Limit.of(64);
|
||||
34
|
||||
35 private final FileserverQuotaRepository reservations;
|
||||
36 private final Clock clock;
|
||||
37
|
||||
38 public JpaQuotaReclaimGateway(FileserverQuotaRepository reservations, Clock clock) {
|
||||
39 this.reservations = reservations;
|
||||
40 this.clock = clock;
|
||||
41 }
|
||||
42
|
||||
43 @Override
|
||||
44 public void reclaim(QuotaScope scope, long bytes) {
|
||||
45 if (bytes < 0) {
|
||||
46 throw new IllegalArgumentException("bytes must not be negative");
|
||||
47 }
|
||||
48 if (bytes == 0) {
|
||||
49 return;
|
||||
50 }
|
||||
51 Instant now = clock.instant();
|
||||
52 long outstanding = bytes;
|
||||
53 for (QuotaReservationEntity committed :
|
||||
54 reservations.findCommittedWithBytes(scope.type(), scope.value(), RECLAIM_PAGE)) {
|
||||
55 if (outstanding == 0) {
|
||||
56 return;
|
||||
57 }
|
||||
58 long draw = Math.min(outstanding, committed.getCommittedBytes());
|
||||
59 if (reservations.reduceCommitted(committed.getReservationId(), draw, now) == 1) {
|
||||
60 outstanding -= draw;
|
||||
61 }
|
||||
62 }
|
||||
63 }
|
||||
64 }
|
||||
|
||||
=== recovery upsert ===
|
||||
1 package dev.caskeleton.adapter.outbound.persistence.fileserver;
|
||||
2
|
||||
3 import dev.caskeleton.adapter.outbound.persistence.fileserver.entity.RecoveryItemEntity;
|
||||
4 import dev.caskeleton.adapter.outbound.persistence.fileserver.repository.FileserverRecoveryRepository;
|
||||
5 import dev.caskeleton.application.fileserver.api.FileId;
|
||||
6 import dev.caskeleton.application.fileserver.recovery.ReconciliationStatus;
|
||||
7 import dev.caskeleton.application.fileserver.recovery.RecoveryQueue;
|
||||
8 import java.time.Clock;
|
||||
9 import java.time.Instant;
|
||||
10 import java.util.List;
|
||||
11 import java.util.UUID;
|
||||
12 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
13 import org.springframework.data.domain.Limit;
|
||||
14 import org.springframework.stereotype.Repository;
|
||||
15
|
||||
16 /**
|
||||
17 * Durable recovery queue over {@code fs_recovery_item}.
|
||||
18 *
|
||||
19 * <p>An enqueue is an upsert: the same file reported twice updates the open item rather than adding
|
||||
20 * a second one. Reconciliation runs on a schedule and re-raises whatever it still cannot settle, so
|
||||
21 * an append-only queue would grow one row per sweep per unresolved file and bury the distinct
|
||||
22 * problems under repetitions of the same one.
|
||||
23 *
|
||||
24 * <p>Resolution keeps the outcome rather than deleting the row. {@code UNRESOLVED} and {@code
|
||||
25 * QUARANTINE_REQUIRED} are the two answers a human has to act on, and both are worthless if the
|
||||
26 * record of what the system concluded disappears with the item.
|
||||
27 */
|
||||
28 @Repository
|
||||
29 @ConditionalOnProperty(prefix = "app.fileserver-platform", name = "enabled", havingValue = "true")
|
||||
30 public class JpaRecoveryQueue implements RecoveryQueue {
|
||||
31
|
||||
32 private static final String STATUS_PENDING = "PENDING";
|
||||
33
|
||||
34 private final FileserverRecoveryRepository items;
|
||||
35 private final Clock clock;
|
||||
36
|
||||
37 public JpaRecoveryQueue(FileserverRecoveryRepository items, Clock clock) {
|
||||
38 this.items = items;
|
||||
39 this.clock = clock;
|
||||
40 }
|
||||
41
|
||||
42 @Override
|
||||
43 public void enqueue(FileId fileId, String reasonCode) {
|
||||
44 Instant now = clock.instant();
|
||||
45 if (items.refreshPending(fileId.value(), reasonCode, now) > 0) {
|
||||
46 return;
|
||||
47 }
|
||||
48 items.save(
|
||||
49 new RecoveryItemEntity(UUID.randomUUID(), fileId.value(), reasonCode, STATUS_PENDING, now));
|
||||
50 }
|
||||
51
|
||||
52 @Override
|
||||
53 public List<FileId> pending(int limit) {
|
||||
54 if (limit < 1) {
|
||||
55 throw new IllegalArgumentException("limit must be positive");
|
||||
56 }
|
||||
57 return items.findPending(Limit.of(limit)).stream()
|
||||
58 .map(RecoveryItemEntity::getFileId)
|
||||
59 .map(FileId::of)
|
||||
60 .toList();
|
||||
61 }
|
||||
62
|
||||
63 @Override
|
||||
64 public void resolve(FileId fileId, ReconciliationStatus status) {
|
||||
65 items.resolvePending(fileId.value(), status.name(), clock.instant());
|
||||
66 }
|
||||
67 }
|
||||
1 package dev.caskeleton.adapter.outbound.persistence.fileserver.repository;
|
||||
2
|
||||
3 import dev.caskeleton.adapter.outbound.persistence.fileserver.entity.RecoveryItemEntity;
|
||||
4 import java.time.Instant;
|
||||
5 import java.util.List;
|
||||
6 import java.util.UUID;
|
||||
7 import org.springframework.data.domain.Limit;
|
||||
8 import org.springframework.data.jpa.repository.JpaRepository;
|
||||
9 import org.springframework.data.jpa.repository.Modifying;
|
||||
10 import org.springframework.data.jpa.repository.Query;
|
||||
11 import org.springframework.data.repository.query.Param;
|
||||
12
|
||||
13 /** Durable list of files whose physical and logical state could not be reconciled automatically. */
|
||||
14 public interface FileserverRecoveryRepository extends JpaRepository<RecoveryItemEntity, UUID> {
|
||||
15
|
||||
16 @Query(
|
||||
17 """
|
||||
18 select r from RecoveryItemEntity r
|
||||
19 where r.status = 'PENDING'
|
||||
20 order by r.createdAt asc
|
||||
21 """)
|
||||
22 List<RecoveryItemEntity> findPending(Limit limit);
|
||||
23
|
||||
24 /**
|
||||
25 * Re-raises an open item instead of adding a second one.
|
||||
26 *
|
||||
27 * <p>Reconciliation is retried on a schedule, so the same file reaches the queue repeatedly. One
|
||||
28 * open item per file keeps the queue a worklist rather than a failure log; the newest reason wins
|
||||
29 * because it describes the most recent evidence.
|
||||
30 */
|
||||
31 @Modifying(clearAutomatically = true, flushAutomatically = true)
|
||||
32 @Query(
|
||||
33 """
|
||||
34 update RecoveryItemEntity r
|
||||
35 set r.reasonCode = :reasonCode,
|
||||
36 r.attempt = r.attempt + 1,
|
||||
37 r.updatedAt = :now
|
||||
38 where r.fileId = :fileId
|
||||
39 and r.status = 'PENDING'
|
||||
40 """)
|
||||
41 int refreshPending(
|
||||
42 @Param("fileId") UUID fileId,
|
||||
43 @Param("reasonCode") String reasonCode,
|
||||
44 @Param("now") Instant now);
|
||||
45
|
||||
46 @Modifying(clearAutomatically = true, flushAutomatically = true)
|
||||
47 @Query(
|
||||
48 """
|
||||
49 update RecoveryItemEntity r
|
||||
50 set r.status = :status,
|
||||
51 r.updatedAt = :now
|
||||
52 where r.fileId = :fileId
|
||||
53 and r.status = 'PENDING'
|
||||
54 """)
|
||||
55 int resolvePending(
|
||||
56 @Param("fileId") UUID fileId, @Param("status") String status, @Param("now") Instant now);
|
||||
57 }
|
||||
|
||||
=== schema revision and activation ===
|
||||
1 package dev.caskeleton.adapter.outbound.persistence.fileserver;
|
||||
2
|
||||
3 import org.springframework.jdbc.core.JdbcOperations;
|
||||
4
|
||||
5 /**
|
||||
6 * Proves the Fileserver schema stream was applied and promoted before the capability serves a
|
||||
7 * request.
|
||||
8 *
|
||||
9 * <p>The stream is operator-applied, like every other optional capability stream: the application
|
||||
10 * migrates {@code db/migration/postgresql} only, and {@code db/migration/jpa/fileserver} is applied
|
||||
11 * and promoted to {@code ACTIVE} deliberately. Until that happens the {@code fs_*} tables either do
|
||||
12 * not exist or are not sanctioned for use.
|
||||
13 *
|
||||
14 * <p>The check runs once, at startup, rather than per operation. The sibling capabilities verify on
|
||||
15 * every call because they are low-frequency; a file download is not, and a registry round trip on
|
||||
16 * the metadata read path would be paid by every byte served. Startup is also the honest place for
|
||||
17 * it — an unpromoted stream is a deployment state, not a per-request condition.
|
||||
18 *
|
||||
19 * <p>Failing here rather than at the first upload is the point. The alternative is a raw "relation
|
||||
20 * fs_file does not exist" surfacing as a 500 to whoever happened to upload first.
|
||||
21 */
|
||||
22 public final class FileserverSchemaActivation {
|
||||
23
|
||||
24 static final String CAPABILITY_ID = "jpa-fileserver-metadata-v1";
|
||||
25
|
||||
26 private static final String ACTIVE_CAPABILITY_SQL =
|
||||
27 """
|
||||
28 select count(*)
|
||||
29 from capability_schema_registry
|
||||
30 where capability_id = 'jpa-fileserver-metadata-v1'
|
||||
31 and core_epoch = 1
|
||||
32 and feature_revision >= 2
|
||||
33 and lifecycle_state = 'ACTIVE'
|
||||
34 """;
|
||||
35
|
||||
36 private final JdbcOperations jdbc;
|
||||
37
|
||||
38 public FileserverSchemaActivation(JdbcOperations jdbc) {
|
||||
39 this.jdbc = jdbc;
|
||||
40 }
|
||||
41
|
||||
42 /**
|
||||
43 * Fails closed unless the stream is applied and promoted.
|
||||
44 *
|
||||
45 * <p>An unreadable registry is treated as "not promoted" rather than "assume fine": the registry
|
||||
46 * table itself is created by the core stream, so its absence means the prerequisite chain was
|
||||
47 * never established.
|
||||
48 */
|
||||
49 public void requireActive() {
|
||||
50 Integer active;
|
||||
51 try {
|
||||
52 active = jdbc.queryForObject(ACTIVE_CAPABILITY_SQL, Integer.class);
|
||||
53 } catch (RuntimeException unreadable) {
|
||||
54 throw new IllegalStateException(
|
||||
55 CAPABILITY_ID
|
||||
56 + " could not be verified: the capability schema registry is unreadable, so the "
|
||||
57 + "Fileserver schema stream cannot be confirmed as applied",
|
||||
58 unreadable);
|
||||
59 }
|
||||
60 if (active == null || active != 1) {
|
||||
61 throw new IllegalStateException(
|
||||
62 CAPABILITY_ID
|
||||
63 + " is not ACTIVE at core epoch 1 revision 2. Apply db/migration/jpa/fileserver "
|
||||
64 + "against history table flyway_jpa_fileserver_history and promote the capability "
|
||||
65 + "before enabling app.fileserver-platform.enabled");
|
||||
66 }
|
||||
67 }
|
||||
68 }
|
||||
--- src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V1__create_fileserver_metadata.sql
|
||||
13: AND lifecycle_state = 'ACTIVE'
|
||||
187: feature_revision,
|
||||
188: lifecycle_state
|
||||
--- src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V2__fileserver_recovery_and_staging_cleanup.sql
|
||||
16: AND feature_revision >= 1
|
||||
18: RAISE EXCEPTION 'fileserver recovery schema requires fileserver metadata revision 1';
|
||||
61: SET feature_revision = 2
|
||||
63: AND feature_revision < 2;
|
||||
--- src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V3__fileserver_fenced_cleanup_lease.sql
|
||||
19: ADD COLUMN IF NOT EXISTS claim_token uuid,
|
||||
23:COMMENT ON COLUMN fs_cleanup_item.claim_token IS
|
||||
--- src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V4__fileserver_upload_terminal_state.sql
|
||||
24: AND feature_revision >= 2
|
||||
26: RAISE EXCEPTION 'fileserver upload terminal state requires fileserver metadata revision 2';
|
||||
32: ADD COLUMN IF NOT EXISTS lifecycle_state varchar(16) NOT NULL DEFAULT 'ACTIVE';
|
||||
35: DROP CONSTRAINT IF EXISTS ck_fs_upload_lifecycle_state;
|
||||
38: ADD CONSTRAINT ck_fs_upload_lifecycle_state
|
||||
39: CHECK (lifecycle_state IN ('ACTIVE', 'TERMINAL'));
|
||||
41:COMMENT ON COLUMN fs_upload_session.lifecycle_state IS
|
||||
49: WHERE lifecycle_state = 'TERMINAL';
|
||||
|
||||
=== quota contract docs ===
|
||||
### 9. Quota settlement is FIFO within a scope
|
||||
|
||||
Nothing links a reservation row to the upload that took it, and the design deliberately reclaims
|
||||
stragglers by TTL and the `STALE_QUOTA_RESERVATION` cleanup type rather than threading a reservation
|
||||
id through the upload session. `QuotaCommitGateway` therefore settles the oldest live reservation in
|
||||
the file's namespace.
|
||||
|
||||
Which row closes does not change any quota decision: enforcement sums reserved and committed bytes
|
||||
per scope and never reads an individual row. Concurrent uploads of different sizes can leave the
|
||||
reserved total transiently high or low, and it converges as each settles. Durable usage with no live
|
||||
reservation behind it — an upload that outlived its TTL — is still recorded, because a ledger that
|
||||
silently under-counts is worse than one that is briefly imprecise.
|
||||
|
||||
Pinned by `PostgreSqlFileserverReclamationIntegrationTest`.
|
||||
Expected: FAIL because admission control is not implemented.
|
||||
|
||||
- [ ] **Step 3: Implement reservation and bounded permits**
|
||||
|
||||
Use DB conditional updates for quota bytes and JVM semaphores for per-instance transfer concurrency. A create request with unknown length reserves the configured initial chunk; append extends the reservation before writing additional bytes. On cancellation or failure, release the reservation in `finally` or cleanup recovery.
|
||||
|
||||
```java
|
||||
public interface TransferAdmissionController {
|
||||
TransferPermit acquireUpload(QuotaScope scope, long requestedBytes);
|
||||
TransferPermit acquireDirectDownload(QuotaScope scope);
|
||||
}
|
||||
```
|
||||
|
||||
A hard storage high-water condition maps to `StorageFullException`; scope limit maps to `QuotaExceededException`; temporary permit exhaustion maps to `TransferAdmissionRejectedException` with `retryable=true`.
|
||||
|
||||
- [ ] **Step 4: Run quota and concurrency tests**
|
||||
+530
@@ -0,0 +1,530 @@
|
||||
package dev.caskeleton.adapter.outbound.persistence.readiness;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaCleanupQueue;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaContentReferenceLedger;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaFileMetadataStore;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaFileQuotaService;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaQuotaCommitGateway;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaQuotaReclaimGateway;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaRecoveryQueue;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaStagingUploadLocator;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaUploadSessionStore;
|
||||
import dev.caskeleton.application.fileserver.api.ContentKey;
|
||||
import dev.caskeleton.application.fileserver.api.FileId;
|
||||
import dev.caskeleton.application.fileserver.api.FileState;
|
||||
import dev.caskeleton.application.fileserver.api.StorageNamespace;
|
||||
import dev.caskeleton.application.fileserver.api.UploadId;
|
||||
import dev.caskeleton.application.fileserver.api.metadata.FileRecord;
|
||||
import dev.caskeleton.application.fileserver.api.metadata.FileRecordDraft;
|
||||
import dev.caskeleton.application.fileserver.api.metadata.FileRecordMutation;
|
||||
import dev.caskeleton.application.fileserver.api.metadata.QuotaScope;
|
||||
import dev.caskeleton.application.fileserver.api.metadata.UploadSession;
|
||||
import dev.caskeleton.application.fileserver.api.metadata.UploadSessionDraft;
|
||||
import dev.caskeleton.application.fileserver.api.transfer.UploadProtocol;
|
||||
import dev.caskeleton.application.fileserver.cleanup.CleanupItem;
|
||||
import dev.caskeleton.application.fileserver.cleanup.CleanupRequest;
|
||||
import dev.caskeleton.application.fileserver.cleanup.CleanupType;
|
||||
import dev.caskeleton.application.fileserver.recovery.ReconciliationStatus;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
/**
|
||||
* Real-PostgreSQL proof of the reclamation side of the Fileserver.
|
||||
*
|
||||
* <p>The cleanup queue, the recovery queue, and the quota ledger are the components that decide
|
||||
* when physical bytes may be destroyed and how much space a tenant is charged for. All three are
|
||||
* conditional-update designs whose correctness lives in SQL, so an in-memory fake would prove
|
||||
* nothing about them: the partial unique index, the {@code IN ('PENDING','FAILED')} claim guard,
|
||||
* and the {@code committedBytes >= :amount} floor only exist in the database.
|
||||
*/
|
||||
class PostgreSqlFileserverReclamationIntegrationTest {
|
||||
|
||||
private static PostgreSqlReadinessSupport postgres;
|
||||
private static FileserverJpaTestContext context;
|
||||
private static JdbcTemplate jdbc;
|
||||
private static TransactionTemplate transactions;
|
||||
private static JpaFileMetadataStore files;
|
||||
private static JpaUploadSessionStore uploads;
|
||||
private static JpaFileQuotaService quota;
|
||||
private static JpaCleanupQueue cleanupQueue;
|
||||
private static JpaRecoveryQueue recoveryQueue;
|
||||
private static JpaQuotaCommitGateway quotaCommit;
|
||||
private static JpaQuotaReclaimGateway quotaReclaim;
|
||||
private static JpaContentReferenceLedger ledger;
|
||||
private static JpaStagingUploadLocator stagingLocator;
|
||||
|
||||
@BeforeAll
|
||||
static void startAndMigratePostgreSql() {
|
||||
PostgreSqlReadinessSupport.assertDockerAvailable();
|
||||
postgres = PostgreSqlReadinessSupport.start(8, 2_000);
|
||||
migrate("classpath:db/migration/postgresql", "flyway_schema_history");
|
||||
migrateIndependent(
|
||||
"classpath:db/migration/jpa/core", "flyway_jpa_core_history", "explicit-jpa-core-adoption");
|
||||
migrateIndependent(
|
||||
"classpath:db/migration/jpa/fileserver",
|
||||
"flyway_jpa_fileserver_history",
|
||||
"explicit-jpa-fileserver-adoption");
|
||||
|
||||
jdbc = new JdbcTemplate(postgres.dataSource());
|
||||
context = new FileserverJpaTestContext(postgres.dataSource(), Clock.systemUTC());
|
||||
transactions = context.transactions();
|
||||
files = context.bean(JpaFileMetadataStore.class);
|
||||
uploads = context.bean(JpaUploadSessionStore.class);
|
||||
quota = context.bean(JpaFileQuotaService.class);
|
||||
cleanupQueue = context.bean(JpaCleanupQueue.class);
|
||||
recoveryQueue = context.bean(JpaRecoveryQueue.class);
|
||||
quotaCommit = context.bean(JpaQuotaCommitGateway.class);
|
||||
quotaReclaim = context.bean(JpaQuotaReclaimGateway.class);
|
||||
ledger = context.bean(JpaContentReferenceLedger.class);
|
||||
stagingLocator = context.bean(JpaStagingUploadLocator.class);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void stopPostgreSql() {
|
||||
if (context != null) {
|
||||
context.close();
|
||||
}
|
||||
if (postgres != null) {
|
||||
postgres.close();
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void truncate() {
|
||||
jdbc.execute(
|
||||
"TRUNCATE fs_recovery_item, fs_cleanup_item, fs_quota_reservation,"
|
||||
+ " fs_verification_result, fs_upload_session, fs_file CASCADE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aStagingCleanupSurvivesTheRoundTripWithItsUploadIdentity() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadId uploadId = UploadId.of(UUID.randomUUID());
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.enqueue(
|
||||
CleanupRequest.forStaging(CleanupType.CANCELLED_STAGING, record.fileId(), uploadId));
|
||||
return null;
|
||||
});
|
||||
|
||||
List<CleanupItem> claimed = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10));
|
||||
|
||||
assertThat(claimed)
|
||||
.singleElement()
|
||||
.satisfies(
|
||||
item -> {
|
||||
assertThat(item.request().uploadId()).contains(uploadId);
|
||||
assertThat(item.request().contentKey()).isEmpty();
|
||||
assertThat(item.type()).isEqualTo(CleanupType.CANCELLED_STAGING);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void aClaimedItemIsNotHandedToASecondWorker() {
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.enqueue(
|
||||
CleanupRequest.forContent(
|
||||
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
|
||||
return null;
|
||||
});
|
||||
|
||||
List<CleanupItem> first = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10));
|
||||
List<CleanupItem> second = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10));
|
||||
|
||||
assertThat(first).hasSize(1);
|
||||
assertThat(second).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void aFailedItemBecomesDueAgainOnlyAfterItsBackoff() {
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.enqueue(
|
||||
CleanupRequest.forContent(
|
||||
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
|
||||
return null;
|
||||
});
|
||||
CleanupItem item = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10)).get(0);
|
||||
Instant retryAt = Instant.now().plus(Duration.ofMinutes(5));
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.markFailed(item, "STORAGE_UNAVAILABLE", retryAt);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10))).isEmpty();
|
||||
assertThat(inTransaction(() -> cleanupQueue.claimDue(retryAt.plusSeconds(1), 10))).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void anItemThatKeepsFailingIsAbandonedRatherThanRetriedForever() {
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.enqueue(
|
||||
CleanupRequest.forContent(
|
||||
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
|
||||
return null;
|
||||
});
|
||||
|
||||
Instant due = Instant.now();
|
||||
for (int attempt = 0; attempt < JpaCleanupQueue.MAXIMUM_ATTEMPTS; attempt++) {
|
||||
List<CleanupItem> claimed = inTransaction(() -> cleanupQueue.claimDue(due, 10));
|
||||
if (claimed.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.markFailed(claimed.get(0), "STILL_FAILING", due);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
assertThat(inTransaction(() -> cleanupQueue.claimDue(due.plusSeconds(1), 10))).isEmpty();
|
||||
assertThat(statusCounts("fs_cleanup_item", "ABANDONED")).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void aDoneItemIsNeverClaimedAgain() {
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.enqueue(
|
||||
CleanupRequest.forContent(
|
||||
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
|
||||
return null;
|
||||
});
|
||||
CleanupItem item = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10)).get(0);
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.markDone(item);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> cleanupQueue.claimDue(Instant.now().plusSeconds(600), 10)))
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void thesameFileReportedTwiceHoldsOneOpenRecoveryItem() {
|
||||
FileRecord record = insertRecord();
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
recoveryQueue.enqueue(record.fileId(), "READY_DIGEST_MISMATCH");
|
||||
recoveryQueue.enqueue(record.fileId(), "READY_SIZE_MISMATCH");
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> recoveryQueue.pending(10))).containsExactly(record.fileId());
|
||||
assertThat(rowCount("fs_recovery_item")).isEqualTo(1);
|
||||
assertThat(jdbc.queryForObject("SELECT reason_code FROM fs_recovery_item", String.class))
|
||||
.isEqualTo("READY_SIZE_MISMATCH");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aResolvedRecoveryItemLeavesThePendingListButKeepsItsOutcome() {
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(
|
||||
() -> {
|
||||
recoveryQueue.enqueue(record.fileId(), "PUBLISH_EVIDENCE_INCOMPLETE");
|
||||
return null;
|
||||
});
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
recoveryQueue.resolve(record.fileId(), ReconciliationStatus.QUARANTINE_REQUIRED);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> recoveryQueue.pending(10))).isEmpty();
|
||||
assertThat(statusCounts("fs_recovery_item", "QUARANTINE_REQUIRED")).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void aResolvedFileCanBeRaisedAgainLater() {
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(
|
||||
() -> {
|
||||
recoveryQueue.enqueue(record.fileId(), "FIRST");
|
||||
recoveryQueue.resolve(record.fileId(), ReconciliationStatus.UNRESOLVED);
|
||||
recoveryQueue.enqueue(record.fileId(), "SECOND");
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> recoveryQueue.pending(10))).containsExactly(record.fileId());
|
||||
assertThat(rowCount("fs_recovery_item")).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void committingAnUploadMovesReservedBytesToCommitted() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadSession session = insertSession(record.fileId());
|
||||
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
|
||||
inTransaction(() -> quota.reserve(scope, 1_000, Duration.ofHours(1)));
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaCommit.commit(session, 600);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> quota.reservedBytes(scope))).isZero();
|
||||
assertThat(inTransaction(() -> quota.committedBytes(scope))).isEqualTo(600);
|
||||
}
|
||||
|
||||
@Test
|
||||
void committingWithoutALiveReservationStillRecordsTheDurableUsage() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadSession session = insertSession(record.fileId());
|
||||
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaCommit.commit(session, 450);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> quota.committedBytes(scope))).isEqualTo(450);
|
||||
}
|
||||
|
||||
@Test
|
||||
void releasingAnUploadGivesTheReservedCapacityBack() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadSession session = insertSession(record.fileId());
|
||||
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
|
||||
inTransaction(() -> quota.reserve(scope, 2_000, Duration.ofHours(1)));
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaCommit.release(session);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> quota.reservedBytes(scope))).isZero();
|
||||
assertThat(inTransaction(() -> quota.committedBytes(scope))).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void reclaimingDrawsCommittedBytesDownAcrossRows() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadSession session = insertSession(record.fileId());
|
||||
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaCommit.commit(session, 300);
|
||||
quotaCommit.commit(session, 700);
|
||||
return null;
|
||||
});
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaReclaim.reclaim(scope, 800);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> quota.committedBytes(scope))).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reclaimingMoreThanIsRecordedStopsAtZeroRatherThanGoingNegative() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadSession session = insertSession(record.fileId());
|
||||
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaCommit.commit(session, 100);
|
||||
return null;
|
||||
});
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaReclaim.reclaim(scope, 5_000);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> quota.committedBytes(scope))).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void theLedgerReportsAKeyAsReferencedOnlyWhileARecordNamesIt() {
|
||||
ContentKey key = contentKey();
|
||||
assertThat(inTransaction(() -> ledger.isReferenced(key))).isFalse();
|
||||
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(() -> publish(record, key));
|
||||
|
||||
assertThat(inTransaction(() -> ledger.isReferenced(key))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void theStagingLocatorReturnsTheNewestSessionForAFile() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadSession older = insertSession(record.fileId());
|
||||
UploadSession newer = insertSession(record.fileId());
|
||||
|
||||
Optional<UploadId> located = inTransaction(() -> stagingLocator.locate(record.fileId()));
|
||||
|
||||
assertThat(located).isPresent();
|
||||
assertThat(located.get()).isIn(older.uploadId(), newer.uploadId());
|
||||
assertThat(inTransaction(() -> stagingLocator.locate(FileId.of(UUID.randomUUID())))).isEmpty();
|
||||
}
|
||||
|
||||
private FileRecord insertRecord() {
|
||||
return inTransaction(
|
||||
() ->
|
||||
files.insert(
|
||||
new FileRecordDraft(
|
||||
FileId.of(UUID.randomUUID()),
|
||||
StorageNamespace.of("tenant-a"),
|
||||
"report.bin",
|
||||
Optional.of("application/octet-stream"),
|
||||
OptionalLong.of(1_000))));
|
||||
}
|
||||
|
||||
private FileRecord publish(FileRecord record, ContentKey key) {
|
||||
FileRecord uploading =
|
||||
files.transition(
|
||||
record.fileId(),
|
||||
record.version(),
|
||||
FileState.CREATED,
|
||||
FileState.UPLOADING,
|
||||
FileRecordMutation.none());
|
||||
FileRecord uploaded =
|
||||
files.transition(
|
||||
uploading.fileId(),
|
||||
uploading.version(),
|
||||
FileState.UPLOADING,
|
||||
FileState.UPLOADED,
|
||||
FileRecordMutation.uploaded(10, "b".repeat(64)));
|
||||
FileRecord verifying =
|
||||
files.transition(
|
||||
uploaded.fileId(),
|
||||
uploaded.version(),
|
||||
FileState.UPLOADED,
|
||||
FileState.VERIFYING,
|
||||
FileRecordMutation.none());
|
||||
return files.transition(
|
||||
verifying.fileId(),
|
||||
verifying.version(),
|
||||
FileState.VERIFYING,
|
||||
FileState.READY,
|
||||
FileRecordMutation.publishAt(
|
||||
key, 10, "b".repeat(64), "\"" + "b".repeat(64) + "\"", Instant.now()));
|
||||
}
|
||||
|
||||
private UploadSession insertSession(FileId fileId) {
|
||||
return inTransaction(
|
||||
() ->
|
||||
uploads.create(
|
||||
new UploadSessionDraft(
|
||||
UploadId.of(UUID.randomUUID()),
|
||||
fileId,
|
||||
UploadProtocol.RAW,
|
||||
OptionalLong.of(1_000),
|
||||
Instant.now().plus(Duration.ofHours(1)))));
|
||||
}
|
||||
|
||||
private static ContentKey contentKey() {
|
||||
String flat = UUID.randomUUID().toString().replace("-", "");
|
||||
return ContentKey.of(flat.substring(0, 2) + '/' + flat.substring(2, 4) + '/' + flat);
|
||||
}
|
||||
|
||||
private static <T> T inTransaction(Supplier<T> action) {
|
||||
return transactions.execute(status -> action.get());
|
||||
}
|
||||
|
||||
private static int rowCount(String table) {
|
||||
Integer count = jdbc.queryForObject("SELECT count(*) FROM " + table, Integer.class);
|
||||
return count == null ? 0 : count;
|
||||
}
|
||||
|
||||
private static int statusCounts(String table, String status) {
|
||||
Integer count =
|
||||
jdbc.queryForObject(
|
||||
"SELECT count(*) FROM " + table + " WHERE status = ?", Integer.class, status);
|
||||
return count == null ? 0 : count;
|
||||
}
|
||||
|
||||
private static void migrate(String location, String historyTable) {
|
||||
Flyway.configure()
|
||||
.dataSource(postgres.dataSource())
|
||||
.locations(location)
|
||||
.table(historyTable)
|
||||
.baselineOnMigrate(false)
|
||||
.outOfOrder(false)
|
||||
.load()
|
||||
.migrate();
|
||||
}
|
||||
|
||||
private static void migrateIndependent(
|
||||
String location, String historyTable, String baselineDescription) {
|
||||
Flyway flyway =
|
||||
Flyway.configure()
|
||||
.dataSource(postgres.dataSource())
|
||||
.locations(location)
|
||||
.table(historyTable)
|
||||
.baselineVersion("0")
|
||||
.baselineDescription(baselineDescription)
|
||||
.baselineOnMigrate(false)
|
||||
.outOfOrder(false)
|
||||
.load();
|
||||
flyway.baseline();
|
||||
flyway.migrate();
|
||||
}
|
||||
|
||||
@Test
|
||||
void analysisProbeCrashReclaimsBypassMaximumAttempts() {
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.enqueue(
|
||||
CleanupRequest.forContent(
|
||||
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
|
||||
return null;
|
||||
});
|
||||
|
||||
Instant base = Instant.now();
|
||||
for (int crash = 0; crash < JpaCleanupQueue.MAXIMUM_ATTEMPTS + 1; crash++) {
|
||||
Instant claimAt = base.plus(Duration.ofHours(crash));
|
||||
List<CleanupItem> claimed = inTransaction(() -> cleanupQueue.claimDue(claimAt, 10));
|
||||
assertThat(claimed).hasSize(1);
|
||||
int reclaimed =
|
||||
inTransaction(() -> cleanupQueue.reclaimExpiredClaims(claimAt.plus(Duration.ofMinutes(11)), 10));
|
||||
assertThat(reclaimed).isEqualTo(1);
|
||||
}
|
||||
|
||||
Integer attempt =
|
||||
jdbc.queryForObject("SELECT attempt FROM fs_cleanup_item", Integer.class);
|
||||
String status =
|
||||
jdbc.queryForObject("SELECT status FROM fs_cleanup_item", String.class);
|
||||
String error =
|
||||
jdbc.queryForObject("SELECT last_error_code FROM fs_cleanup_item", String.class);
|
||||
System.out.println("fileserverCleanupCrash.maxAttempts=" + JpaCleanupQueue.MAXIMUM_ATTEMPTS);
|
||||
System.out.println("fileserverCleanupCrash.actualAttempt=" + attempt);
|
||||
System.out.println("fileserverCleanupCrash.status=" + status);
|
||||
System.out.println("fileserverCleanupCrash.lastError=" + error);
|
||||
assertThat(attempt).isEqualTo(JpaCleanupQueue.MAXIMUM_ATTEMPTS + 1);
|
||||
assertThat(status).isEqualTo("FAILED");
|
||||
}
|
||||
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
command=./gradlew :adapter:outbound:persistence-jpa:postgresqlFileserverReclamationIntegrationTest --tests dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlFileserverReclamationIntegrationTest.analysisProbeConcurrentRecoveryEnqueueIsNotAnAtomicUpsert --console=plain --no-daemon
|
||||
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
|
||||
Daemon will be stopped at the end of the build
|
||||
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||
> Task :build-logic:compileJava UP-TO-DATE
|
||||
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||
> Task :build-logic:processResources UP-TO-DATE
|
||||
> Task :build-logic:classes UP-TO-DATE
|
||||
> Task :build-logic:jar UP-TO-DATE
|
||||
> Task :shared-contract:compileJava UP-TO-DATE
|
||||
> Task :shared-contract:processResources UP-TO-DATE
|
||||
> Task :shared-contract:classes UP-TO-DATE
|
||||
> Task :shared-contract:jar UP-TO-DATE
|
||||
> Task :application-core:compileJava UP-TO-DATE
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes UP-TO-DATE
|
||||
> Task :application-core:jar UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava
|
||||
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses
|
||||
> Task :adapter:outbound:persistence-jpa:postgresqlFileserverReclamationIntegrationTest
|
||||
|
||||
BUILD SUCCESSFUL in 17s
|
||||
19 actionable tasks: 2 executed, 17 up-to-date
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
=== ASSERTED OBSERVATION (from Gradle test XML) ===
|
||||
fileserverRecovery.concurrentFailures=1
|
||||
fileserverRecovery.first=SUCCESS
|
||||
fileserverRecovery.second=org.springframework.dao.DataIntegrityViolationException
|
||||
fileserverRecovery.rowCount=1
|
||||
source restored after probe: yes
|
||||
+532
@@ -0,0 +1,532 @@
|
||||
package dev.caskeleton.adapter.outbound.persistence.readiness;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaCleanupQueue;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaContentReferenceLedger;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaFileMetadataStore;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaFileQuotaService;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaQuotaCommitGateway;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaQuotaReclaimGateway;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaRecoveryQueue;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaStagingUploadLocator;
|
||||
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaUploadSessionStore;
|
||||
import dev.caskeleton.application.fileserver.api.ContentKey;
|
||||
import dev.caskeleton.application.fileserver.api.FileId;
|
||||
import dev.caskeleton.application.fileserver.api.FileState;
|
||||
import dev.caskeleton.application.fileserver.api.StorageNamespace;
|
||||
import dev.caskeleton.application.fileserver.api.UploadId;
|
||||
import dev.caskeleton.application.fileserver.api.metadata.FileRecord;
|
||||
import dev.caskeleton.application.fileserver.api.metadata.FileRecordDraft;
|
||||
import dev.caskeleton.application.fileserver.api.metadata.FileRecordMutation;
|
||||
import dev.caskeleton.application.fileserver.api.metadata.QuotaScope;
|
||||
import dev.caskeleton.application.fileserver.api.metadata.UploadSession;
|
||||
import dev.caskeleton.application.fileserver.api.metadata.UploadSessionDraft;
|
||||
import dev.caskeleton.application.fileserver.api.transfer.UploadProtocol;
|
||||
import dev.caskeleton.application.fileserver.cleanup.CleanupItem;
|
||||
import dev.caskeleton.application.fileserver.cleanup.CleanupRequest;
|
||||
import dev.caskeleton.application.fileserver.cleanup.CleanupType;
|
||||
import dev.caskeleton.application.fileserver.recovery.ReconciliationStatus;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
/**
|
||||
* Real-PostgreSQL proof of the reclamation side of the Fileserver.
|
||||
*
|
||||
* <p>The cleanup queue, the recovery queue, and the quota ledger are the components that decide
|
||||
* when physical bytes may be destroyed and how much space a tenant is charged for. All three are
|
||||
* conditional-update designs whose correctness lives in SQL, so an in-memory fake would prove
|
||||
* nothing about them: the partial unique index, the {@code IN ('PENDING','FAILED')} claim guard,
|
||||
* and the {@code committedBytes >= :amount} floor only exist in the database.
|
||||
*/
|
||||
class PostgreSqlFileserverReclamationIntegrationTest {
|
||||
|
||||
private static PostgreSqlReadinessSupport postgres;
|
||||
private static FileserverJpaTestContext context;
|
||||
private static JdbcTemplate jdbc;
|
||||
private static TransactionTemplate transactions;
|
||||
private static JpaFileMetadataStore files;
|
||||
private static JpaUploadSessionStore uploads;
|
||||
private static JpaFileQuotaService quota;
|
||||
private static JpaCleanupQueue cleanupQueue;
|
||||
private static JpaRecoveryQueue recoveryQueue;
|
||||
private static JpaQuotaCommitGateway quotaCommit;
|
||||
private static JpaQuotaReclaimGateway quotaReclaim;
|
||||
private static JpaContentReferenceLedger ledger;
|
||||
private static JpaStagingUploadLocator stagingLocator;
|
||||
|
||||
@BeforeAll
|
||||
static void startAndMigratePostgreSql() {
|
||||
PostgreSqlReadinessSupport.assertDockerAvailable();
|
||||
postgres = PostgreSqlReadinessSupport.start(8, 2_000);
|
||||
migrate("classpath:db/migration/postgresql", "flyway_schema_history");
|
||||
migrateIndependent(
|
||||
"classpath:db/migration/jpa/core", "flyway_jpa_core_history", "explicit-jpa-core-adoption");
|
||||
migrateIndependent(
|
||||
"classpath:db/migration/jpa/fileserver",
|
||||
"flyway_jpa_fileserver_history",
|
||||
"explicit-jpa-fileserver-adoption");
|
||||
|
||||
jdbc = new JdbcTemplate(postgres.dataSource());
|
||||
context = new FileserverJpaTestContext(postgres.dataSource(), Clock.systemUTC());
|
||||
transactions = context.transactions();
|
||||
files = context.bean(JpaFileMetadataStore.class);
|
||||
uploads = context.bean(JpaUploadSessionStore.class);
|
||||
quota = context.bean(JpaFileQuotaService.class);
|
||||
cleanupQueue = context.bean(JpaCleanupQueue.class);
|
||||
recoveryQueue = context.bean(JpaRecoveryQueue.class);
|
||||
quotaCommit = context.bean(JpaQuotaCommitGateway.class);
|
||||
quotaReclaim = context.bean(JpaQuotaReclaimGateway.class);
|
||||
ledger = context.bean(JpaContentReferenceLedger.class);
|
||||
stagingLocator = context.bean(JpaStagingUploadLocator.class);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void stopPostgreSql() {
|
||||
if (context != null) {
|
||||
context.close();
|
||||
}
|
||||
if (postgres != null) {
|
||||
postgres.close();
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void truncate() {
|
||||
jdbc.execute(
|
||||
"TRUNCATE fs_recovery_item, fs_cleanup_item, fs_quota_reservation,"
|
||||
+ " fs_verification_result, fs_upload_session, fs_file CASCADE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aStagingCleanupSurvivesTheRoundTripWithItsUploadIdentity() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadId uploadId = UploadId.of(UUID.randomUUID());
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.enqueue(
|
||||
CleanupRequest.forStaging(CleanupType.CANCELLED_STAGING, record.fileId(), uploadId));
|
||||
return null;
|
||||
});
|
||||
|
||||
List<CleanupItem> claimed = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10));
|
||||
|
||||
assertThat(claimed)
|
||||
.singleElement()
|
||||
.satisfies(
|
||||
item -> {
|
||||
assertThat(item.request().uploadId()).contains(uploadId);
|
||||
assertThat(item.request().contentKey()).isEmpty();
|
||||
assertThat(item.type()).isEqualTo(CleanupType.CANCELLED_STAGING);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void aClaimedItemIsNotHandedToASecondWorker() {
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.enqueue(
|
||||
CleanupRequest.forContent(
|
||||
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
|
||||
return null;
|
||||
});
|
||||
|
||||
List<CleanupItem> first = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10));
|
||||
List<CleanupItem> second = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10));
|
||||
|
||||
assertThat(first).hasSize(1);
|
||||
assertThat(second).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void aFailedItemBecomesDueAgainOnlyAfterItsBackoff() {
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.enqueue(
|
||||
CleanupRequest.forContent(
|
||||
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
|
||||
return null;
|
||||
});
|
||||
CleanupItem item = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10)).get(0);
|
||||
Instant retryAt = Instant.now().plus(Duration.ofMinutes(5));
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.markFailed(item, "STORAGE_UNAVAILABLE", retryAt);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10))).isEmpty();
|
||||
assertThat(inTransaction(() -> cleanupQueue.claimDue(retryAt.plusSeconds(1), 10))).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void anItemThatKeepsFailingIsAbandonedRatherThanRetriedForever() {
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.enqueue(
|
||||
CleanupRequest.forContent(
|
||||
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
|
||||
return null;
|
||||
});
|
||||
|
||||
Instant due = Instant.now();
|
||||
for (int attempt = 0; attempt < JpaCleanupQueue.MAXIMUM_ATTEMPTS; attempt++) {
|
||||
List<CleanupItem> claimed = inTransaction(() -> cleanupQueue.claimDue(due, 10));
|
||||
if (claimed.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.markFailed(claimed.get(0), "STILL_FAILING", due);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
assertThat(inTransaction(() -> cleanupQueue.claimDue(due.plusSeconds(1), 10))).isEmpty();
|
||||
assertThat(statusCounts("fs_cleanup_item", "ABANDONED")).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void aDoneItemIsNeverClaimedAgain() {
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.enqueue(
|
||||
CleanupRequest.forContent(
|
||||
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
|
||||
return null;
|
||||
});
|
||||
CleanupItem item = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10)).get(0);
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
cleanupQueue.markDone(item);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> cleanupQueue.claimDue(Instant.now().plusSeconds(600), 10)))
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void thesameFileReportedTwiceHoldsOneOpenRecoveryItem() {
|
||||
FileRecord record = insertRecord();
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
recoveryQueue.enqueue(record.fileId(), "READY_DIGEST_MISMATCH");
|
||||
recoveryQueue.enqueue(record.fileId(), "READY_SIZE_MISMATCH");
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> recoveryQueue.pending(10))).containsExactly(record.fileId());
|
||||
assertThat(rowCount("fs_recovery_item")).isEqualTo(1);
|
||||
assertThat(jdbc.queryForObject("SELECT reason_code FROM fs_recovery_item", String.class))
|
||||
.isEqualTo("READY_SIZE_MISMATCH");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aResolvedRecoveryItemLeavesThePendingListButKeepsItsOutcome() {
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(
|
||||
() -> {
|
||||
recoveryQueue.enqueue(record.fileId(), "PUBLISH_EVIDENCE_INCOMPLETE");
|
||||
return null;
|
||||
});
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
recoveryQueue.resolve(record.fileId(), ReconciliationStatus.QUARANTINE_REQUIRED);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> recoveryQueue.pending(10))).isEmpty();
|
||||
assertThat(statusCounts("fs_recovery_item", "QUARANTINE_REQUIRED")).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void aResolvedFileCanBeRaisedAgainLater() {
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(
|
||||
() -> {
|
||||
recoveryQueue.enqueue(record.fileId(), "FIRST");
|
||||
recoveryQueue.resolve(record.fileId(), ReconciliationStatus.UNRESOLVED);
|
||||
recoveryQueue.enqueue(record.fileId(), "SECOND");
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> recoveryQueue.pending(10))).containsExactly(record.fileId());
|
||||
assertThat(rowCount("fs_recovery_item")).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void committingAnUploadMovesReservedBytesToCommitted() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadSession session = insertSession(record.fileId());
|
||||
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
|
||||
inTransaction(() -> quota.reserve(scope, 1_000, Duration.ofHours(1)));
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaCommit.commit(session, 600);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> quota.reservedBytes(scope))).isZero();
|
||||
assertThat(inTransaction(() -> quota.committedBytes(scope))).isEqualTo(600);
|
||||
}
|
||||
|
||||
@Test
|
||||
void committingWithoutALiveReservationStillRecordsTheDurableUsage() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadSession session = insertSession(record.fileId());
|
||||
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaCommit.commit(session, 450);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> quota.committedBytes(scope))).isEqualTo(450);
|
||||
}
|
||||
|
||||
@Test
|
||||
void releasingAnUploadGivesTheReservedCapacityBack() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadSession session = insertSession(record.fileId());
|
||||
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
|
||||
inTransaction(() -> quota.reserve(scope, 2_000, Duration.ofHours(1)));
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaCommit.release(session);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> quota.reservedBytes(scope))).isZero();
|
||||
assertThat(inTransaction(() -> quota.committedBytes(scope))).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void reclaimingDrawsCommittedBytesDownAcrossRows() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadSession session = insertSession(record.fileId());
|
||||
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaCommit.commit(session, 300);
|
||||
quotaCommit.commit(session, 700);
|
||||
return null;
|
||||
});
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaReclaim.reclaim(scope, 800);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> quota.committedBytes(scope))).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reclaimingMoreThanIsRecordedStopsAtZeroRatherThanGoingNegative() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadSession session = insertSession(record.fileId());
|
||||
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaCommit.commit(session, 100);
|
||||
return null;
|
||||
});
|
||||
|
||||
inTransaction(
|
||||
() -> {
|
||||
quotaReclaim.reclaim(scope, 5_000);
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(inTransaction(() -> quota.committedBytes(scope))).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void theLedgerReportsAKeyAsReferencedOnlyWhileARecordNamesIt() {
|
||||
ContentKey key = contentKey();
|
||||
assertThat(inTransaction(() -> ledger.isReferenced(key))).isFalse();
|
||||
|
||||
FileRecord record = insertRecord();
|
||||
inTransaction(() -> publish(record, key));
|
||||
|
||||
assertThat(inTransaction(() -> ledger.isReferenced(key))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void theStagingLocatorReturnsTheNewestSessionForAFile() {
|
||||
FileRecord record = insertRecord();
|
||||
UploadSession older = insertSession(record.fileId());
|
||||
UploadSession newer = insertSession(record.fileId());
|
||||
|
||||
Optional<UploadId> located = inTransaction(() -> stagingLocator.locate(record.fileId()));
|
||||
|
||||
assertThat(located).isPresent();
|
||||
assertThat(located.get()).isIn(older.uploadId(), newer.uploadId());
|
||||
assertThat(inTransaction(() -> stagingLocator.locate(FileId.of(UUID.randomUUID())))).isEmpty();
|
||||
}
|
||||
|
||||
private FileRecord insertRecord() {
|
||||
return inTransaction(
|
||||
() ->
|
||||
files.insert(
|
||||
new FileRecordDraft(
|
||||
FileId.of(UUID.randomUUID()),
|
||||
StorageNamespace.of("tenant-a"),
|
||||
"report.bin",
|
||||
Optional.of("application/octet-stream"),
|
||||
OptionalLong.of(1_000))));
|
||||
}
|
||||
|
||||
private FileRecord publish(FileRecord record, ContentKey key) {
|
||||
FileRecord uploading =
|
||||
files.transition(
|
||||
record.fileId(),
|
||||
record.version(),
|
||||
FileState.CREATED,
|
||||
FileState.UPLOADING,
|
||||
FileRecordMutation.none());
|
||||
FileRecord uploaded =
|
||||
files.transition(
|
||||
uploading.fileId(),
|
||||
uploading.version(),
|
||||
FileState.UPLOADING,
|
||||
FileState.UPLOADED,
|
||||
FileRecordMutation.uploaded(10, "b".repeat(64)));
|
||||
FileRecord verifying =
|
||||
files.transition(
|
||||
uploaded.fileId(),
|
||||
uploaded.version(),
|
||||
FileState.UPLOADED,
|
||||
FileState.VERIFYING,
|
||||
FileRecordMutation.none());
|
||||
return files.transition(
|
||||
verifying.fileId(),
|
||||
verifying.version(),
|
||||
FileState.VERIFYING,
|
||||
FileState.READY,
|
||||
FileRecordMutation.publishAt(
|
||||
key, 10, "b".repeat(64), "\"" + "b".repeat(64) + "\"", Instant.now()));
|
||||
}
|
||||
|
||||
private UploadSession insertSession(FileId fileId) {
|
||||
return inTransaction(
|
||||
() ->
|
||||
uploads.create(
|
||||
new UploadSessionDraft(
|
||||
UploadId.of(UUID.randomUUID()),
|
||||
fileId,
|
||||
UploadProtocol.RAW,
|
||||
OptionalLong.of(1_000),
|
||||
Instant.now().plus(Duration.ofHours(1)))));
|
||||
}
|
||||
|
||||
private static ContentKey contentKey() {
|
||||
String flat = UUID.randomUUID().toString().replace("-", "");
|
||||
return ContentKey.of(flat.substring(0, 2) + '/' + flat.substring(2, 4) + '/' + flat);
|
||||
}
|
||||
|
||||
private static <T> T inTransaction(Supplier<T> action) {
|
||||
return transactions.execute(status -> action.get());
|
||||
}
|
||||
|
||||
private static int rowCount(String table) {
|
||||
Integer count = jdbc.queryForObject("SELECT count(*) FROM " + table, Integer.class);
|
||||
return count == null ? 0 : count;
|
||||
}
|
||||
|
||||
private static int statusCounts(String table, String status) {
|
||||
Integer count =
|
||||
jdbc.queryForObject(
|
||||
"SELECT count(*) FROM " + table + " WHERE status = ?", Integer.class, status);
|
||||
return count == null ? 0 : count;
|
||||
}
|
||||
|
||||
private static void migrate(String location, String historyTable) {
|
||||
Flyway.configure()
|
||||
.dataSource(postgres.dataSource())
|
||||
.locations(location)
|
||||
.table(historyTable)
|
||||
.baselineOnMigrate(false)
|
||||
.outOfOrder(false)
|
||||
.load()
|
||||
.migrate();
|
||||
}
|
||||
|
||||
private static void migrateIndependent(
|
||||
String location, String historyTable, String baselineDescription) {
|
||||
Flyway flyway =
|
||||
Flyway.configure()
|
||||
.dataSource(postgres.dataSource())
|
||||
.locations(location)
|
||||
.table(historyTable)
|
||||
.baselineVersion("0")
|
||||
.baselineDescription(baselineDescription)
|
||||
.baselineOnMigrate(false)
|
||||
.outOfOrder(false)
|
||||
.load();
|
||||
flyway.baseline();
|
||||
flyway.migrate();
|
||||
}
|
||||
|
||||
@Test
|
||||
void analysisProbeConcurrentRecoveryEnqueueIsNotAnAtomicUpsert() throws Exception {
|
||||
FileRecord record = insertRecord();
|
||||
var pool = java.util.concurrent.Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
var start = new java.util.concurrent.CountDownLatch(1);
|
||||
java.util.concurrent.Callable<Throwable> action =
|
||||
() -> {
|
||||
start.await();
|
||||
try {
|
||||
inTransaction(
|
||||
() -> {
|
||||
recoveryQueue.enqueue(record.fileId(), "CONCURRENT");
|
||||
return null;
|
||||
});
|
||||
return null;
|
||||
} catch (Throwable t) {
|
||||
return t;
|
||||
}
|
||||
};
|
||||
var first = pool.submit(action);
|
||||
var second = pool.submit(action);
|
||||
start.countDown();
|
||||
Throwable a = first.get();
|
||||
Throwable b = second.get();
|
||||
long failures = java.util.stream.Stream.of(a, b).filter(java.util.Objects::nonNull).count();
|
||||
System.out.println("fileserverRecovery.concurrentFailures=" + failures);
|
||||
System.out.println("fileserverRecovery.first=" + (a == null ? "SUCCESS" : a.getClass().getName()));
|
||||
System.out.println("fileserverRecovery.second=" + (b == null ? "SUCCESS" : b.getClass().getName()));
|
||||
System.out.println("fileserverRecovery.rowCount=" + rowCount("fs_recovery_item"));
|
||||
assertThat(failures).isEqualTo(1);
|
||||
assertThat(rowCount("fs_recovery_item")).isEqualTo(1);
|
||||
} finally {
|
||||
pool.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
||||
command=./gradlew :adapter:outbound:persistence-jpa:postgresqlFileserverMigrationIntegrationTest --tests dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlFileserverMigrationIntegrationTest.analysisProbeRevisionTwoIsAcceptedWithoutRevisionThreeAndFourColumns --console=plain --no-daemon
|
||||
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
|
||||
Daemon will be stopped at the end of the build
|
||||
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||
> Task :build-logic:compileJava UP-TO-DATE
|
||||
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||
> Task :build-logic:processResources UP-TO-DATE
|
||||
> Task :build-logic:classes UP-TO-DATE
|
||||
> Task :build-logic:jar UP-TO-DATE
|
||||
> Task :shared-contract:compileJava UP-TO-DATE
|
||||
> Task :shared-contract:processResources UP-TO-DATE
|
||||
> Task :shared-contract:classes UP-TO-DATE
|
||||
> Task :shared-contract:jar UP-TO-DATE
|
||||
> Task :application-core:compileJava UP-TO-DATE
|
||||
> Task :application-core:processResources NO-SOURCE
|
||||
> Task :application-core:classes UP-TO-DATE
|
||||
> Task :application-core:jar UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava
|
||||
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources UP-TO-DATE
|
||||
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses
|
||||
> Task :adapter:outbound:persistence-jpa:postgresqlFileserverMigrationIntegrationTest
|
||||
|
||||
BUILD SUCCESSFUL in 16s
|
||||
19 actionable tasks: 2 executed, 17 up-to-date
|
||||
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||
|
||||
=== ASSERTED OBSERVATION (from Gradle test XML) ===
|
||||
fileserverSchemaV2.featureRevision=2
|
||||
fileserverSchemaV2.activationAccepted=true
|
||||
fileserverSchemaV2.cleanupClaimToken=false
|
||||
fileserverSchemaV2.uploadLifecycleState=false
|
||||
source restored after probe: yes
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
package dev.caskeleton.adapter.outbound.persistence.readiness;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.UUID;
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
/**
|
||||
* Real-PostgreSQL proof that the Fileserver metadata schema exists with the optimistic-locking and
|
||||
* READY-completeness guarantees the design requires.
|
||||
*/
|
||||
class PostgreSqlFileserverMigrationIntegrationTest {
|
||||
|
||||
private static PostgreSqlReadinessSupport postgres;
|
||||
private static JdbcTemplate jdbc;
|
||||
|
||||
@BeforeAll
|
||||
static void startAndMigratePostgreSql() {
|
||||
PostgreSqlReadinessSupport.assertDockerAvailable();
|
||||
postgres = PostgreSqlReadinessSupport.start();
|
||||
migrate("classpath:db/migration/postgresql", "flyway_schema_history");
|
||||
migrateIndependent(
|
||||
"classpath:db/migration/jpa/core", "flyway_jpa_core_history", "explicit-jpa-core-adoption");
|
||||
migrateIndependent(
|
||||
"classpath:db/migration/jpa/fileserver",
|
||||
"flyway_jpa_fileserver_history",
|
||||
"explicit-jpa-fileserver-adoption");
|
||||
jdbc = new JdbcTemplate(postgres.dataSource());
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void stopPostgreSql() {
|
||||
if (postgres != null) {
|
||||
postgres.close();
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void clearRows() {
|
||||
jdbc.update("delete from fs_cleanup_item");
|
||||
jdbc.update("delete from fs_quota_reservation");
|
||||
jdbc.update("delete from fs_verification_result");
|
||||
jdbc.update("delete from fs_upload_session");
|
||||
jdbc.update("delete from fs_file");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsFileserverTablesAndVersionColumns() throws Exception {
|
||||
try (Connection connection = postgres.dataSource().getConnection()) {
|
||||
assertThat(columnExists(connection, "fs_file", "version")).isTrue();
|
||||
assertThat(columnExists(connection, "fs_file", "content_key")).isTrue();
|
||||
assertThat(columnExists(connection, "fs_file", "strong_etag")).isTrue();
|
||||
assertThat(columnExists(connection, "fs_upload_session", "lease_until")).isTrue();
|
||||
assertThat(columnExists(connection, "fs_upload_session", "lease_token")).isTrue();
|
||||
assertThat(columnExists(connection, "fs_upload_session", "committed_offset")).isTrue();
|
||||
assertThat(columnExists(connection, "fs_quota_reservation", "reserved_bytes")).isTrue();
|
||||
assertThat(columnExists(connection, "fs_verification_result", "verdict")).isTrue();
|
||||
assertThat(columnExists(connection, "fs_cleanup_item", "next_attempt_at")).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void registersTheCapabilityStreamAsInstalledButInactive() {
|
||||
String lifecycle =
|
||||
jdbc.queryForObject(
|
||||
"select lifecycle_state from capability_schema_registry where capability_id = ?",
|
||||
String.class,
|
||||
"jpa-fileserver-metadata-v1");
|
||||
|
||||
assertThat(lifecycle).isEqualTo("INSTALLED_INACTIVE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void readyRowsMustCarryCompletePublishedIdentity() {
|
||||
UUID fileId = UUID.randomUUID();
|
||||
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
jdbc.update(
|
||||
"""
|
||||
insert into fs_file(
|
||||
file_id, namespace, state, original_name, version, created_at, updated_at)
|
||||
values (?, ?, 'READY', ?, 0, ?, ?)
|
||||
""",
|
||||
fileId,
|
||||
"tenant-a",
|
||||
"report.bin",
|
||||
now(),
|
||||
now()))
|
||||
.isInstanceOf(DataIntegrityViolationException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void contentKeyIsUniqueAcrossFilesButManyRowsMayHaveNone() {
|
||||
insertCreated(UUID.randomUUID());
|
||||
insertCreated(UUID.randomUUID());
|
||||
|
||||
UUID first = UUID.randomUUID();
|
||||
UUID second = UUID.randomUUID();
|
||||
insertCreated(first);
|
||||
insertCreated(second);
|
||||
jdbc.update(
|
||||
"update fs_file set content_key = ? where file_id = ?", "ab/cd/key0000000001", first);
|
||||
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
jdbc.update(
|
||||
"update fs_file set content_key = ? where file_id = ?",
|
||||
"ab/cd/key0000000001",
|
||||
second))
|
||||
.isInstanceOf(DataIntegrityViolationException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void aLeaseIsAllOrNothing() {
|
||||
UUID fileId = UUID.randomUUID();
|
||||
UUID uploadId = UUID.randomUUID();
|
||||
insertCreated(fileId);
|
||||
insertUpload(uploadId, fileId);
|
||||
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
jdbc.update(
|
||||
"update fs_upload_session set lease_owner = ? where upload_id = ?",
|
||||
"node-a",
|
||||
uploadId))
|
||||
.isInstanceOf(DataIntegrityViolationException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void committedOffsetNeverExceedsTheDeclaredLength() {
|
||||
UUID fileId = UUID.randomUUID();
|
||||
UUID uploadId = UUID.randomUUID();
|
||||
insertCreated(fileId);
|
||||
insertUpload(uploadId, fileId);
|
||||
jdbc.update("update fs_upload_session set expected_length = 10 where upload_id = ?", uploadId);
|
||||
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
jdbc.update(
|
||||
"update fs_upload_session set committed_offset = 11 where upload_id = ?",
|
||||
uploadId))
|
||||
.isInstanceOf(DataIntegrityViolationException.class);
|
||||
}
|
||||
|
||||
private void insertCreated(UUID fileId) {
|
||||
jdbc.update(
|
||||
"""
|
||||
insert into fs_file(
|
||||
file_id, namespace, state, original_name, version, created_at, updated_at)
|
||||
values (?, ?, 'CREATED', ?, 0, ?, ?)
|
||||
""",
|
||||
fileId,
|
||||
"tenant-a",
|
||||
"report.bin",
|
||||
now(),
|
||||
now());
|
||||
}
|
||||
|
||||
private void insertUpload(UUID uploadId, UUID fileId) {
|
||||
jdbc.update(
|
||||
"""
|
||||
insert into fs_upload_session(
|
||||
upload_id, file_id, protocol, committed_offset, expires_at, version,
|
||||
created_at, updated_at)
|
||||
values (?, ?, 'RAW', 0, ?, 0, ?, ?)
|
||||
""",
|
||||
uploadId,
|
||||
fileId,
|
||||
now().plusSeconds(3600),
|
||||
now(),
|
||||
now());
|
||||
}
|
||||
|
||||
private static OffsetDateTime now() {
|
||||
return OffsetDateTime.now(ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
private static boolean columnExists(Connection connection, String table, String column)
|
||||
throws SQLException {
|
||||
try (ResultSet columns = connection.getMetaData().getColumns(null, null, table, column)) {
|
||||
return columns.next();
|
||||
}
|
||||
}
|
||||
|
||||
private static void migrate(String location, String historyTable) {
|
||||
Flyway.configure()
|
||||
.dataSource(postgres.dataSource())
|
||||
.locations(location)
|
||||
.table(historyTable)
|
||||
.baselineOnMigrate(false)
|
||||
.outOfOrder(false)
|
||||
.load()
|
||||
.migrate();
|
||||
}
|
||||
|
||||
private static void migrateIndependent(
|
||||
String location, String historyTable, String baselineDescription) {
|
||||
Flyway flyway =
|
||||
Flyway.configure()
|
||||
.dataSource(postgres.dataSource())
|
||||
.locations(location)
|
||||
.table(historyTable)
|
||||
.baselineVersion("0")
|
||||
.baselineDescription(baselineDescription)
|
||||
.baselineOnMigrate(false)
|
||||
.outOfOrder(false)
|
||||
.load();
|
||||
flyway.baseline();
|
||||
flyway.migrate();
|
||||
}
|
||||
|
||||
@Test
|
||||
void analysisProbeRevisionTwoIsAcceptedWithoutRevisionThreeAndFourColumns() throws Exception {
|
||||
try (PostgreSqlReadinessSupport v2 = PostgreSqlReadinessSupport.start()) {
|
||||
Flyway.configure()
|
||||
.dataSource(v2.dataSource())
|
||||
.locations("classpath:db/migration/postgresql")
|
||||
.table("flyway_schema_history")
|
||||
.baselineOnMigrate(false)
|
||||
.outOfOrder(false)
|
||||
.load()
|
||||
.migrate();
|
||||
|
||||
Flyway core =
|
||||
Flyway.configure()
|
||||
.dataSource(v2.dataSource())
|
||||
.locations("classpath:db/migration/jpa/core")
|
||||
.table("flyway_jpa_core_history")
|
||||
.baselineVersion("0")
|
||||
.baselineDescription("analysis-core")
|
||||
.baselineOnMigrate(false)
|
||||
.outOfOrder(false)
|
||||
.load();
|
||||
core.baseline();
|
||||
core.migrate();
|
||||
|
||||
Flyway fileserver =
|
||||
Flyway.configure()
|
||||
.dataSource(v2.dataSource())
|
||||
.locations("classpath:db/migration/jpa/fileserver")
|
||||
.table("flyway_jpa_fileserver_history")
|
||||
.baselineVersion("0")
|
||||
.baselineDescription("analysis-fileserver-v2")
|
||||
.baselineOnMigrate(false)
|
||||
.outOfOrder(false)
|
||||
.target("2")
|
||||
.load();
|
||||
fileserver.baseline();
|
||||
fileserver.migrate();
|
||||
|
||||
JdbcTemplate v2Jdbc = new JdbcTemplate(v2.dataSource());
|
||||
v2Jdbc.update(
|
||||
"update capability_schema_registry set lifecycle_state = 'ACTIVE' where capability_id = 'jpa-fileserver-metadata-v1'");
|
||||
new dev.caskeleton.adapter.outbound.persistence.fileserver.FileserverSchemaActivation(v2Jdbc)
|
||||
.requireActive();
|
||||
|
||||
int revision =
|
||||
v2Jdbc.queryForObject(
|
||||
"select feature_revision from capability_schema_registry where capability_id = 'jpa-fileserver-metadata-v1'",
|
||||
Integer.class);
|
||||
boolean hasCleanupClaimToken;
|
||||
boolean hasUploadLifecycle;
|
||||
try (Connection connection = v2.dataSource().getConnection()) {
|
||||
hasCleanupClaimToken = columnExists(connection, "fs_cleanup_item", "claim_token");
|
||||
hasUploadLifecycle = columnExists(connection, "fs_upload_session", "lifecycle_state");
|
||||
}
|
||||
System.out.println("fileserverSchemaV2.featureRevision=" + revision);
|
||||
System.out.println("fileserverSchemaV2.activationAccepted=true");
|
||||
System.out.println("fileserverSchemaV2.cleanupClaimToken=" + hasCleanupClaimToken);
|
||||
System.out.println("fileserverSchemaV2.uploadLifecycleState=" + hasUploadLifecycle);
|
||||
assertThat(revision).isEqualTo(2);
|
||||
assertThat(hasCleanupClaimToken).isFalse();
|
||||
assertThat(hasUploadLifecycle).isFalse();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user