feat: add messaging R2 polling producer

This commit is contained in:
donghyeon-ka
2026-07-31 23:48:43 +09:00
parent b3add0162d
commit e5af291269
93 changed files with 19617 additions and 139 deletions
+99
View File
@@ -45,6 +45,22 @@ application-core 포트(`MessagePublisher` / `OutboxMessagePublishPort`) 뒤에
그대로 전파하고 checked 예외는 cause를 보존해 감싼다. 성공 DEBUG나 실패 WARN을 남기지 않는다.
반환 타입을 void 로 둬 broker SDK 타입이 어댑터 밖으로 새지 않는다(B7).
## Legacy R0 characterization
현재 `KafkaSender.send(...)``MessageBroker.send(...)`의 정상 void 반환은 호출이 예외 없이
끝났다는 뜻일 뿐 broker acknowledgement 증거가 아니다. relay는 이 정상 반환 뒤 legacy
`PUBLISHED`를 기록한다. sender 예외는 fail-closed outbox 경로에서 전파되어 relay의
`FAILED/DEAD` 전이를 유발한다.
broker 설정이 blank면 두 포트는 각각 `DisabledMessagePublisher`
`DisabledOutboxMessagePublisher`에 바인딩된다. broker를 선택했지만 project-supplied sender가
없거나 선택 ID와 활성 `MessageBroker.brokerId()`가 다르면 startup이 실패한다.
publisher 정상 반환 뒤 DB mark가 실패하면 row는 `IN_FLIGHT`에 남아 timeout reclaim 후 같은
event가 다시 publish될 수 있다. 현재 consumer/inbox가 없으므로 이 duplicate 가능 구간을
중복 안전 전달로 표현하지 않는다. 또한 legacy FIFO gate는 `occurred_at`의 strict less-than 비교만
사용하므로 동일 aggregate의 timestamp 동률 row는 서로를 gate하지 않는다.
## OutboxRelayFailureReport 구조화 ERROR
`MessagingConfig`는 broker 활성 여부와 무관하게 정확히 하나의
@@ -58,3 +74,86 @@ ERROR 하나로 렌더링한다.
payload/idempotency key/envelope/exception-derived field는 받거나 렌더링하지 않고 cause만 throwable로
붙인다. logging 내부 `RuntimeException`은 adapter와 use case 양쪽에서 방어하므로 persisted
FAILED/DEAD outcome을 바꾸지 않는다.
## Closed local binding compiler R1
`contract`, `destination`, `config` package는 명시적으로 주입된
`IntegrationEventContractContribution` 목록만 사용해 contract catalog와 deployment binding을
로컬에서 컴파일한다. payload record의 exact `Class` token, 선언 순서, schema resource/hash를
검증하며 scan, `Class.forName`, assignable discovery, raw JSON/tree discovery를 하지 않는다.
catalog/settings/schema digest는 정렬된 semantic identity와 length-prefixed UTF-8/raw SHA-256
bytes로 계산한다.
physical topic과 bootstrap server는 `DestinationBindingSettings`
`CompiledPublicationBinding`에만 존재한다. application contract에는 broker, topic, credential
설정이 유출되지 않는다. logical destination과 physical topic은 각각 deployment 안에서 unique다.
compiled publication binding은 package-local compiler만 만들 수 있고 active descriptor는 public
static compile 경로로만 생성한다. compiled integration-event contract도 catalog compiler만
package-local constructor로 만들 수 있다. partition key v1은 canonical non-null tenant scope,
logical destination, aggregate type/id를 strict UTF-8 REPORT + domain-separated
length-prefixed SHA-256으로 계산하고 lowercase hex US-ASCII bytes를 반환한다. malformed
surrogate는 replacement 문자로 바꾸지 않고 거부한다.
adapter 내부 compiler/card/binding 타입은 direct 또는 generic argument 형태의 public instance
response surface로 노출하지 않는다.
cross-package adapter composition이 정확한 compiled identity를 확인해야 할 때만 public static
bridge를 사용하며, instance accessor는 package-private이고 외부에 필요한 상태는 boolean/String/
core value로만 제공한다. 이는 broker SDK뿐 아니라 adapter 자체의 조립 타입도 B7 반환 경계를
통해 application consumer로 새지 않게 한다.
`MessagingCapabilityCardRegistry`는 first R2 tuple의 11개 ID와 각 role을 닫힌 Java vocabulary로
제공할 뿐이다. readiness registry의 `not-implemented` 상태를 승격하거나 R2/`ACTIVE_READY`
주장하지 않는다. `DISABLED + empty catalog/bindings`는 client, thread, scheduler, network,
filesystem resource가 정확히 0인 descriptor다. `ACTIVE`는 empty catalog, missing/duplicate/extra
binding, unknown/mismatched card와 ordering/schema/security 완화를 resource 생성 전에 거부한다.
## Deterministic local JSON Schema encoder candidate
`envelope` package는 exact final `IntegrationPayload` record만 받아 frozen v1 field order로 UTF-8
JSON을 쓴다. `Map`, raw JSON, `JsonNode`, polymorphic typing, custom serializer, assignable search는
입력 경로에 없다. contract compiler가 선언된 generic type graph를 scalar/enum,
`Optional<T>`/`List<T>`/exact final nested record로만 재귀 고정하며 `Object`, interface, raw/wildcard,
generic record와 runtime record discovery를 거부한다. writer는 이 compiled graph를 따라서 각
record accessor를 정확히 한 번 읽은 immutable snapshot을 만들고 payload를 정확히 한 번 encode한다.
schema validation에 건넨 바로 그 payload bytes를 raw JSON API 없이 envelope suffix에 삽입한다.
depth, string character/UTF-8 byte, array, object, number, payload/envelope byte limit는 writer와
parser 양쪽에서 적용한다. output byte limit는 generator가 쓰는 bounded stream에서 allocation 전에
강제하고, list는 size를 먼저 검사한 뒤 bounded iterator로 한 번만 snapshot한다. mutation/concurrent
access와 극단적 `BigDecimal` scale도 큰 배열/String 생성 전에 fail-closed 한다. duplicate key,
malformed UTF-8, trailing data, unpaired surrogate와 non-finite number는 replacement나 coercion
없이 거부한다.
`LocalJsonSchemaRegistry`는 호출자가 명시적으로 제공한 exact bytes와 SHA-256만 startup에서
Draft 2020-12 meta-schema로 검사하고 precompile한다. root `$id`와 absolute `$ref`는 lowercase
exact `urn` scheme만 허용하고 `$ref`는 동일 문서 fragment 또는 제공된 exact URN map으로 닫힌다.
HTTP/HTTPS/file/classpath/resource/jar 및 unknown absolute scheme fetch와 YAML loader는 제공하지
않는다. public validation boundary도 UTF-8 bytes만 받는다. format assertion은 활성이다. 이 Task 6
closed subset은 scope 추적의 모호성을 제거하기 위해 root 외 nested `$id`를 value type과 무관하게
거부하고, `$dynamicRef`, `$dynamicAnchor`, deprecated `$recursiveRef`/`$recursiveAnchor`, `$anchor`
키워드를 어느 위치에서도 지원하지 않는다.
Draft 2020-12 authority는 `draft/2020-12/schema` 1개와 `meta/*` 8개의 exact checked-in bytes 및
digest로 pin한다. registry startup은 9개 digest, 각 `$id`, NetworkNT runtime schema tree를
대조하고 하나라도 다르면 fail-closed 한다. Spring Boot executable/fat/nested JAR 배치를 깨뜨리는
`CodeSource` regular-file/JAR 가정은 하지 않는다. NetworkNT 3.0.2 artifact provenance는 strict
Gradle dependency lock과 `verifyJsonSchemaRuntimeGraph`가 담당한다. 이 검증은 business schema
registry나 runtime remote resolution 경로를 넓히지 않는다.
공통 build evidence manifest는 수동 구조 검사만으로 PASS하지 않는다. test/build 전용
`MessagingEvidenceManifestSchemaValidator`가 exact common schema와 생성된 manifest bytes를 같은
pinned NetworkNT/`LocalJsonSchemaRegistry`로 Draft 2020-12 검증하며, common schema 자체도
meta-schema 검증을 통과해야 한다. `verifyMessagingContracts`는 JSON-only qualification과 그
manifest schema validation을 명시적으로 선행해 CLI task 나열 순서와 무관하게 combined manifest가
`build/messaging-evidence/contracts-schema/manifest.json`의 최종 소유자가 된다.
NetworkNT 3.0.2의 adopted regex 구현은 JDK regex 실행시간을 강제 중단시키는 별도 engine/timeout을
제공하지 않는다. 따라서 현재 후보는 regex 입력 길이와 전체 document 구조를 먼저 제한하고,
pathological pattern의 작은 repository corpus를 회귀 테스트한다. 이는 JSON Schema Test
Suite/Bowtie 전체 호환이나 hostile regex 시간 상한 증명이 아니며 evidence의
`regex-engine-timeout`, `consumer-compatibility-full-suite` unsupported claim으로 남는다. 테스트
runtime은 remote corpus를 내려받지 않는다.
이 Task의 acceptance는 **deterministic local wire contract candidate**다. encoder/catalog는 기존
`MessagingConfig`, runtime append, `KafkaSender` 또는 legacy broker selection에 연결하지 않았다.
Kafka ACK, durable outbox R2, external topic attestation은 아직 구현되지 않았고 R0 runtime authority와
동작은 그대로다.