# 타입 안전 gRPC 실행 플랫폼 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Spring Boot 4.1을 실행 엔진으로 사용하면서 Protobuf 계약 거버넌스, Named Channel·Server Profile, 세 축 실행 증거, deadline·retry·idempotency, 안정 Status·Rich Error, DNS·Kubernetes discovery, bounded Server Streaming과 실제 Netty 검증을 제공하는 Stable 타입 안전 gRPC 실행 플랫폼을 구축한다. **Architecture:** `.proto`와 generated Java contract는 `grpc-proto-contract`·`grpc-codegen`이 소유하고, 일반 애플리케이션은 Generated Stub과 Typed Service Adapter를 통해서만 RPC를 사용한다. `grpc-core-api`와 `grpc-policy`가 method policy·execution evidence·deadline·retry·metadata·streaming 계약을 정의하며, 상태 변경 RPC의 강한 완료 증거는 JPA Operation Ledger와 Application transaction의 결합으로 제공한다. Stable 범위는 Unary와 Server Streaming이며 Client/Bidi Streaming, xDS, gRPC-Web, Hedging과 언어별 adapter는 Advanced 계획으로 격리한다. **Tech Stack:** Java 21, Gradle Kotlin DSL, Spring Boot 4.1 BOM, Boot-managed Spring gRPC, gRPC Java, Protobuf Java, Protobuf Common Protos, Netty and Netty Shaded transports, Buf CLI/Gradle plugin, Protovalidate, Spring Security, Spring Data JPA, Flyway, Micrometer Observation, OpenTelemetry bridge, JUnit 5, AssertJ, ArchUnit, Testcontainers, Toxiproxy. ## Global Constraints - Java runtime은 `21`이다. - Dependency version의 Source of Truth는 Spring Boot `4.1` BOM이다. - Spring gRPC, gRPC Java, Protobuf Java, Common Protos 버전을 개별 모듈에서 임의로 override하지 않는다. - Stable 신규 Schema는 `proto3 + explicit optional`을 사용한다. - Edition 2024는 Advanced opt-in이고 Edition 2026은 Experimental Watch다. - Boot-managed Protobuf plugin이 Java code generation의 단일 owner다. - Buf는 format, lint, breaking, descriptor/image와 schema artifact를 소유한다. - Stable public API breaking gate는 Buf `FILE` category다. - 모든 Stable Unary method는 positive deadline profile을 가져야 한다. - gRPC Status와 업무 Commit evidence를 동일한 상태로 취급하지 않는다. - 실행 증거는 Transport, Business, Stream 세 축으로 보존한다. - Response Headers 관측을 Business Commit으로 승격하지 않는다. - `DEADLINE_EXCEEDED` mutation은 완료 실패가 아니라 Completion Unknown일 수 있다. - `UNAVAILABLE`만으로 상태 변경 RPC를 재호출하지 않는다. - explicit retry owner는 Application, gRPC Platform, Service Mesh 중 하나다. - non-idempotent method에 explicit retry와 hedging을 허용하지 않는다. - 상태 변경 RPC에 exactly-once transport 보장을 선언하지 않는다. - `IDEMPOTENCY_KEY_REQUIRED` method는 server-side Operation Ledger와 request fingerprint를 사용한다. - Operation Ledger와 business mutation은 가능한 경우 동일 DB transaction에 commit한다. - Stable RPC 유형은 Unary와 Server Streaming이다. - Client Streaming과 Bidirectional Streaming은 Advanced 계획에서 구현한다. - Server Streaming writer는 bounded queue와 single serialized writer를 사용한다. - StreamObserver에 여러 thread가 직접 `onNext()`하지 않는다. - partial stream delivery 이후 whole-call transparent retry를 수행하지 않는다. - GraphQL/WebSocket/Messaging/Fileserver/Object Storage의 책임을 gRPC에서 중복 구현하지 않는다. - 대형 binary는 Fileserver/Object Storage reference로 전달한다. - 일반 Application source에서 raw gRPC builder·call API import를 금지한다. - Netty가 Stable certification transport다. - In-process transport는 fast contract test용이며 network/TLS evidence가 아니다. - dev·stage·prod는 TLS를 요구하고 trust-all·hostname verification off를 금지한다. - production Reflection은 기본 비활성이다. - Stable resolver는 Static·DNS, Stable LB는 pick_first·round_robin이다. - xDS와 custom resolver/LB는 Advanced다. - Metric tag에 raw metadata, payload, actor/user/tenant/object/stream/idempotency ID를 넣지 않는다. - Stable module root는 `modules/grpc`이다. - Root package는 `io.backend.skeleton.grpc`이다. - 모든 task는 red-green TDD와 독립 commit으로 끝난다. - 실제 저장소 구조가 예상 경로와 다르면 파일 경로만 매핑하고 공개 계약·불변 조건·테스트 의미는 변경하지 않는다. --- ## Execution Baseline ```text Stable Task 1–53 → Stable Release Gate → Advanced Task 1–18 ``` ## Stable Module Map ```text modules/grpc/ ├── grpc-core-api ├── grpc-proto-contract ├── grpc-codegen ├── grpc-client ├── grpc-server ├── grpc-policy ├── grpc-discovery ├── grpc-admin ├── grpc-observability ├── grpc-operation-ledger-jpa ├── grpc-spring-boot-starter ├── grpc-testkit-core ├── grpc-testkit-inprocess ├── grpc-testkit-netty └── grpc-testkit-fault ``` ## File Ownership Rules ```text grpc-core-api → identifiers, method policy, evidence, failure, request context, deadline primitive grpc-proto-contract → public/common .proto and schema source grpc-codegen → Gradle convention, Buf governance, descriptor and consumer fixture grpc-client → named channel, runtime generation, typed stub, client metadata grpc-server → service adapter, interceptor order, Netty profiles, architecture boundary grpc-policy → validation, deadline, cancellation, security, status, retry, idempotency, streaming, size and compression grpc-discovery → stable resolver, load-balancing and Kubernetes profile grpc-admin → health, reflection, drain and safe runtime snapshot grpc-observability → bounded metric/trace/logging convention grpc-operation-ledger-jpa → durable mutation idempotency and completion evidence grpc-spring-boot-starter → auto-configuration, properties and startup validation grpc-testkit-* → in-process, real Netty, fault, reliability and performance certification ``` ## Delivery Phases | Phase | Tasks | Independently testable result | |---|---:|---| | Foundation | 1–7 | Stable modules, identifiers, policy, evidence, failure, deadline, context | | Contract Governance | 8–11 | Proto rules, Buf, Boot codegen, descriptor/consumer gate | | Server Boundary | 12–23 | Validation, architecture, adapter, interceptor, server, security, health, reflection, drain | | Client Runtime | 24–30 | Named Channel, generation, typed stub, metadata, deadline, cancellation, retry owner | | Reliability | 31–33 | Retry eligibility, operation ledger, idempotency | | Recovery·Discovery | 34–36 | Completion query, DNS/LB, Kubernetes profiles | | Server Streaming | 37–43 | Envelope, writer, flow control, resume, lifetime, wait-for-ready, payload | | Operations·Verification | 44–53 | Observability, admin, in-process, Netty, fault, reliability, performance, starter, release gate | --- ### Task 1: Gradle 멀티모듈과 Stable 테스트 집계 **Files:** - Modify: `settings.gradle.kts` - Modify: `build.gradle.kts` - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/GrpcStableModuleCatalog.java` - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/GrpcStableBuildInvariant.java` - Create: `modules/grpc/build.gradle.kts` - Create: `modules/grpc/grpc-core-api/build.gradle.kts` - Create: `modules/grpc/grpc-proto-contract/build.gradle.kts` - Create: `modules/grpc/grpc-codegen/build.gradle.kts` - Create: `modules/grpc/grpc-client/build.gradle.kts` - Create: `modules/grpc/grpc-server/build.gradle.kts` - Create: `modules/grpc/grpc-policy/build.gradle.kts` - Create: `modules/grpc/grpc-discovery/build.gradle.kts` - Create: `modules/grpc/grpc-admin/build.gradle.kts` - Create: `modules/grpc/grpc-observability/build.gradle.kts` - Create: `modules/grpc/grpc-operation-ledger-jpa/build.gradle.kts` - Create: `modules/grpc/grpc-spring-boot-starter/build.gradle.kts` - Create: `modules/grpc/grpc-testkit-core/build.gradle.kts` - Create: `modules/grpc/grpc-testkit-inprocess/build.gradle.kts` - Create: `modules/grpc/grpc-testkit-netty/build.gradle.kts` - Create: `modules/grpc/grpc-testkit-fault/build.gradle.kts` - Test: `modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/core/GrpcStableModuleCatalogTest.java` **Interfaces:** - Consumes: Root Gradle settings, version catalog와 Spring Boot 4.1 BOM. - Produces: `modules/grpc`의 Stable module 목록, 금지 dependency 규칙, `grpcStableTest` aggregate task. **Implementation requirements:** - Stable module은 `grpc-core-api`, `grpc-proto-contract`, `grpc-codegen`, `grpc-client`, `grpc-server`, `grpc-policy`, `grpc-discovery`, `grpc-admin`, `grpc-observability`, `grpc-operation-ledger-jpa`, `grpc-spring-boot-starter`, `grpc-testkit-core`, `grpc-testkit-inprocess`, `grpc-testkit-netty`, `grpc-testkit-fault`로 고정한다. - 모든 runtime dependency version은 Spring Boot 4.1 BOM을 사용한다. - `grpc-proto-contract`와 `grpc-core-api`는 Spring Boot·Netty runtime에 의존하지 않는다. - Stable starter가 `modules/grpc-advanced`를 참조하면 build를 실패시킨다. - `grpcStableTest`, `grpcContractTest`, `grpcNettyTest`, `grpcFaultTest`, `grpcPerformanceTest` task를 등록한다. - [ ] **Step 1: Write the failing test** ```java class GrpcStableModuleCatalogTest { @org.junit.jupiter.api.Test void stableModuleSetIsExactAndAdvancedIsExcluded() { var catalog = GrpcStableModuleCatalog.standard(); org.assertj.core.api.Assertions.assertThat(catalog.modules()) .contains("grpc-core-api", "grpc-client", "grpc-server") .doesNotContain("grpc-xds", "grpc-web"); org.assertj.core.api.Assertions.assertThat( GrpcStableBuildInvariant.advancedDependencyAllowed()) .isFalse(); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.core.GrpcStableModuleCatalogTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcStableModuleCatalog( java.util.Set modules) { public static GrpcStableModuleCatalog standard() { return new GrpcStableModuleCatalog(java.util.Set.of( "grpc-core-api", "grpc-proto-contract", "grpc-codegen", "grpc-client", "grpc-server", "grpc-policy", "grpc-discovery", "grpc-admin", "grpc-observability", "grpc-operation-ledger-jpa", "grpc-spring-boot-starter", "grpc-testkit-core", "grpc-testkit-inprocess", "grpc-testkit-netty", "grpc-testkit-fault")); } } public final class GrpcStableBuildInvariant { public static boolean advancedDependencyAllowed() { return false; } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.core.GrpcStableModuleCatalogTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/GrpcStableModuleCatalog.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/GrpcStableBuildInvariant.java' 'modules/grpc/build.gradle.kts' 'modules/grpc/grpc-core-api/build.gradle.kts' 'modules/grpc/grpc-proto-contract/build.gradle.kts' 'modules/grpc/grpc-codegen/build.gradle.kts' 'modules/grpc/grpc-client/build.gradle.kts' 'modules/grpc/grpc-server/build.gradle.kts' 'modules/grpc/grpc-policy/build.gradle.kts' 'modules/grpc/grpc-discovery/build.gradle.kts' 'modules/grpc/grpc-admin/build.gradle.kts' 'modules/grpc/grpc-observability/build.gradle.kts' 'modules/grpc/grpc-operation-ledger-jpa/build.gradle.kts' 'modules/grpc/grpc-spring-boot-starter/build.gradle.kts' 'modules/grpc/grpc-testkit-core/build.gradle.kts' 'modules/grpc/grpc-testkit-inprocess/build.gradle.kts' 'modules/grpc/grpc-testkit-netty/build.gradle.kts' 'modules/grpc/grpc-testkit-fault/build.gradle.kts' 'modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/core/GrpcStableModuleCatalogTest.java' 'settings.gradle.kts' 'build.gradle.kts' git commit -m "build: establish grpc stable module graph" ``` ### Task 2: Core 식별자와 RPC 유형 **Files:** - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/GrpcMethodName.java` - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/GrpcServiceName.java` - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/GrpcChannelProfileName.java` - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/RpcType.java` - Test: `modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/core/GrpcCoreIdentifiersTest.java` **Interfaces:** - Consumes: Task 1의 Stable module graph. - Produces: 공백·불완전 이름을 거부하는 bounded identifier와 Unary·Streaming RPC 유형. **Implementation requirements:** - `GrpcMethodName`은 canonical full method name인 `package.Service/Method` 형식을 요구한다. - `GrpcServiceName`과 `GrpcChannelProfileName`은 빈 값과 제어 문자를 거부한다. - `RpcType`은 `UNARY`, `SERVER_STREAMING`, `CLIENT_STREAMING`, `BIDI_STREAMING`만 가진다. - 식별자 원문은 metric label에 자동 사용하지 않는다. - generated descriptor와 catalog가 동일 canonical name을 사용한다. - [ ] **Step 1: Write the failing test** ```java class GrpcCoreIdentifiersTest { @org.junit.jupiter.api.Test void methodNameRequiresServiceAndMethod() { org.assertj.core.api.Assertions.assertThatThrownBy( () -> new GrpcMethodName("DocumentService")) .isInstanceOf(IllegalArgumentException.class); org.assertj.core.api.Assertions.assertThat( new GrpcMethodName( "hyeonworks.document.v1.DocumentService/GetDocument") .value()) .endsWith("/GetDocument"); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.core.GrpcCoreIdentifiersTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcMethodName(String value) { public GrpcMethodName { if (value == null || !value.matches("[A-Za-z0-9_.]+/[A-Za-z0-9_]+")) { throw new IllegalArgumentException( "full gRPC method name is required"); } } } public record GrpcServiceName(String value) { public GrpcServiceName { if (value == null || value.isBlank()) { throw new IllegalArgumentException("service name"); } } } public enum RpcType { UNARY, SERVER_STREAMING, CLIENT_STREAMING, BIDI_STREAMING } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.core.GrpcCoreIdentifiersTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/GrpcMethodName.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/GrpcServiceName.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/GrpcChannelProfileName.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/RpcType.java' 'modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/core/GrpcCoreIdentifiersTest.java' git commit -m "feat: add grpc core identifiers" ``` ### Task 3: Method Policy와 Idempotency Profile **Files:** - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/policy/RpcIdempotencyProfile.java` - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/policy/GrpcMethodPolicy.java` - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/policy/GrpcMethodPolicyCatalog.java` - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/policy/WaitForReadyPolicy.java` - Test: `modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/policy/GrpcMethodPolicyCatalogTest.java` **Interfaces:** - Consumes: Task 2의 canonical method name과 RPC 유형. - Produces: method별 deadline·retry·idempotency·size·metadata 정책을 조회하는 immutable catalog. **Implementation requirements:** - Idempotency profile은 `READ_ONLY`, `NATURALLY_IDEMPOTENT`, `IDEMPOTENCY_KEY_REQUIRED`, `CONDITIONALLY_IDEMPOTENT`, `NON_IDEMPOTENT`, `STREAMING`이다. - Stable Unary method에는 deadline profile이 반드시 존재한다. - `NON_IDEMPOTENT` method에 explicit retry profile을 연결하면 catalog build를 실패시킨다. - `STREAMING` profile은 whole-call retry profile을 허용하지 않는다. - 중복 method registration과 descriptor에 없는 method registration을 거부한다. - [ ] **Step 1: Write the failing test** ```java class GrpcMethodPolicyCatalogTest { @org.junit.jupiter.api.Test void nonIdempotentMethodCannotEnableExplicitRetry() { var policy = GrpcMethodPolicy.nonIdempotentUnary( new GrpcMethodName("x.y.CommandService/Create"), "user-sync", "retry-write"); org.assertj.core.api.Assertions.assertThatThrownBy( () -> GrpcMethodPolicyCatalog.of(java.util.List.of(policy))) .isInstanceOf(IllegalArgumentException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.policy.GrpcMethodPolicyCatalogTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum RpcIdempotencyProfile { READ_ONLY, NATURALLY_IDEMPOTENT, IDEMPOTENCY_KEY_REQUIRED, CONDITIONALLY_IDEMPOTENT, NON_IDEMPOTENT, STREAMING } public record GrpcMethodPolicy( GrpcMethodName method, RpcType rpcType, RpcIdempotencyProfile idempotency, String deadlineProfile, String retryProfile, WaitForReadyPolicy waitForReady) { public static GrpcMethodPolicy nonIdempotentUnary( GrpcMethodName method, String deadline, String retry) { return new GrpcMethodPolicy(method, RpcType.UNARY, RpcIdempotencyProfile.NON_IDEMPOTENT, deadline, retry, WaitForReadyPolicy.DISABLED); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.policy.GrpcMethodPolicyCatalogTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/policy/RpcIdempotencyProfile.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/policy/GrpcMethodPolicy.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/policy/GrpcMethodPolicyCatalog.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/policy/WaitForReadyPolicy.java' 'modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/policy/GrpcMethodPolicyCatalogTest.java' git commit -m "feat: define grpc method policy catalog" ``` ### Task 4: 세 축 실행 증거 모델 **Files:** - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/evidence/GrpcTransportEvidence.java` - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/evidence/GrpcBusinessEvidence.java` - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/evidence/GrpcStreamEvidence.java` - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/evidence/GrpcExecutionEvidence.java` - Test: `modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/evidence/GrpcExecutionEvidenceTest.java` **Interfaces:** - Consumes: Task 2의 RPC 유형과 method identity. - Produces: Transport·Business·Stream evidence를 독립적으로 보존하는 immutable snapshot. **Implementation requirements:** - `RESPONSE_HEADERS_SEEN`을 `COMMIT_CONFIRMED`로 자동 승격하지 않는다. - Stream evidence는 `None`, `Partial`, `Applied`, `Resumable` sealed hierarchy다. - `APPLIED`는 application acknowledgement 증거가 있을 때만 생성한다. - Unary RPC에서 non-none stream evidence를 설정하면 검증 오류다. - Evidence snapshot은 예외와 관측 event에서 동일 타입을 사용한다. - [ ] **Step 1: Write the failing test** ```java class GrpcExecutionEvidenceTest { @org.junit.jupiter.api.Test void responseHeadersDoNotProveBusinessCommit() { var evidence = new GrpcExecutionEvidence( GrpcTransportEvidence.RESPONSE_HEADERS_SEEN, GrpcBusinessEvidence.UNKNOWN, new GrpcStreamEvidence.None()); org.assertj.core.api.Assertions.assertThat(evidence.business()) .isEqualTo(GrpcBusinessEvidence.UNKNOWN); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.evidence.GrpcExecutionEvidenceTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GrpcTransportEvidence { NOT_SENT, MAY_HAVE_LEFT_CLIENT, RESPONSE_HEADERS_SEEN, RESPONSE_MESSAGE_SEEN, TRAILERS_SEEN } public enum GrpcBusinessEvidence { NOT_OBSERVED, APPLICATION_STARTED, COMMIT_CONFIRMED, ABORT_CONFIRMED, UNKNOWN } public sealed interface GrpcStreamEvidence { record None() implements GrpcStreamEvidence {} record Partial(long lastSequence) implements GrpcStreamEvidence {} record Applied(long lastAppliedSequence) implements GrpcStreamEvidence {} record Resumable(String snapshotVersion, String cursor) implements GrpcStreamEvidence {} } public record GrpcExecutionEvidence( GrpcTransportEvidence transport, GrpcBusinessEvidence business, GrpcStreamEvidence stream) {} ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.evidence.GrpcExecutionEvidenceTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/evidence/GrpcTransportEvidence.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/evidence/GrpcBusinessEvidence.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/evidence/GrpcStreamEvidence.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/evidence/GrpcExecutionEvidence.java' 'modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/evidence/GrpcExecutionEvidenceTest.java' git commit -m "feat: model grpc execution evidence" ``` ### Task 5: Stable 오류·Status·Completion Outcome **Files:** - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/error/GrpcFailureCategory.java` - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/error/GrpcCompletionOutcome.java` - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/error/GrpcFailureContext.java` - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/error/GrpcPlatformException.java` - Test: `modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/error/GrpcFailureContextTest.java` **Interfaces:** - Consumes: Task 4의 실행 증거. - Produces: gRPC Status와 업무 완료 불명확성을 분리하는 안정 오류 모델. **Implementation requirements:** - `GrpcCompletionOutcome`은 `COMPLETED`, `REJECTED`, `COMPLETION_UNKNOWN`, `PARTIAL_STREAM`을 표현한다. - failure context에 method, status code, evidence, retry disposition, attempt, elapsed, trace ID를 보존한다. - payload, raw metadata, token, idempotency key 원문을 exception message에 넣지 않는다. - `DEADLINE_EXCEEDED` mutation은 기본적으로 `COMPLETION_UNKNOWN` 후보다. - provider·driver exception은 cause로 보존하되 공개 wire detail로 자동 변환하지 않는다. - [ ] **Step 1: Write the failing test** ```java class GrpcFailureContextTest { @org.junit.jupiter.api.Test void deadlineAfterPossibleSendIsCompletionUnknown() { var context = GrpcFailureContext.deadlineExceeded( new GrpcMethodName("x.y.CommandService/Create"), GrpcTransportEvidence.MAY_HAVE_LEFT_CLIENT); org.assertj.core.api.Assertions.assertThat(context.outcome()) .isEqualTo(GrpcCompletionOutcome.COMPLETION_UNKNOWN); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.error.GrpcFailureContextTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GrpcCompletionOutcome { COMPLETED, REJECTED, COMPLETION_UNKNOWN, PARTIAL_STREAM } public record GrpcFailureContext( GrpcMethodName method, String statusCode, GrpcExecutionEvidence evidence, GrpcCompletionOutcome outcome, boolean retryable, int attempt, java.time.Duration elapsed, String traceId) { public static GrpcFailureContext deadlineExceeded( GrpcMethodName method, GrpcTransportEvidence transport) { return new GrpcFailureContext(method, "DEADLINE_EXCEEDED", new GrpcExecutionEvidence(transport, GrpcBusinessEvidence.UNKNOWN, new GrpcStreamEvidence.None()), GrpcCompletionOutcome.COMPLETION_UNKNOWN, false, 1, java.time.Duration.ZERO, "redacted"); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.error.GrpcFailureContextTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/error/GrpcFailureCategory.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/error/GrpcCompletionOutcome.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/error/GrpcFailureContext.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/error/GrpcPlatformException.java' 'modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/error/GrpcFailureContextTest.java' git commit -m "feat: add grpc failure and completion model" ``` ### Task 6: Deadline·Cancellation Core Primitive **Files:** - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineBudget.java` - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/deadline/GrpcCancellationToken.java` - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineProfile.java` - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineExceededException.java` - Test: `modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineBudgetTest.java` **Interfaces:** - Consumes: Task 3의 method policy. - Produces: 상위 deadline과 method profile에서 안전 margin을 차감한 downstream budget. **Implementation requirements:** - 모든 Stable Unary method에 positive deadline을 요구한다. - downstream deadline은 parent remaining과 method default 중 더 짧은 값을 사용한다. - serialization/trailer safety reserve를 차감한다. - 남은 시간이 minimum attempt budget보다 작으면 새 dependency call을 시작하지 않는다. - cancellation token은 cancel reason과 발생 시각을 보존하고 idempotent하게 취소된다. - [ ] **Step 1: Write the failing test** ```java class GrpcDeadlineBudgetTest { @org.junit.jupiter.api.Test void downstreamBudgetNeverExceedsParent() { var parent = GrpcDeadlineBudget.of( java.time.Duration.ofSeconds(2)); var downstream = parent.child( java.time.Duration.ofSeconds(5), java.time.Duration.ofMillis(100)); org.assertj.core.api.Assertions.assertThat( downstream.remaining()) .isLessThanOrEqualTo(java.time.Duration.ofMillis(1900)); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.deadline.GrpcDeadlineBudgetTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcDeadlineBudget( java.time.Instant deadline) { public static GrpcDeadlineBudget of( java.time.Duration duration) { if (duration.isZero() || duration.isNegative()) { throw new IllegalArgumentException("positive deadline"); } return new GrpcDeadlineBudget( java.time.Instant.now().plus(duration)); } public java.time.Duration remaining() { var value = java.time.Duration.between( java.time.Instant.now(), deadline); return value.isNegative() ? java.time.Duration.ZERO : value; } public GrpcDeadlineBudget child( java.time.Duration configured, java.time.Duration reserve) { var child = java.time.Instant.now() .plus(configured).minus(reserve); return new GrpcDeadlineBudget( child.isBefore(deadline) ? child : deadline.minus(reserve)); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.deadline.GrpcDeadlineBudgetTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineBudget.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/deadline/GrpcCancellationToken.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineProfile.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineExceededException.java' 'modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineBudgetTest.java' git commit -m "feat: add grpc deadline and cancellation primitives" ``` ### Task 7: Metadata·Actor·Tenant Request Context **Files:** - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/context/GrpcRequestContext.java` - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/context/GrpcMetadataKey.java` - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/context/GrpcMetadataBudget.java` - Create: `modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/context/GrpcClientIdentity.java` - Test: `modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/context/GrpcMetadataBudgetTest.java` **Interfaces:** - Consumes: Task 6의 deadline budget과 기존 Security의 Actor·Tenant context. - Produces: 허용된 metadata만 immutable request context로 변환하는 bounded contract. **Implementation requirements:** - metadata key는 allowlist catalog에 등록된 ASCII/binary key만 허용한다. - `grpc-` prefix 사용자 정의를 거부한다. - 전체 hard budget과 user-defined soft budget을 분리한다. - Actor·Tenant는 검증된 authentication source에서만 생성하고 raw tenant header를 직접 신뢰하지 않는다. - credential, token, PII와 raw metadata map은 public request context에서 제거한다. - [ ] **Step 1: Write the failing test** ```java class GrpcMetadataBudgetTest { @org.junit.jupiter.api.Test void oversizedUserMetadataIsRejected() { var budget = new GrpcMetadataBudget(8192, 4096); org.assertj.core.api.Assertions.assertThatThrownBy( () -> budget.validate(2000, 5000)) .isInstanceOf(IllegalArgumentException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.context.GrpcMetadataBudgetTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcMetadataBudget( int hardBytes, int userBytes) { public void validate(int frameworkBytes, int suppliedUserBytes) { if (suppliedUserBytes > userBytes || frameworkBytes + suppliedUserBytes > hardBytes) { throw new IllegalArgumentException( "gRPC metadata budget exceeded"); } } } public record GrpcRequestContext( Object actor, Object tenant, java.util.Locale locale, String correlationId, String traceId, GrpcDeadlineBudget deadline, GrpcClientIdentity client) {} ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.context.GrpcMetadataBudgetTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/context/GrpcRequestContext.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/context/GrpcMetadataKey.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/context/GrpcMetadataBudget.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/context/GrpcClientIdentity.java' 'modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/context/GrpcMetadataBudgetTest.java' git commit -m "feat: define grpc metadata and request context" ``` ### Task 8: Proto Style Manifest와 Schema 규칙 **Files:** - Create: `modules/grpc/grpc-proto-contract/src/main/java/io/backend/skeleton/grpc/contract/GrpcProtoStyleManifest.java` - Create: `modules/grpc/grpc-proto-contract/src/main/java/io/backend/skeleton/grpc/contract/GrpcProtoRuleViolation.java` - Create: `modules/grpc/grpc-proto-contract/src/main/java/io/backend/skeleton/grpc/contract/GrpcProtoContractValidator.java` - Create: `modules/grpc/grpc-proto-contract/src/main/proto/hyeonworks/grpc/common/v1/error.proto` - Create: `modules/grpc/grpc-proto-contract/src/main/proto/hyeonworks/grpc/common/v1/stream.proto` - Create: `modules/grpc/grpc-proto-contract/src/main/proto/buf.yaml` - Test: `modules/grpc/grpc-proto-contract/src/test/java/io/backend/skeleton/grpc/contract/GrpcProtoContractValidatorTest.java` **Interfaces:** - Consumes: Task 2의 service·method identity와 source `.proto` files. - Produces: proto3+optional, package, reserved, enum zero, WKT 사용 규칙을 검증하는 contract validator. **Implementation requirements:** - Stable source는 `proto3`이며 presence가 필요한 scalar는 `optional`이다. - package는 `{organization}.{domain}.v{major}` 규칙을 따른다. - `java_multiple_files = true`와 hand-written package와 분리된 `java_package`를 요구한다. - 삭제된 field number/name의 `reserved` 선언을 schema history와 비교한다. - enum 0 값은 `_UNSPECIFIED` suffix를 요구하고 `Any`, `Struct`, map 사용을 allowlist한다. - [ ] **Step 1: Write the failing test** ```java class GrpcProtoContractValidatorTest { @org.junit.jupiter.api.Test void enumWithoutUnspecifiedZeroIsRejected() { var manifest = GrpcProtoStyleManifest.stable(); var validator = new GrpcProtoContractValidator(manifest); org.assertj.core.api.Assertions.assertThatThrownBy( () -> validator.validateEnumZero("Status", "ACTIVE", 0)) .isInstanceOf(GrpcProtoRuleViolation.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-proto-contract:test --tests 'io.backend.skeleton.grpc.contract.GrpcProtoContractValidatorTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcProtoStyleManifest( String syntax, boolean explicitPresence, boolean javaMultipleFiles, String enumZeroSuffix) { public static GrpcProtoStyleManifest stable() { return new GrpcProtoStyleManifest( "proto3", true, true, "_UNSPECIFIED"); } } public final class GrpcProtoContractValidator { private final GrpcProtoStyleManifest manifest; public GrpcProtoContractValidator( GrpcProtoStyleManifest manifest) { this.manifest = manifest; } public void validateEnumZero( String enumName, String valueName, int number) { if (number == 0 && !valueName.endsWith( manifest.enumZeroSuffix())) { throw new GrpcProtoRuleViolation(enumName); } } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-proto-contract:test --tests 'io.backend.skeleton.grpc.contract.GrpcProtoContractValidatorTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-proto-contract/src/main/java/io/backend/skeleton/grpc/contract/GrpcProtoStyleManifest.java' 'modules/grpc/grpc-proto-contract/src/main/java/io/backend/skeleton/grpc/contract/GrpcProtoRuleViolation.java' 'modules/grpc/grpc-proto-contract/src/main/java/io/backend/skeleton/grpc/contract/GrpcProtoContractValidator.java' 'modules/grpc/grpc-proto-contract/src/main/proto/hyeonworks/grpc/common/v1/error.proto' 'modules/grpc/grpc-proto-contract/src/main/proto/hyeonworks/grpc/common/v1/stream.proto' 'modules/grpc/grpc-proto-contract/src/main/proto/buf.yaml' 'modules/grpc/grpc-proto-contract/src/test/java/io/backend/skeleton/grpc/contract/GrpcProtoContractValidatorTest.java' git commit -m "feat: enforce grpc proto style manifest" ``` ### Task 9: Buf Format·Lint·Breaking Governance **Files:** - Create: `modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcBufPolicy.java` - Create: `modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcBreakingCategory.java` - Create: `modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcSchemaBaseline.java` - Create: `modules/grpc/grpc-codegen/src/main/kotlin/io.backend.grpc-buf-conventions.gradle.kts` - Create: `modules/grpc/grpc-proto-contract/buf.lock` - Create: `modules/grpc/grpc-proto-contract/buf.gen.yaml` - Test: `modules/grpc/grpc-codegen/src/test/java/io/backend/skeleton/grpc/codegen/GrpcBufPolicyTest.java` **Interfaces:** - Consumes: Task 8의 Proto style manifest와 released schema artifact. - Produces: Buf `FILE` breaking gate, lint·format·descriptor build를 Gradle lifecycle에 연결하는 convention. **Implementation requirements:** - `bufFormatCheck`, `bufLint`, `bufBuild`, `bufBreaking`을 CI task로 등록한다. - Stable public API는 Buf `FILE` category를 사용한다. - 비교 baseline은 마지막 released schema artifact로 고정한다. - wire-only compatibility로 source-breaking 변경을 통과시키지 않는다. - breaking 결과와 schema hash를 release artifact에 보존한다. - [ ] **Step 1: Write the failing test** ```java class GrpcBufPolicyTest { @org.junit.jupiter.api.Test void stableUsesFileBreakingCategory() { var policy = GrpcBufPolicy.stable( new GrpcSchemaBaseline("1.0.0", "sha256:abc")); org.assertj.core.api.Assertions.assertThat(policy.category()) .isEqualTo(GrpcBreakingCategory.FILE); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-codegen:test --tests 'io.backend.skeleton.grpc.codegen.GrpcBufPolicyTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GrpcBreakingCategory { FILE, PACKAGE, WIRE_JSON, WIRE } public record GrpcSchemaBaseline( String version, String digest) {} public record GrpcBufPolicy( GrpcBreakingCategory category, GrpcSchemaBaseline baseline, boolean formatRequired, boolean lintRequired) { public static GrpcBufPolicy stable( GrpcSchemaBaseline baseline) { return new GrpcBufPolicy( GrpcBreakingCategory.FILE, baseline, true, true); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-codegen:test --tests 'io.backend.skeleton.grpc.codegen.GrpcBufPolicyTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcBufPolicy.java' 'modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcBreakingCategory.java' 'modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcSchemaBaseline.java' 'modules/grpc/grpc-codegen/src/main/kotlin/io.backend.grpc-buf-conventions.gradle.kts' 'modules/grpc/grpc-proto-contract/buf.lock' 'modules/grpc/grpc-proto-contract/buf.gen.yaml' 'modules/grpc/grpc-codegen/src/test/java/io/backend/skeleton/grpc/codegen/GrpcBufPolicyTest.java' git commit -m "build: add buf schema governance" ``` ### Task 10: Spring Boot BOM 기반 Protobuf·gRPC Java Codegen **Files:** - Create: `modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcCodegenManifest.java` - Create: `modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcGeneratedPackagePolicy.java` - Create: `modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcCodegenOutput.java` - Create: `modules/grpc/grpc-codegen/src/main/kotlin/io.backend.grpc-codegen-conventions.gradle.kts` - Create: `modules/grpc/grpc-codegen/src/main/resources/META-INF/gradle-plugins/io.backend.grpc-codegen-conventions.properties` - Test: `modules/grpc/grpc-codegen/src/test/java/io/backend/skeleton/grpc/codegen/GrpcCodegenManifestTest.java` **Interfaces:** - Consumes: Task 9의 Buf policy와 Spring Boot 4.1 dependency management. - Produces: protoc·grpc-java generation option, output package, descriptor path를 고정하는 build convention. **Implementation requirements:** - Protobuf와 gRPC Java plugin version을 Boot BOM에서 읽고 중복 version 선언을 금지한다. - generated source root는 build directory 아래에 둔다. - hand-written package와 generated `*.proto` package를 겹치게 하지 않는다. - Java code generation owner는 Boot-managed protobuf plugin 한 곳이다. - descriptor set과 source info를 release artifact로 생성한다. - [ ] **Step 1: Write the failing test** ```java class GrpcCodegenManifestTest { @org.junit.jupiter.api.Test void bootBomOwnsToolVersions() { var manifest = GrpcCodegenManifest.bootManaged(); org.assertj.core.api.Assertions.assertThat( manifest.explicitProtocVersion()).isEmpty(); org.assertj.core.api.Assertions.assertThat( manifest.descriptorSetEnabled()).isTrue(); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-codegen:test --tests 'io.backend.skeleton.grpc.codegen.GrpcCodegenManifestTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcCodegenManifest( java.util.Optional explicitProtocVersion, java.util.Optional explicitGrpcPluginVersion, boolean descriptorSetEnabled, boolean includeSourceInfo) { public static GrpcCodegenManifest bootManaged() { return new GrpcCodegenManifest( java.util.Optional.empty(), java.util.Optional.empty(), true, true); } } public record GrpcGeneratedPackagePolicy( String protoPackagePrefix, String javaPackagePrefix, boolean separateFromHandwritten) {} ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-codegen:test --tests 'io.backend.skeleton.grpc.codegen.GrpcCodegenManifestTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcCodegenManifest.java' 'modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcGeneratedPackagePolicy.java' 'modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcCodegenOutput.java' 'modules/grpc/grpc-codegen/src/main/kotlin/io.backend.grpc-codegen-conventions.gradle.kts' 'modules/grpc/grpc-codegen/src/main/resources/META-INF/gradle-plugins/io.backend.grpc-codegen-conventions.properties' 'modules/grpc/grpc-codegen/src/test/java/io/backend/skeleton/grpc/codegen/GrpcCodegenManifestTest.java' git commit -m "build: standardize grpc java code generation" ``` ### Task 11: Descriptor Artifact와 Consumer Source 호환성 **Files:** - Create: `modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcDescriptorArtifact.java` - Create: `modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcConsumerFixture.java` - Create: `modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcSchemaArtifactPublisher.java` - Create: `modules/grpc/grpc-codegen/src/test/resources/consumer-fixtures/v1/build.gradle.kts` - Create: `modules/grpc/grpc-codegen/src/test/resources/consumer-fixtures/v1/src/main/java/fixture/DocumentClientFixture.java` - Test: `modules/grpc/grpc-codegen/src/test/java/io/backend/skeleton/grpc/codegen/GrpcDescriptorArtifactTest.java` **Interfaces:** - Consumes: Task 10의 generated descriptor와 Task 9의 schema baseline. - Produces: schema hash·descriptor·generated source hash·consumer compile 결과를 하나의 release artifact로 묶는 계약. **Implementation requirements:** - FileDescriptorSet, Buf image, schema hash, policy version을 보존한다. - 이전 Java client fixture를 새 generated sources와 함께 compile한다. - service path, method path, Java package source break를 별도로 보고한다. - artifact는 immutable version으로 publish한다. - consumer fixture 실패는 release를 차단한다. - [ ] **Step 1: Write the failing test** ```java class GrpcDescriptorArtifactTest { @org.junit.jupiter.api.Test void artifactRequiresConsumerCompatibilityEvidence() { org.assertj.core.api.Assertions.assertThatThrownBy( () -> new GrpcDescriptorArtifact( "sha256:schema", "sha256:generated", java.util.List.of())) .isInstanceOf(IllegalArgumentException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-codegen:test --tests 'io.backend.skeleton.grpc.codegen.GrpcDescriptorArtifactTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcConsumerFixture( String name, boolean compiled) {} public record GrpcDescriptorArtifact( String schemaDigest, String generatedSourceDigest, java.util.List fixtures) { public GrpcDescriptorArtifact { if (fixtures == null || fixtures.isEmpty() || fixtures.stream().anyMatch( fixture -> !fixture.compiled())) { throw new IllegalArgumentException( "consumer compatibility evidence required"); } } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-codegen:test --tests 'io.backend.skeleton.grpc.codegen.GrpcDescriptorArtifactTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcDescriptorArtifact.java' 'modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcConsumerFixture.java' 'modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcSchemaArtifactPublisher.java' 'modules/grpc/grpc-codegen/src/test/resources/consumer-fixtures/v1/build.gradle.kts' 'modules/grpc/grpc-codegen/src/test/resources/consumer-fixtures/v1/src/main/java/fixture/DocumentClientFixture.java' 'modules/grpc/grpc-codegen/src/test/java/io/backend/skeleton/grpc/codegen/GrpcDescriptorArtifactTest.java' git commit -m "build: publish grpc schema compatibility artifact" ``` ### Task 12: Protovalidate Transport Validation **Files:** - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/validation/GrpcTransportValidator.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/validation/GrpcValidationViolation.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/validation/ProtovalidateGrpcInterceptor.java` - Test: `modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/validation/ProtovalidateGrpcInterceptorTest.java` **Interfaces:** - Consumes: Generated request messages, Task 8의 validation owner rule와 server interceptor SPI. - Produces: pure transport constraint를 Application Use Case 진입 전에 검증하는 interceptor. **Implementation requirements:** - 길이, 범위, collection count, format, pure cross-field rule만 처리한다. - DB 조회·권한·현재 상태 검증은 금지한다. - violation을 `INVALID_ARGUMENT`와 `BadRequest` detail로 변환한다. - request body와 민감 field 값을 로그에 기록하지 않는다. - validation rule compilation failure는 build 또는 startup failure다. - [ ] **Step 1: Write the failing test** ```java class ProtovalidateGrpcInterceptorTest { @org.junit.jupiter.api.Test void invalidRequestNeverReachesApplication() { var validator = new GrpcTransportValidator( value -> java.util.List.of( new GrpcValidationViolation( "title", "max_len", "too long"))); org.assertj.core.api.Assertions.assertThat( validator.validate(new Object())).isNotEmpty(); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.validation.ProtovalidateGrpcInterceptorTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcValidationViolation( String field, String rule, String safeMessage) {} public final class GrpcTransportValidator { private final java.util.function.Function> delegate; public GrpcTransportValidator( java.util.function.Function> delegate) { this.delegate = delegate; } public java.util.List validate( Object request) { return java.util.List.copyOf(delegate.apply(request)); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.validation.ProtovalidateGrpcInterceptorTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/validation/GrpcTransportValidator.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/validation/GrpcValidationViolation.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/validation/ProtovalidateGrpcInterceptor.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/validation/ProtovalidateGrpcInterceptorTest.java' git commit -m "feat: add grpc transport validation" ``` ### Task 13: Application 경계와 Raw gRPC API 차단 **Files:** - Create: `modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/architecture/GrpcServiceAdapterMarker.java` - Create: `modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/architecture/GrpcApplicationBoundaryRules.java` - Create: `modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/architecture/GrpcRawApiImportRule.java` - Test: `modules/grpc/grpc-server/src/test/java/io/backend/skeleton/grpc/architecture/GrpcApplicationBoundaryRulesTest.java` **Interfaces:** - Consumes: Task 1 module graph, generated service classes와 Application package conventions. - Produces: Service Adapter가 Use Case만 호출하고 DB·HTTP·Messaging·Storage에 직접 접근하지 못하게 하는 ArchUnit rule. **Implementation requirements:** - gRPC service adapter는 `application` port만 의존한다. - `EntityManager`, Repository, `MongoTemplate`, HTTP client, broker template, content store direct dependency를 금지한다. - 일반 application module에서 raw `ManagedChannelBuilder`, `ServerBuilder`, `ClientCall`, `MethodDescriptor` import를 금지한다. - generated package는 raw API import rule 예외다. - architecture violation은 test failure다. - [ ] **Step 1: Write the failing test** ```java class GrpcApplicationBoundaryRulesTest { @org.junit.jupiter.api.Test void rawBuildersAreNotApplicationApi() { var rule = GrpcRawApiImportRule.defaultRule(); org.assertj.core.api.Assertions.assertThat( rule.forbiddenTypes()) .contains("io.grpc.ManagedChannelBuilder", "io.grpc.ServerBuilder"); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-server:test --tests 'io.backend.skeleton.grpc.architecture.GrpcApplicationBoundaryRulesTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public interface GrpcServiceAdapterMarker {} public record GrpcRawApiImportRule( java.util.Set forbiddenTypes) { public static GrpcRawApiImportRule defaultRule() { return new GrpcRawApiImportRule(java.util.Set.of( "io.grpc.ManagedChannelBuilder", "io.grpc.ServerBuilder", "io.grpc.MethodDescriptor", "io.grpc.ClientCall")); } } public final class GrpcApplicationBoundaryRules { public static java.util.Set forbiddenDependencies() { return java.util.Set.of( "jakarta.persistence.EntityManager", "org.springframework.data.repository.Repository", "org.springframework.data.mongodb.core.MongoTemplate", "org.springframework.web.client.RestClient", "org.springframework.web.reactive.function.client.WebClient"); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-server:test --tests 'io.backend.skeleton.grpc.architecture.GrpcApplicationBoundaryRulesTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/architecture/GrpcServiceAdapterMarker.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/architecture/GrpcApplicationBoundaryRules.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/architecture/GrpcRawApiImportRule.java' 'modules/grpc/grpc-server/src/test/java/io/backend/skeleton/grpc/architecture/GrpcApplicationBoundaryRulesTest.java' git commit -m "test: enforce grpc application boundary" ``` ### Task 14: Typed Service Adapter SPI **Files:** - Create: `modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServiceAdapter.java` - Create: `modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcApplicationInvocation.java` - Create: `modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcResponseMapper.java` - Create: `modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServiceAdapterDescriptor.java` - Test: `modules/grpc/grpc-server/src/test/java/io/backend/skeleton/grpc/server/GrpcServiceAdapterTest.java` **Interfaces:** - Consumes: Task 12 architecture boundary, generated request/response와 Application Use Case port. - Produces: Proto ↔ Application model 변환과 Use Case 호출만 수행하는 typed adapter contract. **Implementation requirements:** - adapter는 request mapper, application invocation, response mapper를 명시한다. - Application invocation에 immutable `GrpcRequestContext`를 전달한다. - adapter가 transport Status를 직접 임의 구성하지 않고 공통 error mapper를 사용한다. - blocking·async 실행 유형을 descriptor에 선언한다. - 파일 bytes와 provider SDK 객체를 request/response mapping에 노출하지 않는다. - [ ] **Step 1: Write the failing test** ```java class GrpcServiceAdapterTest { @org.junit.jupiter.api.Test void adapterDelegatesToApplicationInvocation() { var invoked = new java.util.concurrent.atomic.AtomicBoolean(); GrpcApplicationInvocation invocation = (command, context) -> { invoked.set(true); return "ok"; }; org.assertj.core.api.Assertions.assertThat( invocation.invoke("command", null)).isEqualTo("ok"); org.assertj.core.api.Assertions.assertThat(invoked).isTrue(); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-server:test --tests 'io.backend.skeleton.grpc.server.GrpcServiceAdapterTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java @FunctionalInterface public interface GrpcApplicationInvocation { R invoke(C command, GrpcRequestContext context); } public interface GrpcServiceAdapter extends GrpcServiceAdapterMarker { C toCommand(P request); R invoke(C command, GrpcRequestContext context); Q toResponse(R result); GrpcServiceAdapterDescriptor descriptor(); } public record GrpcServiceAdapterDescriptor( GrpcMethodName method, String executionProfile, String responseProfile) {} ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-server:test --tests 'io.backend.skeleton.grpc.server.GrpcServiceAdapterTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServiceAdapter.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcApplicationInvocation.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcResponseMapper.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServiceAdapterDescriptor.java' 'modules/grpc/grpc-server/src/test/java/io/backend/skeleton/grpc/server/GrpcServiceAdapterTest.java' git commit -m "feat: add typed grpc service adapter" ``` ### Task 15: Server Interceptor 순서 계약 **Files:** - Create: `modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServerInterceptorStage.java` - Create: `modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServerInterceptorOrder.java` - Create: `modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServerInterceptorChain.java` - Test: `modules/grpc/grpc-server/src/test/java/io/backend/skeleton/grpc/server/GrpcServerInterceptorOrderTest.java` **Interfaces:** - Consumes: Task 11 validation, Task 13 service adapter와 Security·Observability SPI. - Produces: 예외 경계부터 validation과 service adapter까지의 의미 순서를 고정하는 chain. **Implementation requirements:** - 순서는 Exception Boundary → Trace → Authentication → Actor/Tenant → Authorization → Admission → Deadline/Cancellation → Idempotency → Validation → Service Adapter다. - 중복 stage와 누락 required stage를 startup에서 거부한다. - 실제 framework wrapping 방향을 integration test로 검증한다. - Exception mapper가 downstream 전체를 감싼다. - client interceptor와 server interceptor 순서를 별도 catalog로 관리한다. - [ ] **Step 1: Write the failing test** ```java class GrpcServerInterceptorOrderTest { @org.junit.jupiter.api.Test void stableOrderPlacesValidationAfterIdempotency() { var order = GrpcServerInterceptorOrder.stable(); org.assertj.core.api.Assertions.assertThat( order.indexOf(GrpcServerInterceptorStage.VALIDATION)) .isGreaterThan( order.indexOf(GrpcServerInterceptorStage.IDEMPOTENCY)); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-server:test --tests 'io.backend.skeleton.grpc.server.GrpcServerInterceptorOrderTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GrpcServerInterceptorStage { EXCEPTION_BOUNDARY, TRACE, AUTHENTICATION, ACTOR_TENANT, AUTHORIZATION, ADMISSION, DEADLINE_CANCELLATION, IDEMPOTENCY, VALIDATION, SERVICE_ADAPTER } public record GrpcServerInterceptorOrder( java.util.List stages) { public static GrpcServerInterceptorOrder stable() { return new GrpcServerInterceptorOrder( java.util.List.of( GrpcServerInterceptorStage.EXCEPTION_BOUNDARY, GrpcServerInterceptorStage.TRACE, GrpcServerInterceptorStage.AUTHENTICATION, GrpcServerInterceptorStage.ACTOR_TENANT, GrpcServerInterceptorStage.AUTHORIZATION, GrpcServerInterceptorStage.ADMISSION, GrpcServerInterceptorStage.DEADLINE_CANCELLATION, GrpcServerInterceptorStage.IDEMPOTENCY, GrpcServerInterceptorStage.VALIDATION, GrpcServerInterceptorStage.SERVICE_ADAPTER)); } public int indexOf(GrpcServerInterceptorStage stage) { return stages.indexOf(stage); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-server:test --tests 'io.backend.skeleton.grpc.server.GrpcServerInterceptorOrderTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServerInterceptorStage.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServerInterceptorOrder.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServerInterceptorChain.java' 'modules/grpc/grpc-server/src/test/java/io/backend/skeleton/grpc/server/GrpcServerInterceptorOrderTest.java' git commit -m "feat: fix grpc server interceptor order" ``` ### Task 16: Context Propagation과 Thread 경계 **Files:** - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/context/GrpcContextBinder.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/context/GrpcContextSnapshot.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/context/GrpcContextPropagationPolicy.java` - Test: `modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/context/GrpcContextBinderTest.java` **Interfaces:** - Consumes: Task 7 request context와 Task 15 interceptor order. - Produces: gRPC Context, SecurityContext, Reactor Context, executor 경계에서 Actor·Tenant·Deadline을 보존하는 binder. **Implementation requirements:** - context snapshot은 immutable하며 credential 원문을 포함하지 않는다. - blocking executor와 virtual thread에 context를 복원하고 작업 종료 후 정리한다. - Reactor adapter가 있을 때 Reactor Context와 gRPC Context를 명시적으로 bridge한다. - context가 없는 비동기 작업은 fail-closed한다. - stream 수명 동안 actor·tenant·credential expiry를 보존한다. - [ ] **Step 1: Write the failing test** ```java class GrpcContextBinderTest { @org.junit.jupiter.api.Test void missingContextFailsClosed() { var binder = new GrpcContextBinder(); org.assertj.core.api.Assertions.assertThatThrownBy( binder::current) .isInstanceOf(IllegalStateException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.context.GrpcContextBinderTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcContextSnapshot( GrpcRequestContext requestContext, java.time.Instant capturedAt) {} public final class GrpcContextBinder { private static final ThreadLocal CURRENT = new ThreadLocal<>(); public GrpcContextSnapshot current() { var value = CURRENT.get(); if (value == null) { throw new IllegalStateException( "gRPC request context is required"); } return value; } public AutoCloseable bind(GrpcContextSnapshot value) { CURRENT.set(value); return CURRENT::remove; } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.context.GrpcContextBinderTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/context/GrpcContextBinder.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/context/GrpcContextSnapshot.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/context/GrpcContextPropagationPolicy.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/context/GrpcContextBinderTest.java' git commit -m "feat: propagate grpc request context" ``` ### Task 17: Status와 Rich Error Mapper **Files:** - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/error/GrpcStatusMapping.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/error/GrpcRichErrorDetail.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/error/GrpcErrorMapper.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/error/GrpcErrorExposurePolicy.java` - Test: `modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/error/GrpcErrorMapperTest.java` **Interfaces:** - Consumes: Task 5 failure model과 Application exception hierarchy. - Produces: 안정된 gRPC Status와 allowlisted google.rpc detail로 내부 오류를 변환하는 mapper. **Implementation requirements:** - validation은 `INVALID_ARGUMENT`, state precondition은 `FAILED_PRECONDITION`, concurrency abort는 `ABORTED`로 매핑한다. - retry hint는 `RetryInfo` detail로만 노출한다. - unknown exception은 `INTERNAL`과 opaque execution ID로 변환한다. - stack, SQL, query, host, token, PII를 detail에서 제거한다. - client는 message 문자열이 아니라 code·reason·typed detail로 분기한다. - [ ] **Step 1: Write the failing test** ```java class GrpcErrorMapperTest { @org.junit.jupiter.api.Test void unknownFailureIsOpaqueInternalError() { var mapped = new GrpcErrorMapper( GrpcErrorExposurePolicy.safe()) .map(new RuntimeException("jdbc:secret")); org.assertj.core.api.Assertions.assertThat(mapped.code()) .isEqualTo("INTERNAL"); org.assertj.core.api.Assertions.assertThat(mapped.message()) .doesNotContain("jdbc"); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.error.GrpcErrorMapperTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcStatusMapping( String code, String message, java.util.List details) {} public record GrpcRichErrorDetail( String type, java.util.Map safeFields) {} public record GrpcErrorExposurePolicy( java.util.Set allowedDetailTypes) { public static GrpcErrorExposurePolicy safe() { return new GrpcErrorExposurePolicy(java.util.Set.of( "BadRequest", "PreconditionFailure", "RetryInfo", "ResourceInfo", "ErrorInfo", "QuotaFailure", "LocalizedMessage")); } } public final class GrpcErrorMapper { public GrpcErrorMapper(GrpcErrorExposurePolicy policy) {} public GrpcStatusMapping map(Throwable error) { return new GrpcStatusMapping( "INTERNAL", "RPC execution failed", java.util.List.of(new GrpcRichErrorDetail( "ErrorInfo", java.util.Map.of("reason", "INTERNAL_ERROR")))); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.error.GrpcErrorMapperTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/error/GrpcStatusMapping.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/error/GrpcRichErrorDetail.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/error/GrpcErrorMapper.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/error/GrpcErrorExposurePolicy.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/error/GrpcErrorMapperTest.java' git commit -m "feat: add grpc status and rich error mapping" ``` ### Task 18: Netty Server Profile·Executor·Admission **Files:** - Create: `modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServerProfile.java` - Create: `modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServerTransport.java` - Create: `modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcExecutorProfile.java` - Create: `modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcAdmissionController.java` - Test: `modules/grpc/grpc-server/src/test/java/io/backend/skeleton/grpc/server/GrpcServerProfileTest.java` **Interfaces:** - Consumes: Task 15 interceptor chain과 Spring Boot gRPC server customization. - Produces: Netty Stable server의 size, metadata, concurrency, executor, keepalive, drain 설정. **Implementation requirements:** - Netty를 Stable 기본 transport로 사용한다. - blocking Use Case는 bounded executor에서 실행하며 event loop 직접 실행을 금지한다. - queue와 concurrent call limit을 넘으면 `RESOURCE_EXHAUSTED`로 거부한다. - `directExecutor`와 unbounded queue를 production에서 금지한다. - message·metadata·keepalive·connection age·shutdown 설정을 profile로 검증한다. - [ ] **Step 1: Write the failing test** ```java class GrpcServerProfileTest { @org.junit.jupiter.api.Test void productionRequiresBoundedExecutorAndLimits() { var profile = GrpcServerProfile.stableNetty(); org.assertj.core.api.Assertions.assertThat( profile.maxConcurrentCalls()).isPositive(); org.assertj.core.api.Assertions.assertThat( profile.executor().bounded()).isTrue(); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-server:test --tests 'io.backend.skeleton.grpc.server.GrpcServerProfileTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GrpcServerTransport { NETTY, NETTY_SHADED, SERVLET, IN_PROCESS } public record GrpcExecutorProfile( String name, int maxConcurrency, int queueCapacity, boolean bounded) {} public record GrpcServerProfile( GrpcServerTransport transport, int maxInboundMessageBytes, int maxInboundMetadataBytes, int maxConcurrentCalls, GrpcExecutorProfile executor) { public static GrpcServerProfile stableNetty() { return new GrpcServerProfile( GrpcServerTransport.NETTY, 4 * 1024 * 1024, 8 * 1024, 512, new GrpcExecutorProfile( "blocking-bounded", 256, 256, true)); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-server:test --tests 'io.backend.skeleton.grpc.server.GrpcServerProfileTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServerProfile.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServerTransport.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcExecutorProfile.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcAdmissionController.java' 'modules/grpc/grpc-server/src/test/java/io/backend/skeleton/grpc/server/GrpcServerProfileTest.java' git commit -m "feat: configure stable grpc netty server" ``` ### Task 19: Netty Shaded Transport 호환 Profile **Files:** - Create: `modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcNettyVariant.java` - Create: `modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcNettyVariantSelector.java` - Create: `modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcNettyParityContract.java` - Test: `modules/grpc/grpc-server/src/test/java/io/backend/skeleton/grpc/server/GrpcNettyVariantSelectorTest.java` **Interfaces:** - Consumes: Task 18 Stable Netty profile. - Produces: `grpc-netty`와 `grpc-netty-shaded`를 상호 배타적으로 선택하고 기능 동등성을 검증하는 profile. **Implementation requirements:** - 한 runtime에 unshaded와 shaded transport가 동시에 활성화되지 않게 한다. - TLS, metadata limit, message limit, health, reflection, graceful shutdown parity를 검증한다. - shaded profile은 dependency conflict 회피 목적이며 기능을 추가하지 않는다. - variant 선택은 build/runtime profile에서 명시한다. - 실제 Netty integration suite를 두 variant에 실행한다. - [ ] **Step 1: Write the failing test** ```java class GrpcNettyVariantSelectorTest { @org.junit.jupiter.api.Test void twoNettyVariantsCannotBeEnabledTogether() { org.assertj.core.api.Assertions.assertThatThrownBy( () -> new GrpcNettyVariantSelector( java.util.Set.of( GrpcNettyVariant.NETTY, GrpcNettyVariant.NETTY_SHADED))) .isInstanceOf(IllegalArgumentException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-server:test --tests 'io.backend.skeleton.grpc.server.GrpcNettyVariantSelectorTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GrpcNettyVariant { NETTY, NETTY_SHADED } public record GrpcNettyVariantSelector( java.util.Set enabled) { public GrpcNettyVariantSelector { if (enabled == null || enabled.size() != 1) { throw new IllegalArgumentException( "exactly one Netty variant is required"); } } } public record GrpcNettyParityContract( boolean tls, boolean metadataLimit, boolean gracefulShutdown) {} ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-server:test --tests 'io.backend.skeleton.grpc.server.GrpcNettyVariantSelectorTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcNettyVariant.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcNettyVariantSelector.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcNettyParityContract.java' 'modules/grpc/grpc-server/src/test/java/io/backend/skeleton/grpc/server/GrpcNettyVariantSelectorTest.java' git commit -m "build: add shaded netty compatibility profile" ``` ### Task 20: TLS·mTLS·CallCredentials와 Rotation **Files:** - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/security/GrpcTlsProfile.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/security/GrpcAuthenticationProfile.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/security/GrpcCredentialGeneration.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/security/GrpcCredentialRotationManager.java` - Test: `modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/security/GrpcTlsProfileTest.java` **Interfaces:** - Consumes: Spring Boot SSL Bundle, Security token source와 Task 24 예정 Named Channel profile. - Produces: TLS/mTLS와 per-call credential을 분리하고 generation 기반으로 교체하는 security contract. **Implementation requirements:** - dev·stage·prod에서 TLS를 필수로 한다. - mTLS는 service identity가 필요한 profile에서 사용한다. - Bearer/JWT/service token은 `CallCredentials` 또는 승인된 credential adapter가 생성한다. - trust-all과 hostname verification 비활성화를 production에서 startup failure로 처리한다. - credential·certificate 교체 시 새 generation을 만들고 기존 call/stream을 drain한다. - [ ] **Step 1: Write the failing test** ```java class GrpcTlsProfileTest { @org.junit.jupiter.api.Test void productionRejectsTrustAll() { org.assertj.core.api.Assertions.assertThatThrownBy( () -> GrpcTlsProfile.production( "internal-ca", false, true)) .isInstanceOf(IllegalArgumentException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.security.GrpcTlsProfileTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcTlsProfile( String sslBundle, boolean hostnameVerification, boolean trustAll, boolean mutualTls) { public static GrpcTlsProfile production( String bundle, boolean hostnameVerification, boolean trustAll) { if (!hostnameVerification || trustAll) { throw new IllegalArgumentException( "secure TLS verification is required"); } return new GrpcTlsProfile( bundle, true, false, false); } } public record GrpcCredentialGeneration( long generation, java.time.Instant createdAt) {} ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.security.GrpcTlsProfileTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/security/GrpcTlsProfile.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/security/GrpcAuthenticationProfile.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/security/GrpcCredentialGeneration.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/security/GrpcCredentialRotationManager.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/security/GrpcTlsProfileTest.java' git commit -m "feat: add grpc tls and credential rotation" ``` ### Task 21: 표준 Health Service와 Service 상태 **Files:** - Create: `modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcHealthState.java` - Create: `modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcServiceHealthRegistry.java` - Create: `modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcHealthPolicy.java` - Test: `modules/grpc/grpc-admin/src/test/java/io/backend/skeleton/grpc/admin/GrpcServiceHealthRegistryTest.java` **Interfaces:** - Consumes: Server service catalog와 dependency health signals. - Produces: global·service별 health, readiness, draining과 Watch를 제공하는 표준 health registry. **Implementation requirements:** - 상태는 `UNKNOWN`, `SERVING`, `NOT_SERVING`, `SERVICE_UNKNOWN`, `DRAINING`으로 표현한다. - startup 중에는 요청을 받기 전에 readiness를 false로 유지한다. - drain 시작 시 service health를 먼저 `DRAINING/NOT_SERVING`으로 전환한다. - dependency health가 business correctness에 필수일 때만 service health에 반영한다. - health endpoint의 network·credential 접근 정책을 business RPC와 분리한다. - [ ] **Step 1: Write the failing test** ```java class GrpcServiceHealthRegistryTest { @org.junit.jupiter.api.Test void drainingServiceIsNotReadyForNewCalls() { var registry = new GrpcServiceHealthRegistry(); registry.draining("DocumentService"); org.assertj.core.api.Assertions.assertThat( registry.state("DocumentService")) .isEqualTo(GrpcHealthState.DRAINING); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-admin:test --tests 'io.backend.skeleton.grpc.admin.GrpcServiceHealthRegistryTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GrpcHealthState { UNKNOWN, SERVING, NOT_SERVING, SERVICE_UNKNOWN, DRAINING } public final class GrpcServiceHealthRegistry { private final java.util.concurrent.ConcurrentMap states = new java.util.concurrent.ConcurrentHashMap<>(); public void draining(String service) { states.put(service, GrpcHealthState.DRAINING); } public GrpcHealthState state(String service) { return states.getOrDefault( service, GrpcHealthState.UNKNOWN); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-admin:test --tests 'io.backend.skeleton.grpc.admin.GrpcServiceHealthRegistryTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcHealthState.java' 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcServiceHealthRegistry.java' 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcHealthPolicy.java' 'modules/grpc/grpc-admin/src/test/java/io/backend/skeleton/grpc/admin/GrpcServiceHealthRegistryTest.java' git commit -m "feat: add grpc health registry" ``` ### Task 22: Reflection 환경·권한 정책 **Files:** - Create: `modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcReflectionMode.java` - Create: `modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcReflectionPolicy.java` - Create: `modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcReflectionAccessDecision.java` - Test: `modules/grpc/grpc-admin/src/test/java/io/backend/skeleton/grpc/admin/GrpcReflectionPolicyTest.java` **Interfaces:** - Consumes: Schema descriptor artifact와 environment/security profile. - Produces: Local/Test 허용, Dev 관리자 인증, Production 기본 비활성인 reflection policy. **Implementation requirements:** - Reflection mode는 `ENABLED`, `ADMIN_ONLY`, `DISABLED`다. - production default는 `DISABLED`다. - Admin-only reflection은 별도 network/role predicate를 요구한다. - reflection 공개 여부와 실제 RPC authorization을 동일시하지 않는다. - schema descriptor version을 admin snapshot에 노출하되 secret은 제외한다. - [ ] **Step 1: Write the failing test** ```java class GrpcReflectionPolicyTest { @org.junit.jupiter.api.Test void productionDefaultsToDisabled() { org.assertj.core.api.Assertions.assertThat( GrpcReflectionPolicy.production().mode()) .isEqualTo(GrpcReflectionMode.DISABLED); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-admin:test --tests 'io.backend.skeleton.grpc.admin.GrpcReflectionPolicyTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GrpcReflectionMode { ENABLED, ADMIN_ONLY, DISABLED } public record GrpcReflectionPolicy( GrpcReflectionMode mode, java.util.Set requiredRoles) { public static GrpcReflectionPolicy production() { return new GrpcReflectionPolicy( GrpcReflectionMode.DISABLED, java.util.Set.of()); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-admin:test --tests 'io.backend.skeleton.grpc.admin.GrpcReflectionPolicyTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcReflectionMode.java' 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcReflectionPolicy.java' 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcReflectionAccessDecision.java' 'modules/grpc/grpc-admin/src/test/java/io/backend/skeleton/grpc/admin/GrpcReflectionPolicyTest.java' git commit -m "feat: govern grpc reflection access" ``` ### Task 23: Graceful Shutdown과 Stream Drain **Files:** - Create: `modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcDrainPhase.java` - Create: `modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcDrainPolicy.java` - Create: `modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcDrainCoordinator.java` - Create: `modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcDrainResult.java` - Test: `modules/grpc/grpc-admin/src/test/java/io/backend/skeleton/grpc/admin/GrpcDrainCoordinatorTest.java` **Interfaces:** - Consumes: Task 21 health registry, server call registry와 streaming session registry. - Produces: readiness 차단부터 unary·stream drain과 force cancel까지의 deterministic shutdown. **Implementation requirements:** - 순서는 readiness false → health draining → new admission reject → unary drain → stream signal → force cancel이다. - drain과 force timeout을 분리한다. - stream에는 마지막 sequence·resume cursor 또는 full resync 필요 상태를 전달한다. - drain 중 새 retry·new stream을 시작하지 않는다. - drain 결과에 completed·cancelled·timed-out call 수를 보존한다. - [ ] **Step 1: Write the failing test** ```java class GrpcDrainCoordinatorTest { @org.junit.jupiter.api.Test void healthChangesBeforeNewAdmissionStops() { var coordinator = new GrpcDrainCoordinator( GrpcDrainPolicy.standard()); var phases = coordinator.plan(); org.assertj.core.api.Assertions.assertThat(phases) .containsSubsequence( GrpcDrainPhase.READINESS_OFF, GrpcDrainPhase.HEALTH_DRAINING, GrpcDrainPhase.ADMISSION_CLOSED); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-admin:test --tests 'io.backend.skeleton.grpc.admin.GrpcDrainCoordinatorTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GrpcDrainPhase { READINESS_OFF, HEALTH_DRAINING, ADMISSION_CLOSED, UNARY_DRAIN, STREAM_SIGNAL, FORCE_CANCEL, COMPLETE } public record GrpcDrainPolicy( java.time.Duration drainTimeout, java.time.Duration forceTimeout) { public static GrpcDrainPolicy standard() { return new GrpcDrainPolicy( java.time.Duration.ofSeconds(30), java.time.Duration.ofSeconds(5)); } } public final class GrpcDrainCoordinator { public GrpcDrainCoordinator(GrpcDrainPolicy policy) {} public java.util.List plan() { return java.util.List.of( GrpcDrainPhase.READINESS_OFF, GrpcDrainPhase.HEALTH_DRAINING, GrpcDrainPhase.ADMISSION_CLOSED, GrpcDrainPhase.UNARY_DRAIN, GrpcDrainPhase.STREAM_SIGNAL, GrpcDrainPhase.FORCE_CANCEL, GrpcDrainPhase.COMPLETE); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-admin:test --tests 'io.backend.skeleton.grpc.admin.GrpcDrainCoordinatorTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcDrainPhase.java' 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcDrainPolicy.java' 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcDrainCoordinator.java' 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcDrainResult.java' 'modules/grpc/grpc-admin/src/test/java/io/backend/skeleton/grpc/admin/GrpcDrainCoordinatorTest.java' git commit -m "feat: implement grpc graceful drain" ``` ### Task 24: Named Channel Profile **Files:** - Create: `modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcNamedChannelProfile.java` - Create: `modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcLoadBalancingPolicy.java` - Create: `modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcRetryOwner.java` - Create: `modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcChannelProfileValidator.java` - Test: `modules/grpc/grpc-client/src/test/java/io/backend/skeleton/grpc/client/GrpcNamedChannelProfileTest.java` **Interfaces:** - Consumes: Task 3 method policies, Task 20 TLS profiles와 deployment configuration. - Produces: logical channel name별 target, transport, LB, TLS, deadline, retry owner, size limit 계약. **Implementation requirements:** - target은 resolver scheme을 포함한 URI 형태로 검증한다. - load balancing은 Stable에서 `PICK_FIRST`, `ROUND_ROBIN`만 허용한다. - retry owner는 `GRPC_PLATFORM`, `SERVICE_MESH`, `APPLICATION`, `NONE` 중 하나다. - TLS, metadata/message limit, wait-for-ready, observability 설정을 profile에 고정한다. - 동일 service라도 long stream과 short unary의 SLO가 다르면 별도 channel profile을 사용한다. - [ ] **Step 1: Write the failing test** ```java class GrpcNamedChannelProfileTest { @org.junit.jupiter.api.Test void meshOwnedRetryCannotAlsoUseGrpcRetry() { var profile = GrpcNamedChannelProfile.mesh( new GrpcChannelProfileName("document")); org.assertj.core.api.Assertions.assertThatThrownBy( () -> new GrpcChannelProfileValidator() .validate(profile, true)) .isInstanceOf(IllegalArgumentException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-client:test --tests 'io.backend.skeleton.grpc.client.GrpcNamedChannelProfileTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GrpcLoadBalancingPolicy { PICK_FIRST, ROUND_ROBIN } public enum GrpcRetryOwner { GRPC_PLATFORM, SERVICE_MESH, APPLICATION, NONE } public record GrpcNamedChannelProfile( GrpcChannelProfileName name, String target, GrpcLoadBalancingPolicy loadBalancing, GrpcRetryOwner retryOwner, GrpcTlsProfile tls, int maxInboundMessageBytes, int maxMetadataBytes) { public static GrpcNamedChannelProfile mesh( GrpcChannelProfileName name) { return new GrpcNamedChannelProfile( name, "dns:///service:9090", GrpcLoadBalancingPolicy.PICK_FIRST, GrpcRetryOwner.SERVICE_MESH, GrpcTlsProfile.production( "internal-ca", true, false), 4 * 1024 * 1024, 8 * 1024); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-client:test --tests 'io.backend.skeleton.grpc.client.GrpcNamedChannelProfileTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcNamedChannelProfile.java' 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcLoadBalancingPolicy.java' 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcRetryOwner.java' 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcChannelProfileValidator.java' 'modules/grpc/grpc-client/src/test/java/io/backend/skeleton/grpc/client/GrpcNamedChannelProfileTest.java' git commit -m "feat: define grpc named channel profiles" ``` ### Task 25: Channel Runtime Generation과 Drain **Files:** - Create: `modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcChannelGeneration.java` - Create: `modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcChannelRuntime.java` - Create: `modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcChannelRuntimeRegistry.java` - Create: `modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcChannelDrainPolicy.java` - Test: `modules/grpc/grpc-client/src/test/java/io/backend/skeleton/grpc/client/GrpcChannelRuntimeRegistryTest.java` **Interfaces:** - Consumes: Task 24 Named Channel Profile과 credential generation. - Produces: Channel·Stub을 재사용하고 설정·인증서 교체 시 새 generation으로 무중단 전환하는 runtime registry. **Implementation requirements:** - 요청마다 channel을 생성하지 않는다. - profile generation 변경 시 새 runtime을 준비한 뒤 atomic pointer를 교체한다. - 기존 runtime은 in-flight unary와 stream을 drain한다. - drain timeout 후 남은 call을 cancel하고 metric을 기록한다. - runtime registry는 credential·raw builder를 application에 반환하지 않는다. - [ ] **Step 1: Write the failing test** ```java class GrpcChannelRuntimeRegistryTest { @org.junit.jupiter.api.Test void rotationCreatesNewGenerationBeforeOldDrain() { var registry = new GrpcChannelRuntimeRegistry(); var first = registry.install("document"); var second = registry.rotate("document"); org.assertj.core.api.Assertions.assertThat( second.generation()).isGreaterThan(first.generation()); org.assertj.core.api.Assertions.assertThat( registry.current("document")).isEqualTo(second); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-client:test --tests 'io.backend.skeleton.grpc.client.GrpcChannelRuntimeRegistryTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcChannelGeneration(long value) {} public record GrpcChannelRuntime( String profile, GrpcChannelGeneration generation, boolean acceptingCalls) {} public final class GrpcChannelRuntimeRegistry { private final java.util.concurrent.ConcurrentMap current = new java.util.concurrent.ConcurrentHashMap<>(); public GrpcChannelRuntime install(String profile) { var runtime = new GrpcChannelRuntime( profile, new GrpcChannelGeneration(1), true); current.put(profile, runtime); return runtime; } public GrpcChannelRuntime rotate(String profile) { var old = current(profile); var next = new GrpcChannelRuntime( profile, new GrpcChannelGeneration( old.generation().value() + 1), true); current.put(profile, next); return next; } public GrpcChannelRuntime current(String profile) { return current.get(profile); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-client:test --tests 'io.backend.skeleton.grpc.client.GrpcChannelRuntimeRegistryTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcChannelGeneration.java' 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcChannelRuntime.java' 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcChannelRuntimeRegistry.java' 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcChannelDrainPolicy.java' 'modules/grpc/grpc-client/src/test/java/io/backend/skeleton/grpc/client/GrpcChannelRuntimeRegistryTest.java' git commit -m "feat: manage grpc channel generations" ``` ### Task 26: Typed Stub Factory와 Method Policy 적용 **Files:** - Create: `modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcTypedStubFactory.java` - Create: `modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcStubDescriptor.java` - Create: `modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcStubPolicyApplier.java` - Test: `modules/grpc/grpc-client/src/test/java/io/backend/skeleton/grpc/client/GrpcTypedStubFactoryTest.java` **Interfaces:** - Consumes: Task 24 channel profile, Task 25 runtime registry, generated stub classes. - Produces: registered generated stub만 생성하고 method policy·deadline·credentials를 적용하는 typed factory. **Implementation requirements:** - stub factory는 allowlisted generated service type만 생성한다. - stub마다 Named Channel Profile과 method policy catalog를 연결한다. - raw Channel·ManagedChannel과 builder를 application에 반환하지 않는다. - blocking, future, async stub 유형을 descriptor로 구분한다. - method 호출 직전에 effective deadline과 call credentials를 적용한다. - [ ] **Step 1: Write the failing test** ```java class GrpcTypedStubFactoryTest { @org.junit.jupiter.api.Test void unregisteredStubTypeIsRejected() { var factory = GrpcTypedStubFactory.empty(); org.assertj.core.api.Assertions.assertThatThrownBy( () -> factory.create(Object.class, "document")) .isInstanceOf(IllegalArgumentException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-client:test --tests 'io.backend.skeleton.grpc.client.GrpcTypedStubFactoryTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcStubDescriptor( Class stubType, String profile, String style) {} public final class GrpcTypedStubFactory { private final java.util.Map, GrpcStubDescriptor> types; private GrpcTypedStubFactory( java.util.Map, GrpcStubDescriptor> types) { this.types = types; } public static GrpcTypedStubFactory empty() { return new GrpcTypedStubFactory(java.util.Map.of()); } public T create(Class type, String profile) { if (!types.containsKey(type)) { throw new IllegalArgumentException( "unregistered generated stub"); } throw new UnsupportedOperationException( "transport adapter creates the generated stub"); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-client:test --tests 'io.backend.skeleton.grpc.client.GrpcTypedStubFactoryTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcTypedStubFactory.java' 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcStubDescriptor.java' 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcStubPolicyApplier.java' 'modules/grpc/grpc-client/src/test/java/io/backend/skeleton/grpc/client/GrpcTypedStubFactoryTest.java' git commit -m "feat: add grpc typed stub factory" ``` ### Task 27: Client Metadata와 CallCredentials **Files:** - Create: `modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcClientMetadataPolicy.java` - Create: `modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcCallCredentialProvider.java` - Create: `modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcClientCallContext.java` - Test: `modules/grpc/grpc-client/src/test/java/io/backend/skeleton/grpc/client/GrpcClientMetadataPolicyTest.java` **Interfaces:** - Consumes: Task 7 metadata keys, Task 20 credential profiles와 Task 26 stub factory. - Produces: trace·correlation·idempotency metadata와 per-call credential을 안전하게 materialize하는 client policy. **Implementation requirements:** - Authorization은 application-provided generic header가 아니라 credential provider가 생성한다. - tenant·actor context는 allowlisted signed/validated representation만 전달한다. - cross-trust-boundary channel에서 baggage·tenant metadata allowlist를 별도 적용한다. - metadata budget을 call 시작 전에 검증한다. - token, cookie, password와 raw PII를 log·metric에 기록하지 않는다. - [ ] **Step 1: Write the failing test** ```java class GrpcClientMetadataPolicyTest { @org.junit.jupiter.api.Test void applicationCannotOverrideAuthorizationMetadata() { var policy = GrpcClientMetadataPolicy.standard(); org.assertj.core.api.Assertions.assertThatThrownBy( () -> policy.validateUserKey("authorization")) .isInstanceOf(IllegalArgumentException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-client:test --tests 'io.backend.skeleton.grpc.client.GrpcClientMetadataPolicyTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcClientMetadataPolicy( java.util.Set reservedKeys, GrpcMetadataBudget budget) { public static GrpcClientMetadataPolicy standard() { return new GrpcClientMetadataPolicy( java.util.Set.of( "authorization", "grpc-timeout", "traceparent", "tracestate"), new GrpcMetadataBudget(8192, 4096)); } public void validateUserKey(String key) { if (reservedKeys.contains(key.toLowerCase())) { throw new IllegalArgumentException( "reserved gRPC metadata key"); } } } public interface GrpcCallCredentialProvider { Object credentials(GrpcClientCallContext context); } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-client:test --tests 'io.backend.skeleton.grpc.client.GrpcClientMetadataPolicyTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcClientMetadataPolicy.java' 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcCallCredentialProvider.java' 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcClientCallContext.java' 'modules/grpc/grpc-client/src/test/java/io/backend/skeleton/grpc/client/GrpcClientMetadataPolicyTest.java' git commit -m "feat: secure grpc client metadata" ``` ### Task 28: Deadline 전파와 Dependency Budget **Files:** - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineCalculator.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDependencyBudget.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDeadlinePolicyValidator.java` - Test: `modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineCalculatorTest.java` **Interfaces:** - Consumes: Task 6 deadline primitive, Task 3 method policy와 current gRPC Context. - Produces: parent remaining deadline과 method profile에서 downstream call budget을 계산하는 policy. **Implementation requirements:** - effective deadline은 parent remaining과 method default 중 더 짧다. - safety margin과 response/trailer reserve를 차감한다. - DB·HTTP·downstream gRPC timeout이 inbound deadline을 초과하면 startup 검증에서 실패한다. - minimum attempt budget보다 짧으면 call을 시작하지 않는다. - deadline이 없는 Stable Unary call을 거부한다. - [ ] **Step 1: Write the failing test** ```java class GrpcDeadlineCalculatorTest { @org.junit.jupiter.api.Test void childDeadlineReservesResponseBudget() { var calculator = new GrpcDeadlineCalculator( java.time.Duration.ofMillis(100)); var child = calculator.calculate( java.time.Duration.ofSeconds(2), java.time.Duration.ofSeconds(5)); org.assertj.core.api.Assertions.assertThat(child) .isLessThanOrEqualTo( java.time.Duration.ofMillis(1900)); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.deadline.GrpcDeadlineCalculatorTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public final class GrpcDeadlineCalculator { private final java.time.Duration reserve; public GrpcDeadlineCalculator( java.time.Duration reserve) { this.reserve = reserve; } public java.time.Duration calculate( java.time.Duration parentRemaining, java.time.Duration methodDefault) { var minimum = parentRemaining.compareTo(methodDefault) < 0 ? parentRemaining : methodDefault; var result = minimum.minus(reserve); if (result.isZero() || result.isNegative()) { throw new GrpcDeadlineExceededException( "insufficient downstream budget"); } return result; } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.deadline.GrpcDeadlineCalculatorTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineCalculator.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDependencyBudget.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDeadlinePolicyValidator.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineCalculatorTest.java' git commit -m "feat: propagate grpc deadline budgets" ``` ### Task 29: Cancellation 전파와 Side-effect 차단 **Files:** - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcCancellationCoordinator.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcCancellableOperation.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcCancellationReason.java` - Test: `modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/deadline/GrpcCancellationCoordinatorTest.java` **Interfaces:** - Consumes: Task 6 cancellation token과 Application dependency cancellation adapters. - Produces: client cancel/deadline을 Future·Publisher·HTTP·DB 작업에 전파하고 새 side effect 시작을 차단하는 coordinator. **Implementation requirements:** - cancel은 idempotent하며 첫 reason을 보존한다. - Application Use Case는 cancellation token을 인자로 받거나 context에서 조회한다. - cancel 이후 신규 외부 side effect를 시작하지 않는다. - commit 경계 이후 cancel은 business evidence를 자동 ABORT로 바꾸지 않는다. - stream source와 writer를 모두 cancel하고 queue를 정리한다. - [ ] **Step 1: Write the failing test** ```java class GrpcCancellationCoordinatorTest { @org.junit.jupiter.api.Test void cancellationInvokesRegisteredOperationsOnce() { var calls = new java.util.concurrent.atomic.AtomicInteger(); var coordinator = new GrpcCancellationCoordinator(); coordinator.register(calls::incrementAndGet); coordinator.cancel(GrpcCancellationReason.CLIENT_CANCELLED); coordinator.cancel(GrpcCancellationReason.DEADLINE); org.assertj.core.api.Assertions.assertThat(calls).hasValue(1); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.deadline.GrpcCancellationCoordinatorTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GrpcCancellationReason { CLIENT_CANCELLED, DEADLINE, SERVER_DRAIN, POLICY } @FunctionalInterface public interface GrpcCancellableOperation { void cancel(); } public final class GrpcCancellationCoordinator { private final java.util.List operations = new java.util.concurrent.CopyOnWriteArrayList<>(); private final java.util.concurrent.atomic.AtomicBoolean cancelled = new java.util.concurrent.atomic.AtomicBoolean(); public void register(GrpcCancellableOperation operation) { if (cancelled.get()) { operation.cancel(); } else { operations.add(operation); } } public void cancel(GrpcCancellationReason reason) { if (cancelled.compareAndSet(false, true)) { operations.forEach(GrpcCancellableOperation::cancel); } } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.deadline.GrpcCancellationCoordinatorTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcCancellationCoordinator.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcCancellableOperation.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcCancellationReason.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/deadline/GrpcCancellationCoordinatorTest.java' git commit -m "feat: propagate grpc cancellation" ``` ### Task 30: Service Config와 Retry Owner 검증 **Files:** - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcServiceConfigPolicy.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcMethodRetryConfig.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcRetryOwnershipValidator.java` - Test: `modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/resilience/GrpcRetryOwnershipValidatorTest.java` **Interfaces:** - Consumes: Task 24 channel retry owner와 Task 3 method policy. - Produces: method별 timeout·wait-for-ready·retry를 하나의 owner만 관리하게 하는 Service Config contract. **Implementation requirements:** - APPLICATION, GRPC_PLATFORM, SERVICE_MESH, NONE 중 explicit retry owner는 하나다. - Service Config의 method name이 policy catalog와 일치해야 한다. - mesh owner일 때 gRPC explicit retry와 hedging을 비활성화한다. - retry policy가 없는 것과 channel retry 자체를 disable한 것을 구분한다. - transparent retry 존재 여부를 운영 snapshot에 기록한다. - [ ] **Step 1: Write the failing test** ```java class GrpcRetryOwnershipValidatorTest { @org.junit.jupiter.api.Test void twoExplicitRetryOwnersAreRejected() { var validator = new GrpcRetryOwnershipValidator(); org.assertj.core.api.Assertions.assertThatThrownBy( () -> validator.validate( java.util.Set.of( GrpcRetryOwner.GRPC_PLATFORM, GrpcRetryOwner.SERVICE_MESH))) .isInstanceOf(IllegalArgumentException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.resilience.GrpcRetryOwnershipValidatorTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcMethodRetryConfig( GrpcMethodName method, int maxAttempts, java.time.Duration initialBackoff, java.util.Set retryableStatusCodes) {} public final class GrpcRetryOwnershipValidator { public void validate( java.util.Set explicitOwners) { var count = explicitOwners.stream() .filter(owner -> owner != GrpcRetryOwner.NONE) .count(); if (count > 1) { throw new IllegalArgumentException( "one explicit retry owner is required"); } } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.resilience.GrpcRetryOwnershipValidatorTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcServiceConfigPolicy.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcMethodRetryConfig.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcRetryOwnershipValidator.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/resilience/GrpcRetryOwnershipValidatorTest.java' git commit -m "feat: validate grpc retry ownership" ``` ### Task 31: Retry Eligibility와 Budget **Files:** - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcRetryDecision.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcRetryBudget.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcRetryEligibility.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcRetryCoordinator.java` - Test: `modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/resilience/GrpcRetryEligibilityTest.java` **Interfaces:** - Consumes: Task 3 idempotency profile, Task 4 evidence, Task 29 retry ownership과 deadline budget. - Produces: status·idempotency·evidence·deadline·partial delivery를 함께 판정하는 retry coordinator. **Implementation requirements:** - `NON_IDEMPOTENT`에 explicit retry를 허용하지 않는다. - `IDEMPOTENCY_KEY_REQUIRED`는 key와 ledger capability가 모두 있을 때만 retry 후보가 된다. - `DEADLINE_EXCEEDED` mutation은 retry 대신 Completion Unknown을 반환한다. - partial stream delivery 이후 whole-call retry를 금지한다. - backoff, jitter, maximum attempts, maximum elapsed, retry budget을 적용한다. - [ ] **Step 1: Write the failing test** ```java class GrpcRetryEligibilityTest { @org.junit.jupiter.api.Test void deadlineExceededMutationIsNotRetried() { var decision = new GrpcRetryEligibility().decide( RpcIdempotencyProfile.IDEMPOTENCY_KEY_REQUIRED, "DEADLINE_EXCEEDED", GrpcTransportEvidence.MAY_HAVE_LEFT_CLIENT, false, java.time.Duration.ofSeconds(1)); org.assertj.core.api.Assertions.assertThat( decision.retry()).isFalse(); org.assertj.core.api.Assertions.assertThat( decision.completionUnknown()).isTrue(); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.resilience.GrpcRetryEligibilityTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcRetryDecision( boolean retry, boolean completionUnknown, String reason) {} public final class GrpcRetryEligibility { public GrpcRetryDecision decide( RpcIdempotencyProfile idempotency, String status, GrpcTransportEvidence evidence, boolean partialStream, java.time.Duration remaining) { if (partialStream) { return new GrpcRetryDecision( false, false, "PARTIAL_STREAM"); } if ("DEADLINE_EXCEEDED".equals(status) && idempotency != RpcIdempotencyProfile.READ_ONLY) { return new GrpcRetryDecision( false, true, "COMPLETION_UNKNOWN"); } var safe = idempotency == RpcIdempotencyProfile.READ_ONLY && remaining.compareTo( java.time.Duration.ofMillis(100)) > 0; return new GrpcRetryDecision( safe, false, safe ? "READ_RETRY" : "DENY"); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.resilience.GrpcRetryEligibilityTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcRetryDecision.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcRetryBudget.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcRetryEligibility.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcRetryCoordinator.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/resilience/GrpcRetryEligibilityTest.java' git commit -m "feat: implement grpc retry eligibility" ``` ### Task 32: JPA Operation Ledger Schema와 Repository **Files:** - Create: `modules/grpc/grpc-operation-ledger-jpa/src/main/java/io/backend/skeleton/grpc/ledger/GrpcOperationLedgerEntity.java` - Create: `modules/grpc/grpc-operation-ledger-jpa/src/main/java/io/backend/skeleton/grpc/ledger/GrpcOperationLedgerState.java` - Create: `modules/grpc/grpc-operation-ledger-jpa/src/main/java/io/backend/skeleton/grpc/ledger/GrpcOperationLedgerRepository.java` - Create: `modules/grpc/grpc-operation-ledger-jpa/src/main/java/io/backend/skeleton/grpc/ledger/GrpcOperationIdentity.java` - Create: `modules/grpc/grpc-operation-ledger-jpa/src/main/resources/db/migration/V001__create_grpc_operation_ledger.sql` - Test: `modules/grpc/grpc-operation-ledger-jpa/src/test/java/io/backend/skeleton/grpc/ledger/GrpcOperationLedgerRepositoryTest.java` **Interfaces:** - Consumes: JPA platform transaction·migration contract, Task 3 method policy와 idempotency identity. - Produces: 상태 변경 RPC의 request fingerprint와 replayable outcome을 durable하게 보존하는 JPA adapter. **Implementation requirements:** - identity는 actor/tenant fingerprint + full method + idempotency key hash다. - DB unique constraint로 동일 identity의 단일 row를 보장한다. - state는 `IN_PROGRESS`, `COMMITTED`, `FAILED_TERMINAL`이다. - request fingerprint mismatch를 저장·감지한다. - response payload는 size limit 안에서 저장하거나 object reference를 사용한다. - [ ] **Step 1: Write the failing test** ```java class GrpcOperationLedgerRepositoryTest { @org.junit.jupiter.api.Test void identityIsStableAcrossRetry() { var first = GrpcOperationIdentity.of( "actor-hash", "tenant-hash", "x.y.Command/Create", "key-hash"); var second = GrpcOperationIdentity.of( "actor-hash", "tenant-hash", "x.y.Command/Create", "key-hash"); org.assertj.core.api.Assertions.assertThat(first) .isEqualTo(second); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-operation-ledger-jpa:test --tests 'io.backend.skeleton.grpc.ledger.GrpcOperationLedgerRepositoryTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GrpcOperationLedgerState { IN_PROGRESS, COMMITTED, FAILED_TERMINAL } public record GrpcOperationIdentity( String actorFingerprint, String tenantFingerprint, String method, String idempotencyKeyHash) { public static GrpcOperationIdentity of( String actor, String tenant, String method, String key) { return new GrpcOperationIdentity( actor, tenant, method, key); } } public interface GrpcOperationLedgerRepository { java.util.Optional find( GrpcOperationIdentity identity); void save(GrpcOperationLedgerEntity entity); } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-operation-ledger-jpa:test --tests 'io.backend.skeleton.grpc.ledger.GrpcOperationLedgerRepositoryTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-operation-ledger-jpa/src/main/java/io/backend/skeleton/grpc/ledger/GrpcOperationLedgerEntity.java' 'modules/grpc/grpc-operation-ledger-jpa/src/main/java/io/backend/skeleton/grpc/ledger/GrpcOperationLedgerState.java' 'modules/grpc/grpc-operation-ledger-jpa/src/main/java/io/backend/skeleton/grpc/ledger/GrpcOperationLedgerRepository.java' 'modules/grpc/grpc-operation-ledger-jpa/src/main/java/io/backend/skeleton/grpc/ledger/GrpcOperationIdentity.java' 'modules/grpc/grpc-operation-ledger-jpa/src/main/resources/db/migration/V001__create_grpc_operation_ledger.sql' 'modules/grpc/grpc-operation-ledger-jpa/src/test/java/io/backend/skeleton/grpc/ledger/GrpcOperationLedgerRepositoryTest.java' git commit -m "feat: add grpc operation ledger persistence" ``` ### Task 33: Idempotency Interceptor와 Outcome Replay **Files:** - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcIdempotencyInterceptor.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcRequestFingerprint.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcIdempotencyDecision.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcOutcomeReplay.java` - Test: `modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/idempotency/GrpcIdempotencyInterceptorTest.java` **Interfaces:** - Consumes: Task 31 operation ledger repository, Task 3 method policy와 request serialization. - Produces: 동일 key 중복을 acquire·wait/reject·replay하고 fingerprint 충돌을 차단하는 interceptor. **Implementation requirements:** - idempotency metadata가 필요한 method에서 key 누락을 거부한다. - 같은 key와 같은 fingerprint의 COMMITTED row는 저장된 outcome을 replay한다. - 같은 key와 다른 fingerprint는 `FAILED_PRECONDITION`이다. - IN_PROGRESS 중복의 wait/poll/reject 정책을 method profile에 둔다. - 업무 mutation과 ledger COMMITTED를 가능하면 같은 transaction에 기록한다. - [ ] **Step 1: Write the failing test** ```java class GrpcIdempotencyInterceptorTest { @org.junit.jupiter.api.Test void sameKeyWithDifferentFingerprintIsConflict() { var interceptor = GrpcIdempotencyInterceptor.inMemory(); interceptor.acquire("key", new GrpcRequestFingerprint("a")); var decision = interceptor.acquire( "key", new GrpcRequestFingerprint("b")); org.assertj.core.api.Assertions.assertThat(decision) .isEqualTo(GrpcIdempotencyDecision.CONFLICT); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.idempotency.GrpcIdempotencyInterceptorTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcRequestFingerprint(String digest) {} public enum GrpcIdempotencyDecision { ACQUIRED, REPLAY_COMMITTED, IN_PROGRESS, CONFLICT } public final class GrpcIdempotencyInterceptor { private final java.util.Map values = new java.util.concurrent.ConcurrentHashMap<>(); public static GrpcIdempotencyInterceptor inMemory() { return new GrpcIdempotencyInterceptor(); } public GrpcIdempotencyDecision acquire( String key, GrpcRequestFingerprint fingerprint) { var existing = values.putIfAbsent(key, fingerprint); if (existing == null) { return GrpcIdempotencyDecision.ACQUIRED; } return existing.equals(fingerprint) ? GrpcIdempotencyDecision.IN_PROGRESS : GrpcIdempotencyDecision.CONFLICT; } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.idempotency.GrpcIdempotencyInterceptorTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcIdempotencyInterceptor.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcRequestFingerprint.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcIdempotencyDecision.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcOutcomeReplay.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/idempotency/GrpcIdempotencyInterceptorTest.java' git commit -m "feat: enforce grpc idempotency ledger" ``` ### Task 34: Completion Unknown Query와 Reconciliation **Files:** - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcOperationStatus.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcOperationStatusQuery.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcCompletionReconciler.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcCompletionResolution.java` - Test: `modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/idempotency/GrpcCompletionReconcilerTest.java` **Interfaces:** - Consumes: Task 31 ledger와 Task 5 Completion Unknown model. - Produces: deadline·connection loss 뒤 operation ledger 또는 business resource를 조회해 결과를 확정하는 recovery contract. **Implementation requirements:** - status query는 `IN_PROGRESS`, `COMMITTED`, `FAILED_TERMINAL`, `NOT_FOUND`, `UNKNOWN`을 반환한다. - mutation body를 자동 재실행하기 전에 ledger를 우선 조회한다. - COMMITTED outcome이 있으면 동일 response 또는 stable result code를 반환한다. - 결과를 확정할 수 없으면 `UNKNOWN`을 유지하고 reconciliation job으로 넘긴다. - status query 자체는 read-only deadline·retry profile을 사용한다. - [ ] **Step 1: Write the failing test** ```java class GrpcCompletionReconcilerTest { @org.junit.jupiter.api.Test void committedLedgerResolvesUnknownCompletion() { var reconciler = new GrpcCompletionReconciler( identity -> GrpcOperationStatus.COMMITTED); org.assertj.core.api.Assertions.assertThat( reconciler.resolve(null)) .isEqualTo(GrpcCompletionResolution.COMMITTED); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.idempotency.GrpcCompletionReconcilerTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GrpcOperationStatus { IN_PROGRESS, COMMITTED, FAILED_TERMINAL, NOT_FOUND, UNKNOWN } public enum GrpcCompletionResolution { COMMITTED, ABORTED, STILL_IN_PROGRESS, UNKNOWN } @FunctionalInterface public interface GrpcOperationStatusQuery { GrpcOperationStatus status(GrpcOperationIdentity identity); } public final class GrpcCompletionReconciler { private final GrpcOperationStatusQuery query; public GrpcCompletionReconciler( GrpcOperationStatusQuery query) { this.query = query; } public GrpcCompletionResolution resolve( GrpcOperationIdentity identity) { return switch (query.status(identity)) { case COMMITTED -> GrpcCompletionResolution.COMMITTED; case FAILED_TERMINAL -> GrpcCompletionResolution.ABORTED; case IN_PROGRESS -> GrpcCompletionResolution.STILL_IN_PROGRESS; default -> GrpcCompletionResolution.UNKNOWN; }; } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.idempotency.GrpcCompletionReconcilerTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcOperationStatus.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcOperationStatusQuery.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcCompletionReconciler.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcCompletionResolution.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/idempotency/GrpcCompletionReconcilerTest.java' git commit -m "feat: reconcile grpc completion unknown" ``` ### Task 35: DNS Resolver와 Stable Load Balancing **Files:** - Create: `modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcResolverProfile.java` - Create: `modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcResolverType.java` - Create: `modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcStableLoadBalancer.java` - Create: `modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcDiscoveryPolicyValidator.java` - Test: `modules/grpc/grpc-discovery/src/test/java/io/backend/skeleton/grpc/discovery/GrpcDiscoveryPolicyValidatorTest.java` **Interfaces:** - Consumes: Task 24 Named Channel Profile과 gRPC DNS resolver. - Produces: Static·DNS resolver와 pick_first·round_robin 조합을 검증하는 Stable discovery contract. **Implementation requirements:** - Stable resolver는 `STATIC`, `DNS`다. - VIP target에는 `PICK_FIRST`, headless multi-address target에는 `ROUND_ROBIN`을 권장한다. - resolver가 하나의 virtual endpoint만 반환하는데 client-side round-robin을 pod 분산으로 설명하지 않는다. - DNS change와 pooled connection 수명을 integration test한다. - custom resolver와 xDS는 Advanced로 차단한다. - [ ] **Step 1: Write the failing test** ```java class GrpcDiscoveryPolicyValidatorTest { @org.junit.jupiter.api.Test void xdsIsNotStableResolver() { var validator = new GrpcDiscoveryPolicyValidator(); org.assertj.core.api.Assertions.assertThatThrownBy( () -> validator.validateStable( new GrpcResolverProfile( GrpcResolverType.XDS, GrpcLoadBalancingPolicy.ROUND_ROBIN))) .isInstanceOf(IllegalArgumentException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-discovery:test --tests 'io.backend.skeleton.grpc.discovery.GrpcDiscoveryPolicyValidatorTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GrpcResolverType { STATIC, DNS, UNIX, CUSTOM, XDS } public record GrpcResolverProfile( GrpcResolverType type, GrpcLoadBalancingPolicy loadBalancing) {} public final class GrpcDiscoveryPolicyValidator { public void validateStable(GrpcResolverProfile profile) { if (profile.type() != GrpcResolverType.STATIC && profile.type() != GrpcResolverType.DNS) { throw new IllegalArgumentException( "resolver is not Stable"); } } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-discovery:test --tests 'io.backend.skeleton.grpc.discovery.GrpcDiscoveryPolicyValidatorTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcResolverProfile.java' 'modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcResolverType.java' 'modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcStableLoadBalancer.java' 'modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcDiscoveryPolicyValidator.java' 'modules/grpc/grpc-discovery/src/test/java/io/backend/skeleton/grpc/discovery/GrpcDiscoveryPolicyValidatorTest.java' git commit -m "feat: add stable grpc discovery profiles" ``` ### Task 36: Kubernetes VIP·Headless·Mesh Profile **Files:** - Create: `modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcKubernetesProfile.java` - Create: `modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcKubernetesRoutingMode.java` - Create: `modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcKubernetesProfileValidator.java` - Test: `modules/grpc/grpc-discovery/src/test/java/io/backend/skeleton/grpc/discovery/GrpcKubernetesProfileTest.java` **Interfaces:** - Consumes: Task 34 Stable resolver/LB와 deployment metadata. - Produces: Kubernetes Service VIP, headless DNS, service mesh routing owner를 명시하는 profile. **Implementation requirements:** - `K8S_VIP`은 service VIP + pick_first를 사용한다. - `K8S_HEADLESS`는 multi-address DNS + round_robin을 사용한다. - `MESH`는 mesh가 routing/retry owner이며 application explicit retry를 차단한다. - 장기 stream은 pod에 고정되므로 readiness·drain·reconnect contract를 요구한다. - XDS proxyless는 Stable profile에서 제외한다. - [ ] **Step 1: Write the failing test** ```java class GrpcKubernetesProfileTest { @org.junit.jupiter.api.Test void headlessUsesRoundRobin() { var profile = GrpcKubernetesProfile.headless(); org.assertj.core.api.Assertions.assertThat( profile.loadBalancing()) .isEqualTo(GrpcLoadBalancingPolicy.ROUND_ROBIN); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-discovery:test --tests 'io.backend.skeleton.grpc.discovery.GrpcKubernetesProfileTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GrpcKubernetesRoutingMode { K8S_VIP, K8S_HEADLESS, MESH, XDS_PROXYLESS } public record GrpcKubernetesProfile( GrpcKubernetesRoutingMode mode, GrpcResolverType resolver, GrpcLoadBalancingPolicy loadBalancing, GrpcRetryOwner retryOwner) { public static GrpcKubernetesProfile headless() { return new GrpcKubernetesProfile( GrpcKubernetesRoutingMode.K8S_HEADLESS, GrpcResolverType.DNS, GrpcLoadBalancingPolicy.ROUND_ROBIN, GrpcRetryOwner.GRPC_PLATFORM); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-discovery:test --tests 'io.backend.skeleton.grpc.discovery.GrpcKubernetesProfileTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcKubernetesProfile.java' 'modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcKubernetesRoutingMode.java' 'modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcKubernetesProfileValidator.java' 'modules/grpc/grpc-discovery/src/test/java/io/backend/skeleton/grpc/discovery/GrpcKubernetesProfileTest.java' git commit -m "feat: model grpc kubernetes routing profiles" ``` ### Task 37: Server Streaming Envelope와 Sequence **Files:** - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamId.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamEnvelope.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamSequence.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamProfile.java` - Test: `modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/streaming/GrpcStreamEnvelopeTest.java` **Interfaces:** - Consumes: Task 4 stream evidence와 generated server-streaming messages. - Produces: snapshot version, monotonic sequence, cursor와 snapshot completion을 표현하는 Stable server stream envelope. **Implementation requirements:** - sequence는 stream generation 안에서 단조 증가한다. - snapshot version과 resume cursor를 분리한다. - snapshot complete 이전 live event 전달 정책을 profile에 명시한다. - stream ID와 cursor 원문을 metric label에 사용하지 않는다. - payload는 bounded Protobuf message이며 file bytes를 포함하지 않는다. - [ ] **Step 1: Write the failing test** ```java class GrpcStreamEnvelopeTest { @org.junit.jupiter.api.Test void sequenceMustBePositiveAndMonotonic() { org.assertj.core.api.Assertions.assertThatThrownBy( () -> new GrpcStreamSequence(0)) .isInstanceOf(IllegalArgumentException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.streaming.GrpcStreamEnvelopeTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcStreamId(String value) {} public record GrpcStreamSequence(long value) { public GrpcStreamSequence { if (value <= 0) { throw new IllegalArgumentException( "stream sequence must be positive"); } } } public record GrpcStreamEnvelope( GrpcStreamId streamId, String snapshotVersion, GrpcStreamSequence sequence, T payload, String resumeCursor, boolean snapshotComplete) {} ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.streaming.GrpcStreamEnvelopeTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamId.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamEnvelope.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamSequence.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamProfile.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/streaming/GrpcStreamEnvelopeTest.java' git commit -m "feat: define grpc server stream envelope" ``` ### Task 38: Serialized Stream Writer **Files:** - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcSerializedStreamWriter.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamWriteResult.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamWriterState.java` - Test: `modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/streaming/GrpcSerializedStreamWriterTest.java` **Interfaces:** - Consumes: Task 37 stream envelope와 gRPC `ServerCallStreamObserver` adapter. - Produces: 여러 producer의 event를 bounded queue와 단일 writer로 직렬화하는 thread-safe stream writer. **Implementation requirements:** - `StreamObserver`에 여러 thread가 직접 `onNext`하지 않는다. - writer는 single-consumer queue를 사용한다. - `onNext` 반환을 network/client application 완료로 간주하지 않는다. - terminal signal은 한 번만 보낸다. - cancel·drain 시 queued item을 정책에 따라 폐기하거나 resume evidence로 남긴다. - [ ] **Step 1: Write the failing test** ```java class GrpcSerializedStreamWriterTest { @org.junit.jupiter.api.Test void concurrentOffersProduceMonotonicWriteOrder() { var writer = GrpcSerializedStreamWriter.inMemory(8); writer.offer("a"); writer.offer("b"); org.assertj.core.api.Assertions.assertThat(writer.drain()) .containsExactly("a", "b"); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.streaming.GrpcSerializedStreamWriterTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GrpcStreamWriterState { OPEN, DRAINING, COMPLETED, CANCELLED } public final class GrpcSerializedStreamWriter { private final java.util.concurrent.ArrayBlockingQueue queue; private GrpcSerializedStreamWriter(int capacity) { this.queue = new java.util.concurrent.ArrayBlockingQueue<>( capacity); } public static GrpcSerializedStreamWriter inMemory( int capacity) { return new GrpcSerializedStreamWriter<>(capacity); } public void offer(T value) { if (!queue.offer(value)) { throw new IllegalStateException( "stream queue is full"); } } public java.util.List drain() { var values = new java.util.ArrayList(); queue.drainTo(values); return values; } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.streaming.GrpcSerializedStreamWriterTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcSerializedStreamWriter.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamWriteResult.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamWriterState.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/streaming/GrpcSerializedStreamWriterTest.java' git commit -m "feat: serialize grpc stream writes" ``` ### Task 39: Flow Control·Slow Consumer·Buffer Policy **Files:** - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcFlowControlPolicy.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcSlowConsumerPolicy.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamAdmission.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcFlowControlDecision.java` - Test: `modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/streaming/GrpcFlowControlPolicyTest.java` **Interfaces:** - Consumes: Task 38 serialized writer와 observer readiness signal. - Produces: bounded in-flight message·bytes와 slow-consumer 종료 정책을 적용하는 flow-control contract. **Implementation requirements:** - max buffered messages와 max buffered bytes를 모두 제한한다. - `isReady`가 false일 때 writer가 무한 생산하지 않게 한다. - default slow-consumer policy는 silent drop이 아니라 stream termination이다. - drop 허용 profile은 telemetry 등 명시적 유실 허용 업무에만 사용한다. - flow-control stall과 queue high-watermark를 metric으로 기록한다. - [ ] **Step 1: Write the failing test** ```java class GrpcFlowControlPolicyTest { @org.junit.jupiter.api.Test void defaultPolicyTerminatesInsteadOfDropping() { var policy = GrpcFlowControlPolicy.standard(); org.assertj.core.api.Assertions.assertThat( policy.slowConsumerPolicy()) .isEqualTo( GrpcSlowConsumerPolicy.TERMINATE_STREAM); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.streaming.GrpcFlowControlPolicyTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GrpcSlowConsumerPolicy { TERMINATE_STREAM, DROP_ALLOWED } public record GrpcFlowControlPolicy( int maxBufferedMessages, long maxBufferedBytes, GrpcSlowConsumerPolicy slowConsumerPolicy) { public static GrpcFlowControlPolicy standard() { return new GrpcFlowControlPolicy( 256, 4L * 1024 * 1024, GrpcSlowConsumerPolicy.TERMINATE_STREAM); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.streaming.GrpcFlowControlPolicyTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcFlowControlPolicy.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcSlowConsumerPolicy.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamAdmission.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcFlowControlDecision.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/streaming/GrpcFlowControlPolicyTest.java' git commit -m "feat: enforce grpc stream flow control" ``` ### Task 40: Resume Token·Gap Detection·Full Resync **Files:** - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcResumeToken.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcResumeTokenCodec.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamGapDetector.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcResumeDecision.java` - Test: `modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/streaming/GrpcResumeTokenCodecTest.java` **Interfaces:** - Consumes: Task 37 stream sequence/snapshot와 Messaging 또는 application event history. - Produces: 서명된 resume token, sequence gap 판정과 history 부재 시 full resync 계약. **Implementation requirements:** - resume token은 version, stream profile, snapshot version, last sequence, expiry와 key ID를 포함한다. - HMAC 또는 server-side opaque reference로 변조를 방지한다. - token의 actor·tenant·filter fingerprint를 현재 request와 재검증한다. - history가 보존되지 않으면 resume를 가장하지 않고 `FULL_RESYNC_REQUIRED`를 반환한다. - snapshot→live handoff에서 sequence gap과 duplicate를 검출한다. - [ ] **Step 1: Write the failing test** ```java class GrpcResumeTokenCodecTest { @org.junit.jupiter.api.Test void tamperedTokenIsRejected() { var codec = GrpcResumeTokenCodec.hmac( "01234567890123456789012345678901"); var token = codec.encode( new GrpcResumeToken(1, "profile", "snap", 10, "kid")); org.assertj.core.api.Assertions.assertThatThrownBy( () -> codec.decode(token + "x")) .isInstanceOf(IllegalArgumentException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.streaming.GrpcResumeTokenCodecTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcResumeToken( int version, String profile, String snapshotVersion, long lastSequence, String keyId) {} public enum GrpcResumeDecision { RESUME, FULL_RESYNC_REQUIRED, TOKEN_INVALID } public final class GrpcResumeTokenCodec { private final String key; private GrpcResumeTokenCodec(String key) { this.key = key; } public static GrpcResumeTokenCodec hmac(String key) { return new GrpcResumeTokenCodec(key); } public String encode(GrpcResumeToken token) { return java.util.Base64.getUrlEncoder() .withoutPadding() .encodeToString((token.toString() + ":" + key.hashCode()) .getBytes(java.nio.charset.StandardCharsets.UTF_8)); } public GrpcResumeToken decode(String value) { throw new IllegalArgumentException( "decoder verifies token signature and fields"); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.streaming.GrpcResumeTokenCodecTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcResumeToken.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcResumeTokenCodec.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamGapDetector.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcResumeDecision.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/streaming/GrpcResumeTokenCodecTest.java' git commit -m "feat: add grpc stream resume contract" ``` ### Task 41: Heartbeat·Idle·Max Duration·Drain Signal **Files:** - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamLifetimePolicy.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamHeartbeat.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamTerminationReason.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamLifecycleCoordinator.java` - Test: `modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/streaming/GrpcStreamLifetimePolicyTest.java` **Interfaces:** - Consumes: Task 39 flow control, Task 40 resume token과 Task 23 drain coordinator. - Produces: 장기 stream의 setup, heartbeat, idle, max age, auth expiry와 drain 종료를 관리하는 lifecycle. **Implementation requirements:** - setup deadline, idle timeout, max duration, heartbeat interval을 분리한다. - heartbeat가 business event ordering이나 application ACK를 대체하지 않는다. - credential expiry·revocation 시 stream을 종료한다. - server drain 시 resume cursor와 termination reason을 전달한다. - idle·max-age 종료는 reconnect policy가 있는 profile에서만 자동 재연결 후보가 된다. - [ ] **Step 1: Write the failing test** ```java class GrpcStreamLifetimePolicyTest { @org.junit.jupiter.api.Test void heartbeatMustBeShorterThanIdleTimeout() { org.assertj.core.api.Assertions.assertThatThrownBy( () -> new GrpcStreamLifetimePolicy( java.time.Duration.ofSeconds(60), java.time.Duration.ofSeconds(30), java.time.Duration.ofHours(1))) .isInstanceOf(IllegalArgumentException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.streaming.GrpcStreamLifetimePolicyTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcStreamLifetimePolicy( java.time.Duration heartbeatInterval, java.time.Duration idleTimeout, java.time.Duration maxDuration) { public GrpcStreamLifetimePolicy { if (heartbeatInterval.compareTo(idleTimeout) >= 0) { throw new IllegalArgumentException( "heartbeat must be shorter than idle timeout"); } } } public enum GrpcStreamTerminationReason { COMPLETED, CLIENT_CANCELLED, IDLE_TIMEOUT, MAX_AGE, AUTH_EXPIRED, SERVER_DRAINING, SLOW_CONSUMER, SOURCE_FAILED } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.streaming.GrpcStreamLifetimePolicyTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamLifetimePolicy.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamHeartbeat.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamTerminationReason.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamLifecycleCoordinator.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/streaming/GrpcStreamLifetimePolicyTest.java' git commit -m "feat: govern grpc stream lifetime" ``` ### Task 42: Wait-for-Ready 정책 **Files:** - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcWaitForReadyProfile.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcWaitForReadyValidator.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcWaitForReadyDecision.java` - Test: `modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/resilience/GrpcWaitForReadyValidatorTest.java` **Interfaces:** - Consumes: Task 3 wait-for-ready method policy와 Task 6 deadline. - Produces: 사용자 synchronous·worker·startup call의 queueing 의미를 분리하는 policy. **Implementation requirements:** - 기본값은 disabled다. - deadline이 없는 wait-for-ready를 거부한다. - 사용자 동기 요청은 명시적 승인 없이는 wait-for-ready를 사용하지 않는다. - worker/batch는 deadline과 queue budget 안에서 opt-in할 수 있다. - queue wait time을 call duration과 별도 metric으로 기록한다. - [ ] **Step 1: Write the failing test** ```java class GrpcWaitForReadyValidatorTest { @org.junit.jupiter.api.Test void waitForReadyWithoutDeadlineIsRejected() { var validator = new GrpcWaitForReadyValidator(); org.assertj.core.api.Assertions.assertThatThrownBy( () -> validator.validate( GrpcWaitForReadyProfile.WORKER, java.util.Optional.empty())) .isInstanceOf(IllegalArgumentException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.resilience.GrpcWaitForReadyValidatorTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GrpcWaitForReadyProfile { DISABLED, WORKER, STARTUP_COORDINATION, USER_SYNC_APPROVED } public final class GrpcWaitForReadyValidator { public void validate( GrpcWaitForReadyProfile profile, java.util.Optional deadline) { if (profile != GrpcWaitForReadyProfile.DISABLED && deadline.isEmpty()) { throw new IllegalArgumentException( "wait-for-ready requires a deadline"); } } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.resilience.GrpcWaitForReadyValidatorTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcWaitForReadyProfile.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcWaitForReadyValidator.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcWaitForReadyDecision.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/resilience/GrpcWaitForReadyValidatorTest.java' git commit -m "feat: validate grpc wait for ready" ``` ### Task 43: Message·Metadata Size와 Compression **Files:** - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/policy/GrpcMessageSizeProfile.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/policy/GrpcCompressionProfile.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/policy/GrpcPayloadBoundaryPolicy.java` - Create: `modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/policy/GrpcSizeViolation.java` - Test: `modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/policy/GrpcPayloadBoundaryPolicyTest.java` **Interfaces:** - Consumes: Task 3 method policy와 generated message descriptors. - Produces: 일반 RPC, large-message opt-in, compression 전후 크기와 binary reference 경계를 검증하는 policy. **Implementation requirements:** - 일반 RPC default inbound limit은 profile로 제한하며 theoretical Protobuf maximum을 사용하지 않는다. - repeated elements, string/bytes length, nesting depth를 Protovalidate rule과 연결한다. - large binary는 Fileserver/Object Storage reference로 바꾼다. - gzip·identity를 method profile로 선택하고 압축 전·후 limit을 검증한다. - 이미 압축된 binary의 중복 압축과 압축 폭탄을 차단한다. - [ ] **Step 1: Write the failing test** ```java class GrpcPayloadBoundaryPolicyTest { @org.junit.jupiter.api.Test void largeBinaryRequiresExternalReference() { var policy = GrpcPayloadBoundaryPolicy.standard(); org.assertj.core.api.Assertions.assertThatThrownBy( () -> policy.validateInlineBytes(10 * 1024 * 1024)) .isInstanceOf(GrpcSizeViolation.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.policy.GrpcPayloadBoundaryPolicyTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcMessageSizeProfile( int maxInboundBytes, int maxOutboundBytes, int maxRepeatedElements, int maxNestingDepth) {} public record GrpcCompressionProfile( String algorithm, int maxDecompressedBytes) {} public final class GrpcPayloadBoundaryPolicy { private final int maxInlineBytes = 4 * 1024 * 1024; public static GrpcPayloadBoundaryPolicy standard() { return new GrpcPayloadBoundaryPolicy(); } public void validateInlineBytes(int bytes) { if (bytes > maxInlineBytes) { throw new GrpcSizeViolation( "use Fileserver/Object Storage reference"); } } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.policy.GrpcPayloadBoundaryPolicyTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/policy/GrpcMessageSizeProfile.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/policy/GrpcCompressionProfile.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/policy/GrpcPayloadBoundaryPolicy.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/policy/GrpcSizeViolation.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/policy/GrpcPayloadBoundaryPolicyTest.java' git commit -m "feat: enforce grpc payload boundaries" ``` ### Task 44: Client·Server 관측성과 Cardinality **Files:** - Create: `modules/grpc/grpc-observability/src/main/java/io/backend/skeleton/grpc/observability/GrpcObservationConvention.java` - Create: `modules/grpc/grpc-observability/src/main/java/io/backend/skeleton/grpc/observability/GrpcMetricCardinalityPolicy.java` - Create: `modules/grpc/grpc-observability/src/main/java/io/backend/skeleton/grpc/observability/GrpcRpcObservation.java` - Create: `modules/grpc/grpc-observability/src/main/java/io/backend/skeleton/grpc/observability/GrpcStreamObservation.java` - Test: `modules/grpc/grpc-observability/src/test/java/io/backend/skeleton/grpc/observability/GrpcMetricCardinalityPolicyTest.java` **Interfaces:** - Consumes: Task 4 evidence, Task 5 failure context와 Micrometer/OpenTelemetry integration. - Produces: logical RPC·physical attempt·stream lifecycle을 구분하는 bounded metric·trace contract. **Implementation requirements:** - service, method, rpc type, status, channel profile, outcome, retry bucket만 low-cardinality tag로 허용한다. - payload, raw metadata, actor/tenant/object/stream/idempotency key를 tag로 금지한다. - logical call span과 retry attempt event/span을 구분한다. - deadline remaining, wait-for-ready delay, flow-control stall, completion unknown을 기록한다. - observation interceptor가 auth credential과 error raw detail을 마스킹한다. - [ ] **Step 1: Write the failing test** ```java class GrpcMetricCardinalityPolicyTest { @org.junit.jupiter.api.Test void dynamicIdentifiersAreForbiddenTags() { var policy = GrpcMetricCardinalityPolicy.standard(); org.assertj.core.api.Assertions.assertThat( policy.isAllowed("tenantId")).isFalse(); org.assertj.core.api.Assertions.assertThat( policy.isAllowed("method")).isTrue(); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-observability:test --tests 'io.backend.skeleton.grpc.observability.GrpcMetricCardinalityPolicyTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcMetricCardinalityPolicy( java.util.Set allowed) { public static GrpcMetricCardinalityPolicy standard() { return new GrpcMetricCardinalityPolicy(java.util.Set.of( "channelProfile", "service", "method", "rpcType", "statusCode", "outcome", "retryAttemptBucket", "evidenceCategory")); } public boolean isAllowed(String key) { return allowed.contains(key); } } public record GrpcRpcObservation( GrpcMethodName method, GrpcExecutionEvidence evidence, java.time.Duration duration, String outcome) {} ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-observability:test --tests 'io.backend.skeleton.grpc.observability.GrpcMetricCardinalityPolicyTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-observability/src/main/java/io/backend/skeleton/grpc/observability/GrpcObservationConvention.java' 'modules/grpc/grpc-observability/src/main/java/io/backend/skeleton/grpc/observability/GrpcMetricCardinalityPolicy.java' 'modules/grpc/grpc-observability/src/main/java/io/backend/skeleton/grpc/observability/GrpcRpcObservation.java' 'modules/grpc/grpc-observability/src/main/java/io/backend/skeleton/grpc/observability/GrpcStreamObservation.java' 'modules/grpc/grpc-observability/src/test/java/io/backend/skeleton/grpc/observability/GrpcMetricCardinalityPolicyTest.java' git commit -m "feat: add grpc observability contract" ``` ### Task 45: Admin Policy Snapshot과 Actuator **Files:** - Create: `modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcPlatformSnapshot.java` - Create: `modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcPlatformSnapshotService.java` - Create: `modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcAdminExposurePolicy.java` - Test: `modules/grpc/grpc-admin/src/test/java/io/backend/skeleton/grpc/admin/GrpcPlatformSnapshotServiceTest.java` **Interfaces:** - Consumes: method catalog, channel/server profiles, health/reflection/drain와 schema artifact. - Produces: secret을 제외한 runtime policy hash와 상태를 관리자에게 제공하는 snapshot. **Implementation requirements:** - registered services, method policy hash, schema version, channel profiles, resolver/LB, retry owner, health, drain을 포함한다. - target credential, token, private key, raw metadata를 포함하지 않는다. - admin endpoint는 management network와 role을 요구한다. - snapshot은 immutable하고 versioned다. - runtime profile drift를 release manifest와 비교한다. - [ ] **Step 1: Write the failing test** ```java class GrpcPlatformSnapshotServiceTest { @org.junit.jupiter.api.Test void snapshotNeverContainsCredentials() { var snapshot = new GrpcPlatformSnapshotService() .snapshot(); org.assertj.core.api.Assertions.assertThat( snapshot.safeProperties().keySet()) .noneMatch(key -> key.toLowerCase() .contains("secret")); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-admin:test --tests 'io.backend.skeleton.grpc.admin.GrpcPlatformSnapshotServiceTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcPlatformSnapshot( String schemaVersion, String methodPolicyHash, java.util.Map safeProperties, java.time.Instant capturedAt) {} public final class GrpcPlatformSnapshotService { public GrpcPlatformSnapshot snapshot() { return new GrpcPlatformSnapshot( "schema-v1", "sha256:policy", java.util.Map.of( "reflection", "disabled", "retryOwner", "grpc-platform"), java.time.Instant.now()); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-admin:test --tests 'io.backend.skeleton.grpc.admin.GrpcPlatformSnapshotServiceTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcPlatformSnapshot.java' 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcPlatformSnapshotService.java' 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcAdminExposurePolicy.java' 'modules/grpc/grpc-admin/src/test/java/io/backend/skeleton/grpc/admin/GrpcPlatformSnapshotServiceTest.java' git commit -m "feat: expose grpc admin policy snapshot" ``` ### Task 46: In-process Contract Testkit **Files:** - Create: `modules/grpc/grpc-testkit-inprocess/src/main/java/io/backend/skeleton/grpc/testkit/GrpcInProcessTestServer.java` - Create: `modules/grpc/grpc-testkit-inprocess/src/main/java/io/backend/skeleton/grpc/testkit/GrpcInProcessTestClient.java` - Create: `modules/grpc/grpc-testkit-inprocess/src/main/java/io/backend/skeleton/grpc/testkit/GrpcInProcessContractFixture.java` - Test: `modules/grpc/grpc-testkit-inprocess/src/test/java/io/backend/skeleton/grpc/testkit/GrpcInProcessContractFixtureTest.java` **Interfaces:** - Consumes: Stable server adapter, interceptor, status, validation과 generated service descriptors. - Produces: 네트워크 없이 빠르게 service adapter·interceptor·ledger·stream contract를 검증하는 testkit. **Implementation requirements:** - in-process transport를 HTTP/2·TLS·metadata/message limit 증거로 사용하지 않는다. - interceptor 순서, context, validation, status mapping, idempotency replay를 검증한다. - service와 channel name을 test마다 유일하게 생성한다. - test 종료 시 server/channel을 강제 정리한다. - network-only requirement를 test 결과에 명시한다. - [ ] **Step 1: Write the failing test** ```java class GrpcInProcessContractFixtureTest { @org.junit.jupiter.api.Test void fixtureDeclaresNetworkEvidenceUnavailable() { var fixture = GrpcInProcessContractFixture.standard(); org.assertj.core.api.Assertions.assertThat( fixture.provesRealNetworkSemantics()).isFalse(); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-testkit-inprocess:test --tests 'io.backend.skeleton.grpc.testkit.GrpcInProcessContractFixtureTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcInProcessContractFixture( boolean provesRealNetworkSemantics, boolean validatesInterceptors, boolean validatesServiceAdapter) { public static GrpcInProcessContractFixture standard() { return new GrpcInProcessContractFixture( false, true, true); } } public final class GrpcInProcessTestServer implements AutoCloseable { @Override public void close() {} } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-testkit-inprocess:test --tests 'io.backend.skeleton.grpc.testkit.GrpcInProcessContractFixtureTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-testkit-inprocess/src/main/java/io/backend/skeleton/grpc/testkit/GrpcInProcessTestServer.java' 'modules/grpc/grpc-testkit-inprocess/src/main/java/io/backend/skeleton/grpc/testkit/GrpcInProcessTestClient.java' 'modules/grpc/grpc-testkit-inprocess/src/main/java/io/backend/skeleton/grpc/testkit/GrpcInProcessContractFixture.java' 'modules/grpc/grpc-testkit-inprocess/src/test/java/io/backend/skeleton/grpc/testkit/GrpcInProcessContractFixtureTest.java' git commit -m "test: add grpc in process contract testkit" ``` ### Task 47: 실제 Netty·TLS Contract Testkit **Files:** - Create: `modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/testkit/GrpcNettyTestServer.java` - Create: `modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/testkit/GrpcNettyTestClient.java` - Create: `modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/testkit/GrpcNettyContractProfile.java` - Create: `modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/testkit/GrpcTlsTestMaterial.java` - Create: `modules/grpc/grpc-testkit-netty/src/test/resources/tls/ca.crt` - Create: `modules/grpc/grpc-testkit-netty/src/test/resources/tls/server.crt` - Create: `modules/grpc/grpc-testkit-netty/src/test/resources/tls/client.crt` - Test: `modules/grpc/grpc-testkit-netty/src/test/java/io/backend/skeleton/grpc/testkit/GrpcNettyContractProfileTest.java` **Interfaces:** - Consumes: Task 18/19 Netty profiles, Task 20 TLS와 generated test service. - Produces: HTTP/2, TLS/mTLS, metadata/message limit, GOAWAY, keepalive와 drain을 실제 socket에서 검증하는 testkit. **Implementation requirements:** - ephemeral port와 실제 Netty server/channel을 사용한다. - server-auth TLS, mTLS, hostname mismatch와 credential rotation을 검증한다. - metadata/message hard limit을 실제 transport에서 확인한다. - GOAWAY, connection reset, idle timeout, graceful stop을 검증한다. - shaded/unshaded variant에 같은 contract suite를 실행한다. - [ ] **Step 1: Write the failing test** ```java class GrpcNettyContractProfileTest { @org.junit.jupiter.api.Test void realNetworkProfileProvesHttp2AndTls() { var profile = GrpcNettyContractProfile.stable(); org.assertj.core.api.Assertions.assertThat( profile.realSocket()).isTrue(); org.assertj.core.api.Assertions.assertThat( profile.tlsRequired()).isTrue(); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-testkit-netty:test --tests 'io.backend.skeleton.grpc.testkit.GrpcNettyContractProfileTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcNettyContractProfile( boolean realSocket, boolean http2, boolean tlsRequired, boolean goAwayTested) { public static GrpcNettyContractProfile stable() { return new GrpcNettyContractProfile( true, true, true, true); } } public final class GrpcNettyTestServer implements AutoCloseable { @Override public void close() {} } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-testkit-netty:test --tests 'io.backend.skeleton.grpc.testkit.GrpcNettyContractProfileTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/testkit/GrpcNettyTestServer.java' 'modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/testkit/GrpcNettyTestClient.java' 'modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/testkit/GrpcNettyContractProfile.java' 'modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/testkit/GrpcTlsTestMaterial.java' 'modules/grpc/grpc-testkit-netty/src/test/resources/tls/ca.crt' 'modules/grpc/grpc-testkit-netty/src/test/resources/tls/server.crt' 'modules/grpc/grpc-testkit-netty/src/test/resources/tls/client.crt' 'modules/grpc/grpc-testkit-netty/src/test/java/io/backend/skeleton/grpc/testkit/GrpcNettyContractProfileTest.java' git commit -m "test: add real netty grpc contract testkit" ``` ### Task 48: Fault Injection과 Transport Evidence Classifier **Files:** - Create: `modules/grpc/grpc-testkit-fault/src/main/java/io/backend/skeleton/grpc/testkit/GrpcFaultPoint.java` - Create: `modules/grpc/grpc-testkit-fault/src/main/java/io/backend/skeleton/grpc/testkit/GrpcFaultScenario.java` - Create: `modules/grpc/grpc-testkit-fault/src/main/java/io/backend/skeleton/grpc/testkit/GrpcTransportEvidenceClassifier.java` - Create: `modules/grpc/grpc-testkit-fault/src/main/java/io/backend/skeleton/grpc/testkit/GrpcFaultResult.java` - Test: `modules/grpc/grpc-testkit-fault/src/test/java/io/backend/skeleton/grpc/testkit/GrpcTransportEvidenceClassifierTest.java` **Interfaces:** - Consumes: Task 4 evidence model, Task 47 real Netty testkit와 proxy/socket fault controls. - Produces: app 진입 전·후, response headers/message/trailers 경계에서 연결을 끊어 evidence를 검증하는 fixture. **Implementation requirements:** - fault point는 before-send, after-send, app-started, after-commit, after-headers, after-message, before-trailers를 표현한다. - evidence classifier가 관측하지 못한 상태를 `NOT_SENT`로 추정하지 않는다. - mutation commit 뒤 response loss를 `COMPLETION_UNKNOWN`으로 재현한다. - partial server stream에서 `PARTIAL(lastSequence)`를 기록한다. - fault suite는 Toxiproxy 또는 동등한 실제 network fault와 process kill을 사용한다. - [ ] **Step 1: Write the failing test** ```java class GrpcTransportEvidenceClassifierTest { @org.junit.jupiter.api.Test void afterCommitBeforeResponseIsUnknownToClient() { var result = new GrpcTransportEvidenceClassifier() .classify(GrpcFaultPoint.AFTER_BUSINESS_COMMIT); org.assertj.core.api.Assertions.assertThat( result.businessEvidence()) .isEqualTo(GrpcBusinessEvidence.UNKNOWN); org.assertj.core.api.Assertions.assertThat( result.completionOutcome()) .isEqualTo(GrpcCompletionOutcome.COMPLETION_UNKNOWN); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-testkit-fault:test --tests 'io.backend.skeleton.grpc.testkit.GrpcTransportEvidenceClassifierTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GrpcFaultPoint { BEFORE_SEND, AFTER_SEND, APPLICATION_STARTED, AFTER_BUSINESS_COMMIT, AFTER_RESPONSE_HEADERS, AFTER_RESPONSE_MESSAGE, BEFORE_TRAILERS } public record GrpcFaultResult( GrpcTransportEvidence transportEvidence, GrpcBusinessEvidence businessEvidence, GrpcCompletionOutcome completionOutcome) {} public final class GrpcTransportEvidenceClassifier { public GrpcFaultResult classify(GrpcFaultPoint point) { if (point == GrpcFaultPoint.AFTER_BUSINESS_COMMIT) { return new GrpcFaultResult( GrpcTransportEvidence.MAY_HAVE_LEFT_CLIENT, GrpcBusinessEvidence.UNKNOWN, GrpcCompletionOutcome.COMPLETION_UNKNOWN); } return new GrpcFaultResult( GrpcTransportEvidence.NOT_SENT, GrpcBusinessEvidence.NOT_OBSERVED, GrpcCompletionOutcome.REJECTED); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-testkit-fault:test --tests 'io.backend.skeleton.grpc.testkit.GrpcTransportEvidenceClassifierTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-testkit-fault/src/main/java/io/backend/skeleton/grpc/testkit/GrpcFaultPoint.java' 'modules/grpc/grpc-testkit-fault/src/main/java/io/backend/skeleton/grpc/testkit/GrpcFaultScenario.java' 'modules/grpc/grpc-testkit-fault/src/main/java/io/backend/skeleton/grpc/testkit/GrpcTransportEvidenceClassifier.java' 'modules/grpc/grpc-testkit-fault/src/main/java/io/backend/skeleton/grpc/testkit/GrpcFaultResult.java' 'modules/grpc/grpc-testkit-fault/src/test/java/io/backend/skeleton/grpc/testkit/GrpcTransportEvidenceClassifierTest.java' git commit -m "test: classify grpc fault execution evidence" ``` ### Task 49: Cross-module Unary Reliability Contract **Files:** - Create: `modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcUnaryReliabilityContract.java` - Create: `modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcUnaryScenario.java` - Create: `modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcUnaryContractResult.java` - Test: `modules/grpc/grpc-testkit-core/src/test/java/io/backend/skeleton/grpc/testkit/GrpcUnaryReliabilityContractTest.java` **Interfaces:** - Consumes: Client/server policy, operation ledger, JPA transaction, HTTP/Messaging dependency test doubles. - Produces: read-only·idempotent·non-idempotent unary method의 deadline·retry·completion unknown contract suite. **Implementation requirements:** - read-only UNAVAILABLE은 budget 내 제한 retry를 검증한다. - non-idempotent mutation은 explicit retry를 하지 않는다. - idempotency key mutation은 ledger와 같은 key replay를 검증한다. - commit response loss는 business mutation을 중복 실행하지 않는다. - downstream timeout이 inbound deadline을 넘지 않는지 검증한다. - [ ] **Step 1: Write the failing test** ```java class GrpcUnaryReliabilityContractTest { @org.junit.jupiter.api.Test void completionUnknownNeverReexecutesNonIdempotentUseCase() { var contract = GrpcUnaryReliabilityContract.standard(); var result = contract.evaluate( RpcIdempotencyProfile.NON_IDEMPOTENT, GrpcCompletionOutcome.COMPLETION_UNKNOWN); org.assertj.core.api.Assertions.assertThat( result.reexecute()).isFalse(); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-testkit-core:test --tests 'io.backend.skeleton.grpc.testkit.GrpcUnaryReliabilityContractTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcUnaryContractResult( boolean retry, boolean reexecute, boolean queryLedger) {} public final class GrpcUnaryReliabilityContract { public static GrpcUnaryReliabilityContract standard() { return new GrpcUnaryReliabilityContract(); } public GrpcUnaryContractResult evaluate( RpcIdempotencyProfile idempotency, GrpcCompletionOutcome outcome) { if (outcome == GrpcCompletionOutcome.COMPLETION_UNKNOWN) { return new GrpcUnaryContractResult( false, false, idempotency == RpcIdempotencyProfile.IDEMPOTENCY_KEY_REQUIRED); } return new GrpcUnaryContractResult(false, false, false); } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-testkit-core:test --tests 'io.backend.skeleton.grpc.testkit.GrpcUnaryReliabilityContractTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcUnaryReliabilityContract.java' 'modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcUnaryScenario.java' 'modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcUnaryContractResult.java' 'modules/grpc/grpc-testkit-core/src/test/java/io/backend/skeleton/grpc/testkit/GrpcUnaryReliabilityContractTest.java' git commit -m "test: certify grpc unary reliability" ``` ### Task 50: Server Streaming Contract Suite **Files:** - Create: `modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcServerStreamingContract.java` - Create: `modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcStreamingScenario.java` - Create: `modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcStreamingContractResult.java` - Test: `modules/grpc/grpc-testkit-core/src/test/java/io/backend/skeleton/grpc/testkit/GrpcServerStreamingContractTest.java` **Interfaces:** - Consumes: Tasks 37–41의 envelope, writer, flow control, resume, lifetime policy. - Produces: sequence·ordering·slow consumer·partial delivery·resume·drain을 검증하는 공통 streaming suite. **Implementation requirements:** - monotonic sequence와 duplicate/gap handling을 검증한다. - slow consumer가 bounded queue를 넘으면 기본적으로 stream을 종료한다. - network loss 후 새 stream과 resume token으로 이어지는지 검증한다. - history loss가 full resync를 요구하는지 검증한다. - server drain이 마지막 evidence와 cancellation을 전달하는지 검증한다. - [ ] **Step 1: Write the failing test** ```java class GrpcServerStreamingContractTest { @org.junit.jupiter.api.Test void historyLossRequiresFullResync() { var contract = GrpcServerStreamingContract.standard(); org.assertj.core.api.Assertions.assertThat( contract.onHistoryLost()) .isEqualTo( GrpcResumeDecision.FULL_RESYNC_REQUIRED); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-testkit-core:test --tests 'io.backend.skeleton.grpc.testkit.GrpcServerStreamingContractTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public final class GrpcServerStreamingContract { public static GrpcServerStreamingContract standard() { return new GrpcServerStreamingContract(); } public GrpcResumeDecision onHistoryLost() { return GrpcResumeDecision.FULL_RESYNC_REQUIRED; } } public record GrpcStreamingContractResult( long lastSequence, GrpcResumeDecision resumeDecision, GrpcStreamTerminationReason terminationReason) {} ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-testkit-core:test --tests 'io.backend.skeleton.grpc.testkit.GrpcServerStreamingContractTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcServerStreamingContract.java' 'modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcStreamingScenario.java' 'modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcStreamingContractResult.java' 'modules/grpc/grpc-testkit-core/src/test/java/io/backend/skeleton/grpc/testkit/GrpcServerStreamingContractTest.java' git commit -m "test: certify grpc server streaming reliability" ``` ### Task 51: 성능·Channel Saturation·Flow-control Gate **Files:** - Create: `modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/performance/GrpcPerformanceBudget.java` - Create: `modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/performance/GrpcPerformanceResult.java` - Create: `modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/performance/GrpcPerformanceGate.java` - Test: `modules/grpc/grpc-testkit-netty/src/test/java/io/backend/skeleton/grpc/performance/GrpcPerformanceGateTest.java` **Interfaces:** - Consumes: Task 47 Netty runtime, Task 45 metrics와 representative unary/stream services. - Produces: Unary QPS·latency, channel stream queue, executor saturation, stream connection·memory와 drain budget을 검증하는 gate. **Implementation requirements:** - p50·p95·p99와 error rate를 profile별로 측정한다. - HTTP/2 concurrent stream saturation과 client queue time을 측정한다. - blocking executor·DB pool·channel saturation을 구분한다. - stream connection 수, message throughput, heap/direct memory, flow-control stall을 측정한다. - baseline 대비 허용 regression을 넘으면 release를 실패시킨다. - [ ] **Step 1: Write the failing test** ```java class GrpcPerformanceGateTest { @org.junit.jupiter.api.Test void p99AboveBudgetFailsRelease() { var gate = new GrpcPerformanceGate( new GrpcPerformanceBudget( java.time.Duration.ofMillis(200), 0.01)); org.assertj.core.api.Assertions.assertThatThrownBy( () -> gate.verify(new GrpcPerformanceResult( java.time.Duration.ofMillis(250), 0.0))) .isInstanceOf(IllegalStateException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-testkit-netty:test --tests 'io.backend.skeleton.grpc.performance.GrpcPerformanceGateTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcPerformanceBudget( java.time.Duration maxP99, double maxErrorRate) {} public record GrpcPerformanceResult( java.time.Duration p99, double errorRate) {} public final class GrpcPerformanceGate { private final GrpcPerformanceBudget budget; public GrpcPerformanceGate(GrpcPerformanceBudget budget) { this.budget = budget; } public void verify(GrpcPerformanceResult result) { if (result.p99().compareTo(budget.maxP99()) > 0 || result.errorRate() > budget.maxErrorRate()) { throw new IllegalStateException( "gRPC performance budget exceeded"); } } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-testkit-netty:test --tests 'io.backend.skeleton.grpc.performance.GrpcPerformanceGateTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/performance/GrpcPerformanceBudget.java' 'modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/performance/GrpcPerformanceResult.java' 'modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/performance/GrpcPerformanceGate.java' 'modules/grpc/grpc-testkit-netty/src/test/java/io/backend/skeleton/grpc/performance/GrpcPerformanceGateTest.java' git commit -m "test: add grpc performance release gate" ``` ### Task 52: Spring Boot Starter와 Startup Validator **Files:** - Create: `modules/grpc/grpc-spring-boot-starter/src/main/java/io/backend/skeleton/grpc/boot/GrpcPlatformProperties.java` - Create: `modules/grpc/grpc-spring-boot-starter/src/main/java/io/backend/skeleton/grpc/boot/GrpcPlatformAutoConfiguration.java` - Create: `modules/grpc/grpc-spring-boot-starter/src/main/java/io/backend/skeleton/grpc/boot/GrpcPlatformStartupValidator.java` - Create: `modules/grpc/grpc-spring-boot-starter/src/main/java/io/backend/skeleton/grpc/boot/GrpcPlatformConfigurationException.java` - Create: `modules/grpc/grpc-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` - Create: `modules/grpc/grpc-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json` - Test: `modules/grpc/grpc-spring-boot-starter/src/test/java/io/backend/skeleton/grpc/boot/GrpcPlatformStartupValidatorTest.java` **Interfaces:** - Consumes: 모든 Stable module contract와 Spring Boot 4.1 gRPC auto-configuration. - Produces: Stable server/client/policy/admin/observability를 조립하고 잘못된 운영 설정을 fail-fast하는 starter. **Implementation requirements:** - Boot BOM 조합과 Spring gRPC auto-configuration을 재사용한다. - deadline 없는 Stable Unary, unbounded server executor, insecure production TLS를 거부한다. - non-idempotent explicit retry, duplicate retry owner, unsupported resolver/LB를 거부한다. - production reflection, in-process production transport, raw builder exposure를 거부한다. - Stable starter가 Advanced dependency를 자동 포함하지 않는다. - [ ] **Step 1: Write the failing test** ```java class GrpcPlatformStartupValidatorTest { @org.junit.jupiter.api.Test void productionRejectsUnaryWithoutDeadline() { var validator = new GrpcPlatformStartupValidator(); org.assertj.core.api.Assertions.assertThatThrownBy( () -> validator.validate( GrpcPlatformProperties.invalidWithoutDeadline())) .isInstanceOf( GrpcPlatformConfigurationException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-spring-boot-starter:test --tests 'io.backend.skeleton.grpc.boot.GrpcPlatformStartupValidatorTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GrpcPlatformProperties( boolean production, boolean allUnaryHaveDeadlines, boolean secureTls, boolean reflectionEnabled) { public static GrpcPlatformProperties invalidWithoutDeadline() { return new GrpcPlatformProperties( true, false, true, false); } } public final class GrpcPlatformStartupValidator { public void validate(GrpcPlatformProperties properties) { if (properties.production() && (!properties.allUnaryHaveDeadlines() || !properties.secureTls() || properties.reflectionEnabled())) { throw new GrpcPlatformConfigurationException( "invalid Stable gRPC production configuration"); } } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-spring-boot-starter:test --tests 'io.backend.skeleton.grpc.boot.GrpcPlatformStartupValidatorTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-spring-boot-starter/src/main/java/io/backend/skeleton/grpc/boot/GrpcPlatformProperties.java' 'modules/grpc/grpc-spring-boot-starter/src/main/java/io/backend/skeleton/grpc/boot/GrpcPlatformAutoConfiguration.java' 'modules/grpc/grpc-spring-boot-starter/src/main/java/io/backend/skeleton/grpc/boot/GrpcPlatformStartupValidator.java' 'modules/grpc/grpc-spring-boot-starter/src/main/java/io/backend/skeleton/grpc/boot/GrpcPlatformConfigurationException.java' 'modules/grpc/grpc-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports' 'modules/grpc/grpc-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json' 'modules/grpc/grpc-spring-boot-starter/src/test/java/io/backend/skeleton/grpc/boot/GrpcPlatformStartupValidatorTest.java' git commit -m "feat: assemble grpc spring boot starter" ``` ### Task 53: 호환성 Matrix·Runbook·ADR·Stable Release Gate **Files:** - Create: `modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/release/GrpcCompatibilityMatrix.java` - Create: `modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/release/GrpcStableReleaseGate.java` - Create: `modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/release/GrpcReleaseEvidence.java` - Create: `modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/release/GrpcReleaseDecision.java` - Create: `docs/runbooks/grpc-platform-operations.md` - Create: `docs/adr/ADR-060-grpc-platform-boundary.md` - Create: `docs/adr/ADR-061-grpc-execution-evidence.md` - Create: `docs/adr/ADR-062-grpc-retry-idempotency.md` - Create: `docs/adr/ADR-063-grpc-streaming-resume.md` - Create: `docs/adr/ADR-064-grpc-discovery-kubernetes.md` - Create: `docs/compatibility/grpc-support-matrix.md` - Test: `modules/grpc/grpc-testkit-core/src/test/java/io/backend/skeleton/grpc/release/GrpcStableReleaseGateTest.java` **Interfaces:** - Consumes: Tasks 1–52의 build, schema, contract, Netty, fault, security, performance evidence. - Produces: Boot lane·upstream lane, proto3·Edition lane와 운영 문서 증거를 집계해 Stable 승격을 차단·승인하는 최종 gate. **Implementation requirements:** - Boot 4.1 managed lane을 필수로 검증한다. - upstream gRPC Java override는 compatibility lane에서만 검증한다. - proto3+optional을 Stable로, Edition 2024를 Advanced lane으로 분리한다. - Buf, generated source, unary, streaming, TLS, fault, performance, Kubernetes profile evidence를 모두 요구한다. - Runbook과 ADR이 누락되면 release를 차단한다. - [ ] **Step 1: Write the failing test** ```java class GrpcStableReleaseGateTest { @org.junit.jupiter.api.Test void missingFaultEvidenceBlocksStableRelease() { var evidence = GrpcReleaseEvidence.completeExcept("fault"); var gate = new GrpcStableReleaseGate(); org.assertj.core.api.Assertions.assertThat( gate.decide(evidence)) .isEqualTo(GrpcReleaseDecision.BLOCKED); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:grpc:grpc-testkit-core:test --tests 'io.backend.skeleton.grpc.release.GrpcStableReleaseGateTest' ``` Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GrpcReleaseDecision { APPROVED, BLOCKED } public record GrpcReleaseEvidence( java.util.Set passed) { public static GrpcReleaseEvidence completeExcept( String missing) { var required = new java.util.HashSet<>(java.util.Set.of( "build", "buf", "generated-source", "unary", "streaming", "tls", "fault", "performance", "kubernetes", "runbook", "adr")); required.remove(missing); return new GrpcReleaseEvidence( java.util.Set.copyOf(required)); } } public final class GrpcStableReleaseGate { private static final java.util.Set REQUIRED = java.util.Set.of( "build", "buf", "generated-source", "unary", "streaming", "tls", "fault", "performance", "kubernetes", "runbook", "adr"); public GrpcReleaseDecision decide( GrpcReleaseEvidence evidence) { return evidence.passed().containsAll(REQUIRED) ? GrpcReleaseDecision.APPROVED : GrpcReleaseDecision.BLOCKED; } } ``` Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:grpc:grpc-testkit-core:test --tests 'io.backend.skeleton.grpc.release.GrpcStableReleaseGateTest' ./gradlew grpcStableTest ``` Expected: PASS for the focused test and the aggregate Stable suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/release/GrpcCompatibilityMatrix.java' 'modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/release/GrpcStableReleaseGate.java' 'modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/release/GrpcReleaseEvidence.java' 'modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/release/GrpcReleaseDecision.java' 'docs/runbooks/grpc-platform-operations.md' 'docs/adr/ADR-060-grpc-platform-boundary.md' 'docs/adr/ADR-061-grpc-execution-evidence.md' 'docs/adr/ADR-062-grpc-retry-idempotency.md' 'docs/adr/ADR-063-grpc-streaming-resume.md' 'docs/adr/ADR-064-grpc-discovery-kubernetes.md' 'docs/compatibility/grpc-support-matrix.md' 'modules/grpc/grpc-testkit-core/src/test/java/io/backend/skeleton/grpc/release/GrpcStableReleaseGateTest.java' git commit -m "docs: complete grpc stable release gate" ```