feat: grpc 기능 deep 구현
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
||||
# ADR-GRPC-001: The gRPC platform ships as a registered family, not as one adapter leaf
|
||||
|
||||
- Status: accepted
|
||||
- Date: 2026-08-30
|
||||
- Scope: `:grpc:*`, `:grpc-advanced:*`, `src/config/architecture/modules.json`
|
||||
|
||||
## Context
|
||||
|
||||
The two source plans describe a type-safe gRPC execution platform with its own API, SPI, adapters and
|
||||
composition root: fifteen Stable modules under `modules/grpc` and sixteen Advanced ones under
|
||||
`modules/grpc-advanced`, on Gradle Kotlin DSL, in package `io.backend.skeleton.grpc`, against
|
||||
Spring Boot 4.1.
|
||||
|
||||
None of that layout exists here. This repository uses Groovy DSL, a fail-closed module registry that
|
||||
owns the leaf list, package root `dev.caskeleton`, and Spring Boot 4.0.8. The plans anticipate this:
|
||||
their last Global Constraint says that when the repository structure differs, file paths are remapped
|
||||
and the public contracts, invariants and test meanings are not changed.
|
||||
|
||||
Two shapes were available. Fold the platform into the existing `:adapter:inbound:grpc` leaf as
|
||||
packages — which is what the JPA, GraphQL, WebSocket and HTTP platforms did here — or register it as
|
||||
a family the way `messaging:*` is registered.
|
||||
|
||||
## Decision
|
||||
|
||||
Register it as a family: twelve Stable leaves under `src/grpc/` and six Advanced ones under
|
||||
`src/grpc-advanced/`.
|
||||
|
||||
The deciding property is that this is not a layer of this application. Root `CLAUDE.md` already
|
||||
describes `messaging:*` as "a vendored messaging platform: a product with its own API, SPI, adapters
|
||||
and composition boundary, not a layer of this application", and the gRPC platform is the same shape
|
||||
for the same reason — the application is meant to reach it the way it reaches a library, through an
|
||||
application-owned port. The four platforms that became packages are all layers of this application;
|
||||
this one is not.
|
||||
|
||||
The split between `src/grpc/` and `src/grpc-advanced/` is not organisational. The Stable plan
|
||||
requires that the Stable starter's build fail if it reaches an Advanced module, and separate Gradle
|
||||
path prefixes make that a `verifyCleanArchitectureDependencies` failure rather than a review note:
|
||||
`grpc-spring-boot-starter`'s registry entry names no advanced id, and it cannot acquire one silently.
|
||||
|
||||
## Consequences
|
||||
|
||||
**The registry grew from 44 leaves to 62.** That is a large registry change, made deliberately and in
|
||||
one place. Every new leaf is `runtime_memberships: []`, so nothing ships until a second, explicit
|
||||
decision moves it.
|
||||
|
||||
**The advanced boundary is checked twice.** Once by the registry at build time, and once by
|
||||
`GrpcStableBuildInvariant` at runtime, because a fat jar or a shaded artifact is assembled by
|
||||
something the registry never sees.
|
||||
|
||||
**Four testkit modules became four test lanes.** The plan's split exists so in-process results cannot
|
||||
be mistaken for network results; this repository expresses that with `ca.strict-test-lane`, whose
|
||||
lanes fail when they discover nothing and never serve an up-to-date result. `GrpcEvidenceGrade` keeps
|
||||
the same rule inside the code, so a report cannot cite a contract run as transport evidence.
|
||||
|
||||
**Codegen is not wired.** See ADR-GRPC-002.
|
||||
@@ -0,0 +1,62 @@
|
||||
# ADR-GRPC-002: Schema governance runs without protoc and without the Buf CLI
|
||||
|
||||
- Status: accepted
|
||||
- Date: 2026-08-30
|
||||
- Scope: `:grpc:grpc-proto-contract`, `:grpc:grpc-codegen`
|
||||
|
||||
## Context
|
||||
|
||||
Stable Tasks 8 through 11 require proto style rules, Buf format/lint/breaking governance, a single
|
||||
Java codegen owner, and a descriptor artifact whose consumer-compile result gates a release.
|
||||
|
||||
Two of the tools those tasks name are absent from this toolchain. The Buf CLI is not installed. And
|
||||
`protoc` is available through the Gradle protobuf plugin, but every leaf in this repository passes
|
||||
spotless with google-java-format, checkstyle, SpotBugs at HIGH confidence, Error Prone and `-Werror`
|
||||
— and generated protobuf sources pass none of them. Turning codegen on means excluding a source set
|
||||
from five quality gates.
|
||||
|
||||
There is precedent for such an exclusion: the `jmh` source set has `spotbugsJmh` and `checkstyleJmh`
|
||||
disabled and Error Prone off. So the carve-out is available. It is also a decision about the quality
|
||||
baseline of a leaf, taken for one task, and outside what this work was asked to change.
|
||||
|
||||
`adapter:inbound:grpc` also carries a recorded decision in the opposite direction: its `CLAUDE.md`
|
||||
forbids the protobuf plugin and `.proto` in that leaf, on the grounds that a consuming feature module
|
||||
should own its schema.
|
||||
|
||||
## Decision
|
||||
|
||||
Commit the `.proto` sources and implement every rule the tasks require as executable Java, with no
|
||||
protoc run and no Buf CLI invocation.
|
||||
|
||||
`GrpcProtoContractValidator` reads `.proto` text and enforces proto3 syntax, the
|
||||
`{organization}.{domain}.v{major}` package rule, `java_multiple_files`, a generated Java package
|
||||
disjoint from the hand-written one, `_UNSPECIFIED` enum zero values, `reserved` declarations checked
|
||||
against a supplied removal history, a well-known-type allowlist and a map-field allowlist. It runs
|
||||
against the committed schema in its own test, so the shipped `.proto` files are live rather than
|
||||
decorative.
|
||||
|
||||
`GrpcBufPolicy` fixes the breaking gate at Buf's `FILE` category and names the four lifecycle stages
|
||||
a compliant pipeline registers. `GrpcCodegenManifest` fixes one codegen owner and refuses a literal
|
||||
generator version. `GrpcDescriptorArtifact`, `GrpcConsumerFixture` and `GrpcSchemaArtifactPublisher`
|
||||
carry the schema hash, the descriptor digest and the per-consumer source-break report, and refuse a
|
||||
publish that breaks a consumer or republishes a released version with different bytes.
|
||||
|
||||
The committed `buf.yaml` states the same rules, so running the CLI in an environment that has it
|
||||
reaches the same verdict.
|
||||
|
||||
## Consequences
|
||||
|
||||
**The invariants are enforced; the process is not run.** Everything Tasks 8 to 11 are about — which
|
||||
schema changes are refused, which consumer breaks block a release, who owns generation — is a
|
||||
build-checkable rule here. What is missing is the protoc invocation and the Buf binary.
|
||||
|
||||
**Turning codegen on is a bounded change.** `GrpcCodegenManifest.caSkeleton()` already names the
|
||||
owner, the managed version source, the build-directory output paths and the disjoint package policy
|
||||
that a real plugin configuration has to satisfy. The work is a source-set carve-out and a plugin
|
||||
block, not a redesign.
|
||||
|
||||
**The fixtures use a text codec.** `GrpcTextCodec` gives the testkit a UTF-8 marshaller so the
|
||||
in-process and Netty lanes can exercise interceptors, status mapping, metadata limits and stream
|
||||
sequencing without generated stubs. Those contracts are properties of the platform and the transport,
|
||||
not of any message shape, so the substitution costs nothing — and the lanes run today rather than
|
||||
after codegen lands.
|
||||
@@ -0,0 +1,50 @@
|
||||
# ADR-GRPC-003: Transport, business and stream evidence are three axes, and none implies another
|
||||
|
||||
- Status: accepted
|
||||
- Date: 2026-08-30
|
||||
- Scope: `:grpc:grpc-core-api`, `:grpc:grpc-policy`, `:grpc:grpc-testkit`
|
||||
|
||||
## Context
|
||||
|
||||
A failed RPC produces a status code, and a status code is not an answer to the question the caller
|
||||
actually has. `DEADLINE_EXCEEDED` on a mutation does not say whether the mutation happened;
|
||||
`UNAVAILABLE` after the request was sent does not say the server never saw it; response headers
|
||||
arriving does not say a transaction committed.
|
||||
|
||||
Every one of those is a place where a plausible inference produces a duplicate write or a lost one,
|
||||
and none of them is visible in a test that only exercises the happy path.
|
||||
|
||||
## Decision
|
||||
|
||||
Model what happened as three independent axes, and refuse the inferences between them.
|
||||
|
||||
`GrpcTransportEvidence` records what the client observed on the wire, and distinguishes `NOT_SENT` —
|
||||
the client watched its own send fail — from `UNOBSERVED`, which is every other case where nothing is
|
||||
known. `GrpcBusinessEvidence` records what the application confirmed, with `COMMIT_UNKNOWN` as a real
|
||||
state rather than a placeholder. `GrpcStreamEvidence` is a sealed hierarchy whose non-empty cases all
|
||||
carry a position, because "partial" without a last sequence can be neither resumed nor reconciled.
|
||||
|
||||
`GrpcExecutionEvidence` holds all three and rejects combinations nobody could have observed: a unary
|
||||
call with stream evidence, or a request the client watched fail to send that nonetheless carries
|
||||
business evidence. Promoting response headers to a confirmed commit is possible only by editing
|
||||
`withResponseHeadersSeen`, which is one method rather than a plausible line in an interceptor.
|
||||
|
||||
`GrpcCompletionOutcome.forMutation` derives what a caller may conclude, and defaults
|
||||
`DEADLINE_EXCEEDED` and post-send `UNAVAILABLE` on a mutation to `COMPLETION_UNKNOWN`.
|
||||
|
||||
The same types are used by the failure model and by the observation convention, so an incident has
|
||||
one account of a call rather than two.
|
||||
|
||||
## Consequences
|
||||
|
||||
**A whole class of retry bug becomes unrepresentable.** `GrpcRetryEligibility` reads all three axes
|
||||
plus the idempotency profile; a caller cannot reach "retry" from a status alone because the status
|
||||
alone is not an input.
|
||||
|
||||
**The fault lane has something to check.** `GrpcTransportEvidenceClassifier` turns a client's
|
||||
observations into evidence and refuses to infer `NOT_SENT` from an unobserved state — and the lane
|
||||
exercises it against a real connection dropped mid-call, not against a mock.
|
||||
|
||||
**Callers must handle a third outcome.** `COMPLETION_UNKNOWN` is not a failure and not a success, and
|
||||
a caller that treats it as either is wrong. `GrpcOperationStatusQuery` and `GrpcCompletionReconciler`
|
||||
exist so that resolving it is a supported path rather than an exercise for the caller.
|
||||
@@ -0,0 +1,52 @@
|
||||
# ADR-GRPC-004: One retry owner, and keyed mutations need a durable ledger
|
||||
|
||||
- Status: accepted
|
||||
- Date: 2026-08-30
|
||||
- Scope: `:grpc:grpc-policy`, `:grpc:grpc-operation-ledger-jpa`, `:grpc:grpc-core-api`
|
||||
|
||||
## Context
|
||||
|
||||
Three layers can retry a gRPC call: the application, the channel's service config, and a service
|
||||
mesh. Their effects multiply. Three attempts at each layer is twenty-seven requests for one call, and
|
||||
the load arrives exactly when the dependency is already failing.
|
||||
|
||||
Separately, a mutation that is safe to repeat needs somewhere to record that it ran. Without one, a
|
||||
retry after a lost response either duplicates the effect or drops it, and nothing distinguishes the
|
||||
two afterwards.
|
||||
|
||||
## Decision
|
||||
|
||||
**Exactly one retry owner per channel.** `GrpcRetryOwner` has four values including `NONE`, which is a
|
||||
decision rather than an omission. `GrpcServiceConfigPolicy` refuses an in-process retry entry when the
|
||||
owner is the mesh or nobody, and `GrpcRetryOwnershipValidator` compares the service config's method
|
||||
names against the policy catalog — a renamed method leaves its retry entry matching nothing, silently,
|
||||
and the method then runs with channel defaults.
|
||||
|
||||
**Retry eligibility reads the method, the evidence and the status together.**
|
||||
`GrpcRetryEligibility` refuses a non-idempotent method outright, refuses any call whose stream
|
||||
delivered a prefix, and turns a `DEADLINE_EXCEEDED` or post-send `UNAVAILABLE` mutation into
|
||||
"resolve the completion first" rather than a retry.
|
||||
|
||||
**A keyed mutation is retryable only with both a caller key and a durable ledger.**
|
||||
`GrpcOperationLedger` is a port in `grpc-core-api`, so the policy layer can require durable
|
||||
idempotency without depending on a database. Its `claim` contract is a single atomic insert-or-read
|
||||
against a unique constraint: `JpaGrpcOperationLedger` inserts first and reads on constraint violation,
|
||||
because a read-then-insert implementation has a window exactly as wide as the race it closes and
|
||||
passes every test that does not run two attempts concurrently.
|
||||
|
||||
The identity is caller fingerprint plus full method plus hashed key. All three are load-bearing:
|
||||
without the caller, one tenant's key suppresses another's write; without the method, a key reused
|
||||
across operations makes the second a replay of the first.
|
||||
|
||||
## Consequences
|
||||
|
||||
**A budget bounds retries as a fraction of traffic.** `GrpcRetryBudget` degrades to roughly no
|
||||
retries when everything is failing, which is the behaviour that lets a dependency recover.
|
||||
|
||||
**The ledger and the mutation should commit together.** `JpaGrpcOperationLedger` carries no
|
||||
transaction annotations, deliberately: a `REQUIRES_NEW` would put the claim in its own transaction and
|
||||
reintroduce the window where the write is durable and the claim is not.
|
||||
|
||||
**A key reused for a different request is a caller error, not a duplicate.** The stored request
|
||||
fingerprint turns that into `FAILED_PRECONDITION` rather than silently returning the first request's
|
||||
answer.
|
||||
@@ -0,0 +1,50 @@
|
||||
# ADR-GRPC-005: One writer per stream, a bounded queue, and resume that refuses to guess
|
||||
|
||||
- Status: accepted
|
||||
- Date: 2026-08-30
|
||||
- Scope: `:grpc:grpc-policy`
|
||||
|
||||
## Context
|
||||
|
||||
`StreamObserver` is not thread-safe, and the failure when two producers call `onNext` concurrently is
|
||||
not an exception — it is interleaved bytes, which a client decodes as a corrupt message or, worse, as
|
||||
a valid one it should never have received.
|
||||
|
||||
Two further properties of server streams are easy to get wrong in ways that look healthy. A consumer
|
||||
that falls behind either terminates the stream or silently loses messages, and the second leaves a
|
||||
client with a stream that appears fine and is missing changes. And a reconnect either continues from
|
||||
a position the server can still replay, or skips whatever is no longer there.
|
||||
|
||||
## Decision
|
||||
|
||||
**A bounded queue drained by one writer.** `GrpcSerializedStreamWriter` accepts messages from any
|
||||
thread and hands them to the transport only from `flush`, which is synchronized. `write` returning
|
||||
`ACCEPTED` means queued, and the name is deliberately not `sent`: the transport call returns as soon
|
||||
as bytes are handed over, so no method here can honestly report delivery.
|
||||
|
||||
**Both a message bound and a byte bound.** Either alone is unbounded in the other dimension.
|
||||
`GrpcFlowControlPolicy` also takes the transport's own readiness signal, because a writer that relies
|
||||
only on its queue bound produces as fast as it can allocate.
|
||||
|
||||
**Termination is the default for a slow consumer.** `GrpcSlowConsumerPolicy.DROP_OLDEST` exists for
|
||||
feeds whose business meaning tolerates loss, and is not the default, because a client cannot detect
|
||||
dropped messages: the sequence numbers it sees are the ones it was sent.
|
||||
|
||||
**Resume is refused rather than faked.** `GrpcStreamGapDetector` requires a signed, unexpired token
|
||||
whose caller and filter fingerprints match the current request, refuses one whose snapshot version
|
||||
moved, and returns `FULL_RESYNC_REQUIRED` when the cursor predates retained history. `GrpcResumeToken`
|
||||
carries a key id so the signing key can rotate without invalidating every outstanding token.
|
||||
|
||||
## Consequences
|
||||
|
||||
**A stream carries an envelope, not a bare payload.** `GrpcStreamEnvelope` holds the stream id,
|
||||
generation, sequence, snapshot version and resume token, because resume, gap detection and drain all
|
||||
need a position and a generation.
|
||||
|
||||
**Four clocks, not one.** `GrpcStreamLifetimePolicy` separates setup deadline, idle timeout, max
|
||||
duration and heartbeat interval, and refuses combinations where one can never fire. Merging any pair
|
||||
produces a familiar bug: an idle timeout used as a max duration kills healthy busy streams.
|
||||
|
||||
**A heartbeat is a liveness signal and nothing else.** It is not an application acknowledgement and
|
||||
not an ordering guarantee; `GrpcStreamHeartbeat` says so in the place somebody would otherwise reuse
|
||||
it.
|
||||
@@ -0,0 +1,68 @@
|
||||
# ADR-GRPC-006: Stable discovery is DNS and static, and a Kubernetes profile names who balances
|
||||
|
||||
- Status: accepted
|
||||
- Date: 2026-08-31
|
||||
- Scope: `:grpc:grpc-discovery`, `:grpc:grpc-client`
|
||||
|
||||
## Context
|
||||
|
||||
A gRPC channel's discovery configuration has a failure mode with no runtime symptom: it works, and
|
||||
it does not do what the dashboard says it does.
|
||||
|
||||
The specific case is `round_robin` over a Kubernetes Service ClusterIP. The Service is one virtual
|
||||
address, so the resolver returns one endpoint and the client-side balancer has nothing to rotate
|
||||
across; kube-proxy picks a pod at connect time, and an HTTP/2 connection is long-lived, so every
|
||||
request from that client goes to the same pod for the life of the connection. Nothing fails. The
|
||||
configuration says `round_robin`, the metrics show requests spread across clients rather than pods,
|
||||
and the conclusion "we have client-side load balancing" is wrong in a way nobody is prompted to
|
||||
check.
|
||||
|
||||
The mirror-image mistake is `pick_first` over a headless record, which pins a client to one pod out
|
||||
of many.
|
||||
|
||||
Separately, a service mesh changes who owns retries, and a deployment that adds mesh routing without
|
||||
removing its own retry policy has two retriers whose effects multiply.
|
||||
|
||||
## Decision
|
||||
|
||||
**Stable resolvers are Static, DNS and Unix domain socket; Stable load balancing is `pick_first` and
|
||||
`round_robin`.** `GrpcDiscoveryPolicyValidator.requireStableScheme` refuses `xds`, `consul`, `etcd`
|
||||
and `eureka` by name, with a message saying they are Advanced capabilities with their own control
|
||||
plane and promotion gate rather than unknown schemes.
|
||||
|
||||
**The pairing is checked against the resolved address count, not against intent.**
|
||||
`GrpcResolverProfile` carries `expectedAddressCount`, and `GrpcStableLoadBalancer.effective` answers
|
||||
whether the policy distributes anything over that many endpoints. A `round_robin` profile over one
|
||||
address is a reported violation whose message says it describes spreading that is not happening.
|
||||
|
||||
**A Kubernetes deployment names its routing mode**, and the mode implies both the balancer and the
|
||||
retry owner. `GrpcKubernetesRoutingMode` has three values — `K8S_VIP`, `K8S_HEADLESS`, `MESH` — and
|
||||
`GrpcKubernetesProfile` refuses a mesh profile whose retry owner retries in-process.
|
||||
|
||||
**A profile that carries long-lived streams must state a reconnect budget and a readiness drain
|
||||
grace.** A stream pins a client to one pod for its whole life, so every rollout, eviction and
|
||||
scale-down ends it. `GrpcKubernetesProfileValidator` additionally reports a VIP profile carrying
|
||||
long streams, and a drain grace shorter than the reconnect budget — the second means the pod stops
|
||||
serving before its clients have finished reconnecting elsewhere.
|
||||
|
||||
**A DNS profile must refresh.** `GrpcResolverProfile` refuses a zero refresh interval on DNS,
|
||||
because a channel that resolved once at startup keeps sending to addresses that stopped existing an
|
||||
hour ago, and the resulting `UNAVAILABLE` looks like an unhealthy deployment long after the rollout
|
||||
finished.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Two validators, not one.** `GrpcDiscoveryPolicyValidator` asks whether a balancer does anything
|
||||
over the addresses it will see; `GrpcKubernetesProfileValidator` asks whether the deployment shape,
|
||||
the retry owner and the stream obligations agree. A deployment can have a coherent resolver profile
|
||||
and still have put retries in two places, so merging them would let one answer hide the other.
|
||||
|
||||
**`expectedAddressCount` has to come from somewhere.** It is a declared number, and a declaration can
|
||||
be wrong. It is still better than the alternative, which is not comparing anything: a wrong
|
||||
declaration is a wrong statement somebody wrote down, and a missing one is a question nobody asked.
|
||||
`GrpcChannelProfileValidator` takes resolved counts where they are known at startup and skips the
|
||||
check where they are not, rather than guessing and failing on a name that cannot be resolved yet.
|
||||
|
||||
**xDS is reachable, and not by this route.** It lives in `grpc-advanced-resilience` behind its
|
||||
capability flag and its production approval, and `GrpcXdsStartupGuard.advertisableAsStableSupport()`
|
||||
returns false so the Stable support statement cannot widen quietly. See ADR-GRPC-ADV-001.
|
||||
@@ -0,0 +1,55 @@
|
||||
# ADR-GRPC-ADV-001: Each advanced capability has its own flag, its own grade and its own promotion
|
||||
|
||||
- Status: accepted
|
||||
- Date: 2026-08-30
|
||||
- Scope: `:grpc-advanced:*`
|
||||
|
||||
## Context
|
||||
|
||||
The advanced plan covers sixteen capabilities that differ by orders of magnitude in what they bring
|
||||
with them. gRPC-Web adds a proxy. Reactor adds a dependency. xDS adds a control plane, its outage
|
||||
modes, its own security boundary and its own version skew. Hedging duplicates production traffic.
|
||||
|
||||
Bundling them under one flag makes enabling the cheapest of those the same decision as enabling the
|
||||
most consequential.
|
||||
|
||||
## Decision
|
||||
|
||||
**One flag per capability**, under `ca-skeleton.grpc.advanced.<capability>.enabled`, all off by
|
||||
default.
|
||||
|
||||
**Four grades.** `ADVANCED_STABLE` starts on its flag; `EXPERIMENTAL` additionally needs a separate
|
||||
production approval, because the flag says somebody wanted the feature and the approval says somebody
|
||||
accepted that its failure modes are not fully characterised; `WATCH` cannot start at all; `DISABLED`
|
||||
is withdrawn.
|
||||
|
||||
`GrpcAdvancedModuleGuard` distinguishes the three refusals — flag unset, grade unstartable,
|
||||
production unapproved — because the remedy differs in each case.
|
||||
|
||||
**Promotion evidence is per capability.** `GrpcAdvancedPromotionEvidence` is one record per
|
||||
capability, so no promotion can drag another along;
|
||||
`GrpcAdvancedPromotionGate.capabilitiesDraggedAlong` returns an empty list, and that is a tested
|
||||
property rather than a claim. Two thresholds: seven days of soak plus complete evidence for
|
||||
`ADVANCED_STABLE`, thirty for a Stable default, because the second means every deployment gets the
|
||||
capability's dependencies and its failure modes.
|
||||
|
||||
**Infrastructure is named per capability.** `GrpcAdvancedInfrastructureTestkit` records that
|
||||
gRPC-Web needs a proxy, Servlet needs a container, xDS needs a stoppable control plane and Kotlin
|
||||
needs a toolchain. A suite that runs without its infrastructure passes and establishes nothing, which
|
||||
is worse than not having one.
|
||||
|
||||
## Consequences
|
||||
|
||||
**The Kotlin adapter fails closed here, and says why.** This repository has no Kotlin toolchain, so
|
||||
`GrpcKotlinCompatibilityGate.supportableHere()` returns false. The four contract requirements — one
|
||||
schema source, coroutine cancellation propagation, Flow backpressure inside the Stable bounds,
|
||||
platform evidence types preserved — are checkable and are checked; only the compile lane is missing.
|
||||
|
||||
**Edition 2026 cannot be used however its watch report reads.** `GrpcEdition2026Guard` is not
|
||||
conditional on the report, because letting a status record also authorise use means a schema moves
|
||||
onto an edition the moment somebody marks four fields SUPPORTED, with no promotion decision, no
|
||||
consumer migration and no ADR.
|
||||
|
||||
**xDS is not part of the Stable support statement.** It works, behind its flag and its approval;
|
||||
`GrpcXdsStartupGuard.advertisableAsStableSupport()` returns false so a support matrix cannot widen
|
||||
quietly.
|
||||
@@ -0,0 +1,62 @@
|
||||
# gRPC advanced capability support matrix
|
||||
|
||||
Every capability in `:grpc-advanced:*`, its grade, and what it would take to raise it.
|
||||
`GrpcAdvancedSupportMatrix` is the machine-readable form; `GrpcAdvancedCapability.defaultGrade`
|
||||
carries the same values.
|
||||
|
||||
All capabilities are off by default. Flags are `ca-skeleton.grpc.advanced.<capability>.enabled`.
|
||||
|
||||
## Grades
|
||||
|
||||
| Grade | May start | Production needs a separate approval |
|
||||
| --- | --- | --- |
|
||||
| `ADVANCED_STABLE` | Yes | No |
|
||||
| `EXPERIMENTAL` | Yes | Yes |
|
||||
| `WATCH` | No | — |
|
||||
| `DISABLED` | No | — |
|
||||
|
||||
## Capabilities
|
||||
|
||||
| Capability | Flag | Grade | Real infrastructure its evidence needs |
|
||||
| --- | --- | --- | --- |
|
||||
| Protobuf Edition 2024 | `edition-2024` | `ADVANCED_STABLE` | — |
|
||||
| Protobuf Edition 2026 | `edition-2026` | `WATCH` | — |
|
||||
| Client streaming | `client-streaming` | `ADVANCED_STABLE` | — |
|
||||
| Bidirectional streaming | `bidi-streaming` | `ADVANCED_STABLE` | — |
|
||||
| Manual flow control | `manual-flow-control` | `ADVANCED_STABLE` | — |
|
||||
| Read-only unary hedging | `hedging` | `EXPERIMENTAL` | — |
|
||||
| Custom name resolver | `custom-resolver` | `ADVANCED_STABLE` | — |
|
||||
| Custom load balancer | `custom-load-balancer` | `EXPERIMENTAL` | — |
|
||||
| Proxyless xDS | `xds` | `EXPERIMENTAL` | xDS control plane |
|
||||
| gRPC-Web | `grpc-web` | `ADVANCED_STABLE` | gRPC-Web proxy |
|
||||
| Servlet HTTP/2 | `servlet-compat` | `ADVANCED_STABLE` | Servlet container |
|
||||
| Spring Integration bridge | `integration-bridge` | `ADVANCED_STABLE` | — |
|
||||
| Reactor adapter | `reactor` | `ADVANCED_STABLE` | — |
|
||||
| Kotlin coroutine / Flow | `kotlin` | `ADVANCED_STABLE` | Kotlin toolchain |
|
||||
| Channelz / CSDS diagnostics | `channel-diagnostics` | `ADVANCED_STABLE` | — |
|
||||
|
||||
## What the grades mean here, concretely
|
||||
|
||||
**Grade is a statement about the contract, not about a deployment.** Every capability's contract is
|
||||
implemented and tested in this repository. What no capability has is evidence from a real deployment:
|
||||
`GrpcAdvancedPromotionEvidence` for each one is empty, and no promotion has been granted.
|
||||
|
||||
**Four capabilities cannot produce meaningful evidence here at all**, because the infrastructure they
|
||||
need is absent. `GrpcAdvancedInfrastructureTestkit.missingInfrastructure` names them, and a suite that
|
||||
runs without its infrastructure passes and establishes nothing.
|
||||
|
||||
**Kotlin is the sharpest case.** This repository has no Kotlin toolchain, so
|
||||
`GrpcKotlinCompatibilityGate.supportableHere()` returns false and always will until one exists. The
|
||||
four contract requirements — one schema source shared with Java, coroutine cancellation propagated,
|
||||
Flow backpressure inside the Stable buffer bounds, platform evidence types preserved — are checkable
|
||||
without a toolchain and are checked. The compile lane is not.
|
||||
|
||||
## Promotion thresholds
|
||||
|
||||
| To | Soak | Also required |
|
||||
| --- | --- | --- |
|
||||
| `ADVANCED_STABLE` | 7 days | compatibility evidence, security review, fault evidence, performance evidence, ADR, runbook, real-environment test |
|
||||
| Stable default | 30 days | all of the above, plus a dependency, security and operational-cost review |
|
||||
|
||||
`WATCH` becomes `EXPERIMENTAL` before anything else. Promotions are independent: promoting one
|
||||
capability changes no other's grade.
|
||||
@@ -0,0 +1,75 @@
|
||||
# gRPC platform support matrix
|
||||
|
||||
What the Stable gRPC platform (`:grpc:*`) is certified against, what it is only checked against, and
|
||||
what is merely watched. The distinction is the point: "works with Spring Boot" is not a statement
|
||||
anyone can act on.
|
||||
|
||||
`GrpcCompatibilityMatrix.caSkeleton()` is the machine-readable form of this table, and
|
||||
`GrpcStableReleaseGate` blocks a release when a certified lane has no result or a failing one.
|
||||
|
||||
## Lanes
|
||||
|
||||
| Lane | Grade | Failure blocks a release |
|
||||
| --- | --- | --- |
|
||||
| Boot-managed platform (Spring Boot 4.0.8 BOM) | Certified | Yes |
|
||||
| proto3 with explicit `optional` | Certified | Yes |
|
||||
| `grpc-netty-shaded` | Certified | Yes |
|
||||
| `grpc-netty` (unshaded) | Compatibility | No |
|
||||
| Upstream gRPC Java version override | Compatibility | No |
|
||||
| Protobuf Edition 2024 | Watch | No |
|
||||
| Protobuf Edition 2026 | Watch | No |
|
||||
|
||||
## Runtime baseline
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Java | 21 |
|
||||
| Spring Boot | 4.0.8 (the repository baseline; the plans assume 4.1) |
|
||||
| io.grpc | `ext.grpcVersion` in `src/build.gradle` |
|
||||
| Protobuf | `ext.protobufVersion` in `src/build.gradle` |
|
||||
| Stable transport | Netty (shaded) |
|
||||
| Stable RPC shapes | Unary, Server Streaming |
|
||||
| Stable resolvers | Static, DNS, Unix domain socket |
|
||||
| Stable load balancing | `pick_first`, `round_robin` |
|
||||
|
||||
## Evidence grades
|
||||
|
||||
A capability may only be advertised on evidence of a grade that can establish it.
|
||||
`GrpcEvidenceGrade.requireCertifies` enforces this, and `GrpcReleaseEvidence.supports` refuses a
|
||||
claim backed by the wrong lane.
|
||||
|
||||
| Grade | Lane | Establishes |
|
||||
| --- | --- | --- |
|
||||
| `CONTRACT` | `grpcInProcessContractTest` | adapter, interceptor order, status mapping, validation, idempotency replay, context propagation |
|
||||
| `TRANSPORT` | `grpcNettyContractTest` | HTTP/2, TLS, mTLS, metadata limit, message limit, GOAWAY, keepalive, graceful shutdown |
|
||||
| `FAULT` | `grpcFaultTest` | connection loss, completion unknown, partial stream, evidence classifier |
|
||||
| `PERFORMANCE` | `grpcPerformanceTest` | latency, stream saturation, executor saturation, drain budget |
|
||||
|
||||
In-process results are never transport evidence. The in-process transport does not negotiate TLS,
|
||||
does not frame HTTP/2 and does not enforce transport-level limits, so a suite that passes there has
|
||||
tested the adapter and not the transport.
|
||||
|
||||
## What is not supported
|
||||
|
||||
| | Where it lives |
|
||||
| --- | --- |
|
||||
| Client streaming, bidirectional streaming | `grpc-advanced-streaming` |
|
||||
| Manual flow control | `grpc-advanced-streaming` |
|
||||
| Hedging | `grpc-advanced-resilience` |
|
||||
| Custom name resolver, custom load balancer | `grpc-advanced-resilience` |
|
||||
| xDS | `grpc-advanced-resilience` |
|
||||
| gRPC-Web, Servlet HTTP/2, Spring Integration, Reactor, Kotlin | `grpc-advanced-compat` |
|
||||
| Channelz / CSDS diagnostics | `grpc-advanced-diagnostics` |
|
||||
|
||||
## Current release status
|
||||
|
||||
Not released. Every `:grpc:*` leaf is `runtime_memberships: []` in the module registry, so the
|
||||
platform is build-only: it compiles, its lanes run, and no deployed artifact carries it.
|
||||
|
||||
Two release gate inputs are outstanding and are the work between here and a release:
|
||||
|
||||
- **Performance baseline.** The performance lane runs and asserts shape — ordered percentiles, a gate
|
||||
that reads them — rather than absolute numbers. A recorded baseline on a known runner is what turns
|
||||
it into a regression gate.
|
||||
- **Schema codegen.** No `protoc` runs in this build (ADR-GRPC-002), so the descriptor artifact and
|
||||
the consumer-compile fixture are governed as policy rather than produced from a compiled schema.
|
||||
@@ -0,0 +1,98 @@
|
||||
# Runbook: gRPC advanced capabilities
|
||||
|
||||
Scope: the `:grpc-advanced:*` family. Everything here is off by default and stays off until a
|
||||
deployment names it. Nothing in this family ships in a runtime composition today.
|
||||
|
||||
Flags are `ca-skeleton.grpc.advanced.<capability>.enabled`. `GrpcAdvancedCapability` owns the list of
|
||||
capability names; `GrpcAdvancedSupportMatrix` owns their current grades.
|
||||
|
||||
---
|
||||
|
||||
## A capability refuses to start
|
||||
|
||||
`GrpcAdvancedModuleGuard` gives three different refusals, and the remedy differs:
|
||||
|
||||
| Message contains | Meaning | Remedy |
|
||||
| --- | --- | --- |
|
||||
| "its feature flag is not set" | Nobody enabled it | Set the property named in the message |
|
||||
| "tracked rather than implemented" | Grade is `WATCH` | Nothing to do here; the capability is not implemented |
|
||||
| "uncharacterised failure modes" | Grade is `EXPERIMENTAL` and this is production | Record a production approval, or run it outside production |
|
||||
|
||||
The refusal message always names the property key, so the first case is a configuration line rather
|
||||
than a support question.
|
||||
|
||||
---
|
||||
|
||||
## xDS: the control plane went away
|
||||
|
||||
**What you are seeing.** The control plane is unreachable and clients are still routing.
|
||||
|
||||
**What it means.** `GrpcXdsFailurePolicy` serves the last-known-good snapshot, up to its staleness
|
||||
bound.
|
||||
|
||||
**What to do.**
|
||||
|
||||
1. Check the snapshot's age. `SERVE_LAST_KNOWN_GOOD` is the healthy degraded state.
|
||||
2. `STALE_BEYOND_BOUND` means the snapshot is older than the policy allows and is no longer trusted.
|
||||
Beyond that bound, a decommissioned backend would otherwise keep receiving traffic indefinitely.
|
||||
3. `NO_SNAPSHOT_YET` on a starting instance means it never reached the control plane. It fails after
|
||||
the initial fetch timeout rather than starting with no routing.
|
||||
|
||||
**Do not** add application-level retry policy while xDS is enabled. `GrpcXdsStartupGuard` refuses it,
|
||||
because retry defined in two places has a winner that depends on resolution order rather than on a
|
||||
decision.
|
||||
|
||||
---
|
||||
|
||||
## gRPC-Web: a browser call hangs and then fails with no status
|
||||
|
||||
**Almost always the proxy.** A gRPC status arrives as a trailer, and a browser cannot read a trailer
|
||||
the proxy did not expose. Check that the proxy's CORS `expose_headers` includes `grpc-status` and
|
||||
`grpc-message`; `GrpcWebProxyContract.violations` reports exactly this, and the reference
|
||||
configuration in `envoy/envoy.yaml` shows it in place.
|
||||
|
||||
**If the method is client- or bidirectional-streaming**, it cannot work over gRPC-Web at all — a
|
||||
browser has no way to send a stream of messages. `GrpcWebCompatibilityGate` reports such a method
|
||||
before it is exposed.
|
||||
|
||||
---
|
||||
|
||||
## Servlet: a transport setting appears to be ignored
|
||||
|
||||
It is ignored. The container owns the socket, so keepalive tuning, maximum connection age and
|
||||
flow-control window tuning belong to it. `GrpcServletStartupValidator` refuses those settings at
|
||||
startup rather than accepting and dropping them, because a setting that is silently ignored sends the
|
||||
investigation somewhere else.
|
||||
|
||||
A Servlet run never substitutes for Netty certification.
|
||||
|
||||
---
|
||||
|
||||
## Hedging: backend load doubled
|
||||
|
||||
**Expected, within a bound.** Hedging trades duplicate load for tail latency.
|
||||
`GrpcHedgingResult` records both `duplicateBackendCalls` and `cancelledLoserAttempts`; a dashboard
|
||||
showing only the latency improvement makes the trade look free.
|
||||
|
||||
**What to check.** `GrpcHedgingBudget` caps hedges as a fraction of completed calls. If duplicate
|
||||
load is above that fraction, the budget is not being consumed — which means something is issuing
|
||||
hedges outside the coordinator.
|
||||
|
||||
**Hedging is refused** for anything but a read-only unary method. A hedged mutation runs twice by
|
||||
design, and an idempotency key does not help: the second attempt duplicates a success in progress
|
||||
rather than retrying a failure.
|
||||
|
||||
---
|
||||
|
||||
## Promoting a capability
|
||||
|
||||
`GrpcAdvancedPromotionGate.evaluate` names every missing item. Promotion to `ADVANCED_STABLE` needs
|
||||
compatibility evidence, a security review, fault evidence, performance evidence, an ADR, a runbook, a
|
||||
real-environment test and seven days of soak. A Stable default needs thirty.
|
||||
|
||||
Promotions are independent: promoting one capability changes no other's grade, and
|
||||
`GrpcAdvancedSupportMatrix.apply` refuses a decision made against a different matrix state.
|
||||
|
||||
Before citing a suite as evidence, check `GrpcAdvancedInfrastructureTestkit.missingInfrastructure`.
|
||||
A suite that ran without the proxy, the container, the control plane or the toolchain it needs passed
|
||||
and established nothing.
|
||||
@@ -0,0 +1,142 @@
|
||||
# Runbook: gRPC platform operations
|
||||
|
||||
Scope: the `:grpc:*` family. All of it is build-only today — every leaf's `runtime_memberships` is
|
||||
empty — so nothing here fires in production yet. It is written now because the states it covers are
|
||||
the ones an on-call cannot work out from first principles at three in the morning, and shipping the
|
||||
behaviour before the runbook means the first person to meet one is doing that.
|
||||
|
||||
Configuration lives under `ca-skeleton.grpc.platform.*` and is bound by `GrpcPlatformProperties`.
|
||||
The platform does not start unless `ca-skeleton.grpc.platform.enabled=true`.
|
||||
|
||||
---
|
||||
|
||||
## COMPLETION_UNKNOWN on a mutation
|
||||
|
||||
**What you are seeing.** A client received `DEADLINE_EXCEEDED`, `UNAVAILABLE` or `INTERNAL` on a
|
||||
state-changing call, and the response trailer `completion-outcome` reads `COMPLETION_UNKNOWN`.
|
||||
|
||||
**What it means.** The server may have committed. This is not a failure and not a success; the status
|
||||
code cannot distinguish them, which is why the outcome is carried separately.
|
||||
|
||||
**What not to do.** Do not re-issue the call. Do not tell the caller it failed. Both are wrong half
|
||||
the time, and which half is not knowable from the status.
|
||||
|
||||
**What to do.**
|
||||
|
||||
1. Take the `error-execution-id` from the trailers. It is the only link between what the client saw
|
||||
and what the server did.
|
||||
2. If the method is `IDEMPOTENCY_KEY_REQUIRED`, query the operation ledger with the caller
|
||||
fingerprint, the full method name and the caller's key. `GrpcOperationStatusQuery` returns one of
|
||||
`IN_PROGRESS`, `COMMITTED`, `FAILED_TERMINAL`, `NOT_FOUND` or `UNKNOWN`.
|
||||
3. `COMMITTED` means return the stored outcome reference, not a freshly computed answer — the resource
|
||||
may have changed since, and a new answer would describe the state at reconciliation time rather
|
||||
than the state the caller's own call produced.
|
||||
4. `NOT_FOUND` means the operation never started and is safe to re-issue. `FAILED_TERMINAL` means the
|
||||
same.
|
||||
5. `UNKNOWN` means the ledger could not be consulted. Nothing may be concluded. The case is queued by
|
||||
`GrpcCompletionReconciler` and retried later.
|
||||
6. If the method is not keyed, there is no ledger row. Resolve it against the business resource, or
|
||||
escalate to the service owner. This is the case the keyed profile exists to avoid.
|
||||
|
||||
**Escalate when** the reconciler's pending list grows across passes. That means the ledger is
|
||||
unreachable rather than slow.
|
||||
|
||||
---
|
||||
|
||||
## A stream ended with FULL_RESYNC_REQUIRED
|
||||
|
||||
**What you are seeing.** A client's resume was refused and it was told to resynchronise.
|
||||
|
||||
**What it means.** The server can no longer replay from the client's cursor. Either the snapshot
|
||||
version moved, or the cursor predates retained history.
|
||||
|
||||
**What to do.** Nothing on the server. The client is expected to discard its position and start a new
|
||||
stream from a fresh snapshot. A client that instead retries the same token will keep receiving the
|
||||
same answer.
|
||||
|
||||
**Escalate when** it is happening to many clients at once. That usually means history retention was
|
||||
reduced, or snapshots are rotating faster than clients reconnect.
|
||||
|
||||
---
|
||||
|
||||
## A stream ended with SLOW_CONSUMER
|
||||
|
||||
**What you are seeing.** Streams terminating with `SLOW_CONSUMER`, and
|
||||
`grpc.stream.flow_control_stalls` rising.
|
||||
|
||||
**What it means.** The consumer could not keep up with the bounded queue. The stream was terminated
|
||||
rather than silently dropping messages, because a client cannot detect drops — the sequence numbers
|
||||
it sees are the ones it was sent.
|
||||
|
||||
**What to do.**
|
||||
|
||||
1. Check whether the consumer is slow or the producer is fast. `grpc.stream.messages` against
|
||||
`grpc.stream.lifetime` tells you the rate.
|
||||
2. If the consumer is slow, the fix is on the consumer. Raising the queue bound moves the failure
|
||||
later and makes it larger.
|
||||
3. A resume is not available after this ending: the messages that overflowed the queue are gone, so
|
||||
continuing from the last delivered sequence would silently skip them. The client resynchronises.
|
||||
|
||||
---
|
||||
|
||||
## RESOURCE_EXHAUSTED under load
|
||||
|
||||
**What you are seeing.** Calls refused with `RESOURCE_EXHAUSTED` and `GrpcAdmissionController`
|
||||
reporting rejections.
|
||||
|
||||
**What it means.** The server is at its concurrency and queue bounds and is shedding rather than
|
||||
queueing. This is the designed behaviour: accepting work whose callers have already given up spends
|
||||
capacity on nothing.
|
||||
|
||||
**What to do.**
|
||||
|
||||
1. Read `grpc.rpc.duration` and `grpc.rpc.queue_wait` separately. Queue time rising with duration flat
|
||||
means the bottleneck is admission, not the work.
|
||||
2. Check which saturation counter is moving — executor, channel or flow control. They look identical
|
||||
in a latency graph and have different fixes.
|
||||
3. Raising `ca-skeleton.grpc.platform.executor-queue-capacity` defers the problem; it does not remove
|
||||
it. `GrpcExecutorProfile` refuses a queue above ten thousand for that reason.
|
||||
|
||||
---
|
||||
|
||||
## A rollout is producing errors at every deploy
|
||||
|
||||
**What you are seeing.** A burst of `UNAVAILABLE` or `CANCELLED` each time an instance goes away.
|
||||
|
||||
**What it means.** The drain sequence is not completing, or is running out of order.
|
||||
|
||||
**What to do.**
|
||||
|
||||
1. `GrpcDrainResult` records what each drain achieved: completed and cancelled unary calls, signalled
|
||||
and cancelled streams, and which phases ran. A drain that routinely force-cancels is the cause.
|
||||
2. The order matters. Readiness flips first and nothing is refused during that window, because there
|
||||
is a gap between an instance reporting unready and routing acting on it. Refusing during that gap
|
||||
turns a clean rollout into a burst of errors at every deploy.
|
||||
3. For long streams, check the Kubernetes profile's `streamReconnectBudget` and
|
||||
`readinessDrainGrace`. A stream is pinned to one pod for its whole life, so every rollout ends it;
|
||||
a profile with long streams and no reconnect budget has not decided what clients do next.
|
||||
|
||||
---
|
||||
|
||||
## Verifying a deployment's configuration
|
||||
|
||||
`GrpcPlatformSnapshotService` produces a secret-free snapshot for a caller on the admin network
|
||||
holding an admin role. Both gates are required.
|
||||
|
||||
`GrpcPlatformSnapshotService.driftAgainstRelease` compares a running snapshot with the release
|
||||
manifest and reports schema version, method policy hash and per-channel profile differences. An
|
||||
instance running a configuration the release did not ship is behind a whole class of incidents that
|
||||
are otherwise diagnosed by reading logs.
|
||||
|
||||
The snapshot carries hashes and names only. A field whose name looks like a credential is refused at
|
||||
construction rather than redacted.
|
||||
|
||||
---
|
||||
|
||||
## Things that are deliberately off
|
||||
|
||||
- **Reflection in production.** `GrpcReflectionMode.defaultFor` returns `DISABLED` for `STAGE` and
|
||||
`PROD`. Reflection publishes the whole schema to anyone who can open a connection.
|
||||
- **Every advanced capability.** See `docs/runbooks/grpc-advanced-capabilities.md`.
|
||||
- **The platform itself.** `ca-skeleton.grpc.platform.enabled` defaults to false, and every `:grpc:*`
|
||||
leaf is build-only in the registry.
|
||||
@@ -0,0 +1,117 @@
|
||||
# 타입 안전 gRPC 실행 플랫폼 — 실행 계획과 실행 결과
|
||||
|
||||
설계 SSOT: [docs/superpowers/specs/2026-08-30-grpc-platform-adaptation-design.md](../specs/2026-08-30-grpc-platform-adaptation-design.md)
|
||||
|
||||
원본:
|
||||
- `docs/2026-08-13-grpc-type-safe-rpc-platform-implementation-plan.md` (Stable, Task 1–53)
|
||||
- `docs/2026-08-13-grpc-advanced-capabilities-expansion-plan.md` (Advanced, Task 1–18)
|
||||
|
||||
이 문서는 실행 전 계획이자 실행 결과 기록이다. 각 phase는 leaf 단위로 닫혔고, 닫힘 조건은
|
||||
`./gradlew <gradle-path>:check` 통과다 — 즉 test + spotless + checkstyle + SpotBugs(HIGH) +
|
||||
Error Prone/`-Werror` + 저장소 전역 게이트 전이 실행이다.
|
||||
|
||||
## Phase 0 — 레지스트리와 스캐폴딩
|
||||
|
||||
- [x] `src/config/architecture/modules.json`에 18개 leaf 등록 (`grpc:*` 12, `grpc-advanced:*` 6)
|
||||
- [x] leaf별 `build.gradle` 18개. io.grpc를 쓰는 leaf는 `grpc-bom`을 module scope로 import
|
||||
- [x] `src/build.gradle`: `:grpc:` / `:grpc-advanced:` 를 plain JUnit+AssertJ 테스트 분기에 추가
|
||||
(`messaging:*`와 같은 이유 — core-api가 Spring도 io.grpc도 이름 부르지 않는다는 주장을
|
||||
검증 가능하게 만든다)
|
||||
- [x] `./gradlew --write-locks ...resolveAndLockAll` 로 lockfile 18개 생성
|
||||
- [x] `verifyCleanArchitectureDependencies` 통과
|
||||
|
||||
## Phase 1 — Foundation (`grpc-core-api`, Stable Task 1–7 + ledger port)
|
||||
|
||||
- [x] Task 1 `GrpcStableModuleCatalog` / `GrpcStableBuildInvariant`
|
||||
- [x] Task 2 `GrpcMethodName` / `GrpcServiceName` / `GrpcChannelProfileName` / `RpcType` / `GrpcStatusCode`
|
||||
- [x] Task 3 `RpcIdempotencyProfile` / `WaitForReadyPolicy` / `GrpcMethodPolicy` / `GrpcMethodPolicyCatalog`
|
||||
- [x] Task 4 `GrpcTransportEvidence` / `GrpcBusinessEvidence` / `GrpcStreamEvidence` / `GrpcExecutionEvidence`
|
||||
- [x] Task 5 `GrpcFailureCategory` / `GrpcCompletionOutcome` / `GrpcFailureContext` / `GrpcPlatformException`
|
||||
- [x] Task 6 `GrpcDeadlineProfile` / `GrpcDeadlineBudget` / `GrpcCancellationToken` / `GrpcDeadlineExceededException`
|
||||
- [x] Task 7 `GrpcRequestContext` / `GrpcMetadataKey` / `GrpcMetadataBudget` / `GrpcClientIdentity`
|
||||
- [x] 추가: `dev.caskeleton.grpc.ledger` port (`GrpcOperationLedger` 외 3) — 정책 계층이 DB에 의존하지
|
||||
않고 durable idempotency를 요구할 수 있게 하기 위해 core-api에 둔다
|
||||
|
||||
## Phase 2 — Contract governance (`grpc-proto-contract`, `grpc-codegen`, Task 8–11)
|
||||
|
||||
- [x] Task 8 `.proto` 2개 + `buf.yaml` + `GrpcProtoStyleManifest` / `GrpcProtoRuleViolation` / `GrpcProtoContractValidator`
|
||||
- [x] Task 9 `GrpcBufPolicy` / `GrpcBreakingCategory` / `GrpcSchemaBaseline`
|
||||
- [x] Task 10 `GrpcCodegenManifest` / `GrpcGeneratedPackagePolicy` / `GrpcCodegenOutput`
|
||||
- [x] Task 11 `GrpcDescriptorArtifact` / `GrpcConsumerFixture` / `GrpcSchemaArtifactPublisher`
|
||||
- 편차: protoc·Buf CLI 미실행. 근거는 ADR-GRPC-002
|
||||
|
||||
## Phase 3 — Policy (`grpc-policy`, Task 12·16·17·20·28–31·33·34·37–43)
|
||||
|
||||
- [x] Task 12 validation, Task 16 context propagation, Task 17 status/rich error, Task 20 TLS/credential rotation
|
||||
- [x] Task 28 deadline calculator, Task 29 cancellation coordinator
|
||||
- [x] Task 30 service config/retry owner, Task 31 retry eligibility/budget/coordinator, Task 42 wait-for-ready
|
||||
- [x] Task 33 idempotency interceptor, Task 34 completion reconciliation
|
||||
- [x] Task 37–41 stream envelope·writer·flow control·resume token·lifetime
|
||||
- [x] Task 43 message size / compression / payload boundary
|
||||
|
||||
## Phase 4 — Server boundary (`grpc-server`, Task 13–15·18–19)
|
||||
|
||||
- [x] Task 13 boundary rules + raw API import rule, Task 14 typed service adapter SPI
|
||||
- [x] Task 15 interceptor 순서 계약, Task 18 Netty profile/executor/admission, Task 19 shaded parity
|
||||
|
||||
## Phase 5 — Client / discovery / admin / observability / ledger
|
||||
|
||||
- [x] `grpc-client` Task 24–27
|
||||
- [x] `grpc-discovery` Task 35–36
|
||||
- [x] `grpc-admin` Task 21–23·45
|
||||
- [x] `grpc-observability` Task 44
|
||||
- [x] `grpc-operation-ledger-jpa` Task 32 (entity + repository + migration + port impl)
|
||||
|
||||
## Phase 6 — Composition과 인증 (`grpc-spring-boot-starter`, `grpc-testkit`)
|
||||
|
||||
- [x] Task 52 properties / auto-configuration / startup validator
|
||||
- [x] Task 46 in-process fixture, Task 47 실제 Netty + TLS/mTLS fixture
|
||||
- [x] Task 48 fault point / scenario / evidence classifier
|
||||
- [x] Task 49–50 unary·streaming contract suite, Task 51 performance budget/gate
|
||||
- [x] Task 53 compatibility matrix / release evidence / release gate
|
||||
- [x] strict test lane 4개 등록 및 실행: `grpcInProcessContractTest`, `grpcNettyContractTest`,
|
||||
`grpcFaultTest`, `grpcPerformanceTest`
|
||||
|
||||
## Phase 7 — Advanced (`grpc-advanced:*`, Advanced Task 1–18)
|
||||
|
||||
- [x] A1·A18 `grpc-advanced-bootstrap`
|
||||
- [x] A2·A3 `grpc-advanced-edition`
|
||||
- [x] A4–A7 `grpc-advanced-streaming`
|
||||
- [x] A8–A11 `grpc-advanced-resilience`
|
||||
- [x] A12–A16 `grpc-advanced-compat`
|
||||
- [x] A17 `grpc-advanced-diagnostics`
|
||||
|
||||
## Phase 8 — 문서와 게이트
|
||||
|
||||
- [x] `src/grpc/CLAUDE.md`, `src/grpc-advanced/CLAUDE.md`
|
||||
- [x] ADR-GRPC-001..005, ADR-GRPC-ADV-001
|
||||
- [x] `docs/runbooks/grpc-platform-operations.md`, `docs/runbooks/grpc-advanced-capabilities.md`
|
||||
- [x] `docs/compatibility/grpc-support-matrix.md`, `docs/compatibility/grpc-advanced-support-matrix.md`
|
||||
- [x] `verifyRunbookReferences`, `verifyDocumentedLeafCount` 통과
|
||||
|
||||
## Phase 9 — 2026-08-31 계획 대조 감사
|
||||
|
||||
사용자가 "빠짐없이 반영한게 맞나"를 물어 계획의 `Files:` 절 전체를 기계 대조했다. 결과와 조치는
|
||||
설계 문서 §6이 SSOT다. 요약:
|
||||
|
||||
- [x] 대조 스크립트 실행 → 초기 결과 present=316 / missing=38
|
||||
- [x] 실제 누락 6건 보완: `GrpcKubernetesProfileValidator`, `ADR-GRPC-006`(discovery/Kubernetes),
|
||||
xDS `bootstrap.json`, `control-plane-snapshot.json`, `DocumentClientFixture`,
|
||||
`buf.gen.yaml`+`buf.lock`
|
||||
- [x] 추가한 픽스처는 전부 실제 검사에 물렸다 — 장식이 되지 않도록:
|
||||
`GrpcXdsStartupGuard.bootstrapMismatches`(namespace 불일치·비TLS control plane),
|
||||
redactor가 실제 모양의 CSDS 데이터로 검증, `GrpcConsumerFixture.fromJavaSource`가 fixture
|
||||
소스에서 요구사항을 역산
|
||||
- [x] 테스트 클래스 17건을 계획이 명시한 이름으로 분리
|
||||
- [x] 재대조 → present=341 / missing=13, 잔여 13건은 전부 §6.1–6.4의 기록된 편차
|
||||
(ADR 파일명 6, codegen convention plugin 3, TLS 인증서 3, Kotlin `.kt` 1)
|
||||
|
||||
## 남은 작업 (이 계획 범위 밖, 별도 결정 필요)
|
||||
|
||||
1. **런타임 투입.** 신규 leaf 전부 `runtime_memberships: []`다. `app-bootstrap`에 배선하려면 registry
|
||||
membership 변경 + `verifyRuntimeModuleMembership` 통과가 선행이며, 그것은 별도 결정이다.
|
||||
2. **`adapter:inbound:grpc` 브리지.** registry의 `allowed_dependencies`에 `grpc-core-api`·`grpc-server`를
|
||||
추가하고 typed service adapter를 배선하는 작업. 플랫폼이 green이 된 지금이 시작점이다.
|
||||
3. **protoc / Buf CLI 활성화.** ADR-GRPC-002가 조건과 비용을 기록한다.
|
||||
4. **performance baseline 기록.** 현재 lane은 shape만 검증한다. 알려진 runner에서의 baseline이
|
||||
regression gate를 만든다.
|
||||
@@ -0,0 +1,208 @@
|
||||
# 타입 안전 gRPC 실행 플랫폼 — 이 저장소 규약으로의 어댑팅 설계
|
||||
|
||||
두 계획 문서를 이 저장소(`ca-skeleton`)의 실제 구조·정책·빌드 게이트에 맞춰 실행 가능한 형태로
|
||||
옮기는 설계다.
|
||||
|
||||
- 원본 A: `docs/2026-08-13-grpc-type-safe-rpc-platform-implementation-plan.md` (Stable, Task 1–53)
|
||||
- 원본 B: `docs/2026-08-13-grpc-advanced-capabilities-expansion-plan.md` (Advanced, Task 1–18)
|
||||
|
||||
원본은 `modules/grpc/**` · Gradle Kotlin DSL · 패키지 `io.backend.skeleton.grpc` · Spring Boot 4.1을
|
||||
전제한다. 이 저장소는 `src/**` · Groovy DSL · `dev.caskeleton` · Spring Boot 4.0.8 · fail-closed
|
||||
module registry를 쓴다. 원본 Global Constraint의 마지막 항목이 이 어댑팅을 명시적으로 허용한다:
|
||||
"실제 저장소 구조가 예상 경로와 다르면 파일 경로만 매핑하고 공개 계약·불변 조건·테스트 의미는
|
||||
변경하지 않는다."
|
||||
|
||||
## 1. 패밀리 위치 — 왜 `adapter:inbound:grpc` 확장이 아닌가
|
||||
|
||||
원본이 기술하는 것은 인바운드 어댑터 하나가 아니라 **자기 API·SPI·adapter·조립 경계를 가진 벤더드
|
||||
RPC 플랫폼**이다. 이 저장소에는 그 형태의 선례가 이미 있다 — `messaging:*`. root `CLAUDE.md`가
|
||||
직접 그렇게 규정한다: "vendored messaging platform: a product with its own API, SPI, adapters and
|
||||
composition boundary, not a layer of this application".
|
||||
|
||||
따라서 gRPC 플랫폼도 같은 자리에 둔다.
|
||||
|
||||
```text
|
||||
src/grpc/ → :grpc:* Stable 플랫폼 (12 leaf)
|
||||
src/grpc-advanced/ → :grpc-advanced:* Advanced/Experimental (6 leaf)
|
||||
src/adapter/inbound/grpc 기존 인바운드 전송 어댑터 (그대로 유지)
|
||||
```
|
||||
|
||||
`grpc-advanced`를 별도 디렉터리·별도 Gradle prefix로 分離하는 이유는 원본 Stable Task 1의 불변
|
||||
조건 "Stable starter가 `modules/grpc-advanced`를 참조하면 build를 실패시킨다"를 registry의
|
||||
`allowed_dependencies`만으로 기계 검증할 수 있게 만들기 위해서다. `verifyCleanArchitectureDependencies`가
|
||||
그 게이트다.
|
||||
|
||||
패키지 루트는 `dev.caskeleton.grpc` / `dev.caskeleton.grpc.advanced`다.
|
||||
|
||||
## 2. Leaf 매핑 — 원본 31개 모듈 → 이 저장소 18개 leaf
|
||||
|
||||
원본 모듈 경계 중 **이 저장소가 이미 다른 메커니즘으로 표현하는 것**만 합친다. 능력(capability)은
|
||||
하나도 버리지 않는다.
|
||||
|
||||
### Stable — `src/grpc/`
|
||||
|
||||
| leaf | 원본 모듈 | 담는 Task | 의존 |
|
||||
| --- | --- | --- | --- |
|
||||
| `grpc-core-api` | grpc-core-api | 2–7 | (없음, Java stdlib) |
|
||||
| `grpc-proto-contract` | grpc-proto-contract | 8 | core-api |
|
||||
| `grpc-codegen` | grpc-codegen | 9–11 | core-api, proto-contract |
|
||||
| `grpc-policy` | grpc-policy | 12, 16, 17, 20, 28–31, 33, 34, 37–43 | core-api |
|
||||
| `grpc-server` | grpc-server | 13–15, 18, 19 | core-api, policy |
|
||||
| `grpc-client` | grpc-client | 24–27 | core-api, policy |
|
||||
| `grpc-discovery` | grpc-discovery | 35, 36 | core-api, client |
|
||||
| `grpc-admin` | grpc-admin | 21–23, 45 | core-api, server |
|
||||
| `grpc-observability` | grpc-observability | 44 | core-api |
|
||||
| `grpc-operation-ledger-jpa` | grpc-operation-ledger-jpa | 32 | core-api |
|
||||
| `grpc-spring-boot-starter` | grpc-spring-boot-starter | 52 | 위 전부 |
|
||||
| `grpc-testkit` | testkit-core + -inprocess + -netty + -fault | 46–51, 53 | 위 전부 |
|
||||
|
||||
**testkit 4개를 1개로 합친 근거.** 원본이 testkit을 넷으로 쪼갠 목적은 "in-process 증거를 실제
|
||||
네트워크 증거로 오인하지 않게 한다"이다. 이 저장소는 그 목적을 모듈 경계가 아니라 **strict test
|
||||
lane** 컨벤션(`ca.strict-test-lane`)으로 이미 표현한다 — lane은 태그/소스셋/명시 테스트 중 하나로만
|
||||
선택하고, 아무것도 실행하지 않으면 실패하며, up-to-date 결과를 제공하지 않는다. 그래서 네 모듈은
|
||||
`grpc-testkit` 한 leaf 안의 네 lane이 된다:
|
||||
`grpcInProcessContractTest`, `grpcNettyContractTest`, `grpcFaultTest`, `grpcPerformanceTest`.
|
||||
원본의 "in-process는 HTTP/2·TLS 증거가 아니다"는 lane 분리와 `GrpcEvidenceGrade`로 강제한다.
|
||||
|
||||
### Advanced — `src/grpc-advanced/`
|
||||
|
||||
| leaf | 원본 모듈 | 담는 Task |
|
||||
| --- | --- | --- |
|
||||
| `grpc-advanced-bootstrap` | grpc-advanced-bootstrap | A1, A18 |
|
||||
| `grpc-advanced-edition` | grpc-edition-2024 + grpc-edition-2026-experimental | A2, A3 |
|
||||
| `grpc-advanced-streaming` | grpc-client-streaming + grpc-bidi-streaming + grpc-manual-flow-control | A4–A7 |
|
||||
| `grpc-advanced-resilience` | grpc-hedging + grpc-custom-resolver + grpc-custom-load-balancer + grpc-xds | A8–A11 |
|
||||
| `grpc-advanced-compat` | grpc-web + grpc-servlet-compat + grpc-integration-bridge + grpc-reactor + grpc-kotlin | A12–A16 |
|
||||
| `grpc-advanced-diagnostics` | grpc-channel-diagnostics | A17 |
|
||||
|
||||
Advanced 모듈은 capability마다 leaf를 나누는 대신 **capability grade와 feature flag**
|
||||
(`GrpcAdvancedCapability` / `GrpcAdvancedFeatureFlags`)로 분리한다. A18의 요구사항
|
||||
"Edition, streaming, xDS, gRPC-Web, Servlet, language adapter가 서로의 승격을 묶지 않는다"는
|
||||
leaf 경계가 아니라 capability별 독립 promotion evidence로 표현되므로, 합쳐도 그 불변 조건은 유지된다.
|
||||
|
||||
## 3. 원본과 달라지는 지점 (deviation)과 근거
|
||||
|
||||
| # | 원본 | 이 저장소 | 근거 |
|
||||
| --- | --- | --- | --- |
|
||||
| D1 | Spring Boot 4.1 BOM | Spring Boot 4.0.8 BOM | 저장소 실제 baseline(`src/build.gradle:13`). BOM이 SSOT라는 계약 자체는 유지 |
|
||||
| D2 | Boot-managed Spring gRPC starter | self-managed `io.grpc` (`ext.grpcVersion`) | 기존 기록된 결정(`adapter/inbound/grpc/README.md` "왜 self-managed Netty 인가"). starter 커플링 회피 |
|
||||
| D3 | Gradle Kotlin DSL, `settings.gradle.kts` | Groovy DSL + `config/architecture/modules.json` | registry가 leaf 목록의 SSOT이고 settings는 그것을 읽기만 한다 |
|
||||
| D4 | `io.backend.skeleton.grpc` | `dev.caskeleton.grpc` | 저장소 기본 패키지 |
|
||||
| D5 | Buf CLI (`bufLint`/`bufBreaking` 등) | 저장소 소유 규칙 엔진 + Gradle verify task | Buf CLI 바이너리가 이 환경에 없다. lint/format/breaking **규칙**을 Java로 구현해 동일 판정을 내리고, CLI는 동일 규칙을 재확인하는 선택 경로로 남긴다 |
|
||||
| D6 | protoc/grpc-java codegen을 빌드에서 실행 | `.proto` 소스 + codegen **정책·descriptor 계약**만 실행, protoc 실행은 명시적 확장점 | 아래 별도 절 |
|
||||
| D7 | `grpc-kotlin`의 `.kt` 소스 | Java 쪽 coroutine/Flow **경계 계약**만 | 저장소에 Kotlin 플러그인·소스셋이 없다. A16의 5개 요구사항 중 4개(스키마 단일 소스, cancellation 전파 계약, backpressure 우회 금지, evidence 타입 보존)는 Java 계약으로 표현 가능하고, Kotlin 툴체인 lane은 `GrpcKotlinCompatibilityGate`가 미충족으로 fail-closed 판정한다 |
|
||||
| D8 | 새 leaf가 곧 런타임 | 신규 leaf 전부 `runtime_memberships: []` (build-only) | `messaging:*`가 처음 착지한 방식과 동일. 런타임 투입은 registry membership 변경 + `verifyRuntimeModuleMembership` 통과가 선행 조건이며, 그것은 별도 결정이다 |
|
||||
|
||||
### D6 — protoc 실행을 지금 켜지 않는 이유
|
||||
|
||||
이 저장소의 모든 leaf는 예외 없이 spotless(google-java-format) · checkstyle · spotbugs(HIGH) ·
|
||||
errorprone · `-Werror`를 통과해야 한다(`src/build.gradle`의 `configure(subprojects)` 블록). protoc가
|
||||
만든 소스는 그 어느 것도 통과하지 못하므로, 실제 codegen을 켜려면 해당 소스셋에서 다섯 게이트를
|
||||
모두 끄는 carve-out이 필요하다. 저장소에 선례는 있다(`jmh` 소스셋). 하지만 그 carve-out은 Task
|
||||
10 하나를 위해 품질 게이트를 여는 결정이고, 그 결정은 이 작업 범위 밖의 승인 사항이다.
|
||||
|
||||
그래서 Task 8–11의 **불변 조건**은 전부 실행 가능한 형태로 구현한다:
|
||||
proto style 규칙 검증, 삭제 필드 `reserved` 이력 대조, Buf breaking category(`FILE`) 판정,
|
||||
generated package와 hand-written package 겹침 금지, descriptor+schema hash 릴리스 아티팩트,
|
||||
consumer fixture 실패 시 릴리스 차단. 빠지는 것은 protoc 프로세스 호출 하나이고,
|
||||
`GrpcCodegenManifest`가 그 지점을 단일 owner로 고정한 채 비워둔다.
|
||||
|
||||
## 4. 유지되는 원본 불변 조건 (변경 없음)
|
||||
|
||||
- 실행 증거 3축(Transport/Business/Stream) 분리, `RESPONSE_HEADERS_SEEN` → `COMMIT_CONFIRMED` 자동 승격 금지
|
||||
- `DEADLINE_EXCEEDED` mutation = `COMPLETION_UNKNOWN` 후보, `UNAVAILABLE`만으로 상태 변경 RPC 재호출 금지
|
||||
- `NON_IDEMPOTENT`에 explicit retry·hedging 금지, explicit retry owner는 하나
|
||||
- Stable Unary는 positive deadline 필수
|
||||
- Server Streaming: bounded queue + single serialized writer, partial delivery 후 whole-call retry 금지
|
||||
- Stable RPC 유형은 Unary·Server Streaming, Client/Bidi는 Advanced
|
||||
- production reflection 기본 비활성, dev·stage·prod TLS 필수, trust-all 금지
|
||||
- Stable resolver = Static·DNS, Stable LB = pick_first·round_robin
|
||||
- metric tag에 raw metadata/payload/actor/tenant/object/stream/idempotency ID 금지
|
||||
- Netty가 Stable certification transport, in-process는 network/TLS 증거가 아님
|
||||
|
||||
## 5. 애플리케이션이 이 패밀리에 도달하는 경로
|
||||
|
||||
`messaging:*`의 MSG-015(bridge 부재)를 반복하지 않는다. 계약상의 경로는
|
||||
|
||||
```text
|
||||
application-owned port → adapter:inbound:grpc (typed service adapter) → :grpc:grpc-server SPI
|
||||
```
|
||||
|
||||
이고, Stable Task 13/14가 그 경계를 소유한다(`GrpcApplicationBoundaryRules`,
|
||||
`GrpcRawApiImportRule`, `GrpcServiceAdapter`). 플랫폼이 green이 된 뒤 마지막 단계에서
|
||||
`adapter-inbound-grpc`의 `allowed_dependencies`에 `grpc-core-api`·`grpc-server`를 추가하고
|
||||
브리지를 배선한다. 그 전까지 플랫폼은 self-contained build-only다.
|
||||
|
||||
## 6. 원본 파일 목록 대비 대조 (2026-08-31 감사)
|
||||
|
||||
두 계획이 `Files:` 절에 명시한 산출물 전체를 기계적으로 대조했다. 감사 스크립트는 각 Task의 파일
|
||||
basename이 저장소에 존재하는지 확인한다(build 산출물 제외, 단 annotation processor가 생성하는
|
||||
`spring-configuration-metadata.json`은 build 출력에서 확인).
|
||||
|
||||
```text
|
||||
STABLE (Task 1–53) : present=250 missing=11
|
||||
ADVANCED (Task 1–18) : present= 91 missing= 2
|
||||
TOTAL : present=341 missing=13
|
||||
```
|
||||
|
||||
잔여 13건은 전부 아래 편차로 설명된다. **행동이 빠진 것은 없다.**
|
||||
|
||||
### 6.1 ADR 파일명 (6건) — 저장소 명명 규약
|
||||
|
||||
계획은 `ADR-060` ~ `ADR-065` 연번을 쓴다. 이 저장소의 `docs/adr/`는 접두사 규약을 쓴다
|
||||
(`ADR-WS-001`, `ADR-MONGO-004`, `ADR-WEB-ADV-003`). 내용은 1:1이다.
|
||||
|
||||
| 계획 | 이 저장소 |
|
||||
| --- | --- |
|
||||
| ADR-060 platform-boundary | `ADR-GRPC-001-platform-family-and-registry-shape.md` |
|
||||
| ADR-061 execution-evidence | `ADR-GRPC-003-three-axis-execution-evidence.md` |
|
||||
| ADR-062 retry-idempotency | `ADR-GRPC-004-retry-ownership-and-durable-idempotency.md` |
|
||||
| ADR-063 streaming-resume | `ADR-GRPC-005-server-streaming-single-writer-and-resume.md` |
|
||||
| ADR-064 discovery-kubernetes | `ADR-GRPC-006-stable-discovery-and-kubernetes-routing.md` |
|
||||
| ADR-065 advanced-promotion | `ADR-GRPC-ADV-001-capability-promotion-is-per-capability.md` |
|
||||
|
||||
계획에 없던 `ADR-GRPC-002-schema-governance-without-protoc.md`가 추가로 있다 — D6 결정을 기록한다.
|
||||
|
||||
### 6.2 codegen convention plugin 3건 — D6
|
||||
|
||||
`io.backend.grpc-buf-conventions.gradle.kts`, `io.backend.grpc-codegen-conventions.gradle.kts`,
|
||||
그 `.properties`. protoc를 실행하지 않으므로 실행할 convention plugin이 없다. 이들이 고정했을 결정은
|
||||
`GrpcCodegenManifest`·`GrpcCodegenOutput`·`GrpcGeneratedPackagePolicy`가 Java로 강제하고,
|
||||
`buf.yaml`·`buf.gen.yaml`·`buf.lock`이 CLI가 있는 환경에서 같은 판정을 내리도록 커밋되어 있다.
|
||||
|
||||
### 6.3 TLS 인증서 3건 — 런타임 생성으로 대체
|
||||
|
||||
`ca.crt`, `server.crt`, `client.crt`. 커밋된 인증서는 만료되고, 커밋된 개인키는 개인키다.
|
||||
`GrpcTlsTestMaterial`이 JDK `keytool`로 fixture마다 PKCS12를 생성하고 `close()`가 지운다. Netty의
|
||||
`SelfSignedCertificate`는 JDK 21에서 `sun.security.x509` 미export로 실패하므로 쓰지 않았다.
|
||||
|
||||
### 6.4 Kotlin 소스 1건 — D7
|
||||
|
||||
`GrpcCoroutineAdapter.kt`. 저장소에 Kotlin 툴체인이 없다. 계약 요구 4건은 Java로 검증하고
|
||||
(`GrpcKotlinProfile`, `GrpcCoroutineContextBridge`), compile lane은
|
||||
`GrpcKotlinCompatibilityGate.supportableHere()`가 `false`로 fail-closed다.
|
||||
|
||||
### 6.5 2026-08-31 감사에서 실제로 메꾼 것 (6건)
|
||||
|
||||
감사가 아니었으면 남았을 것들이다.
|
||||
|
||||
| 항목 | 조치 |
|
||||
| --- | --- |
|
||||
| `GrpcKubernetesProfileValidator` | 이름 있는 타입으로 분리. VIP+장기스트림, drain grace < reconnect budget 두 규칙 추가 |
|
||||
| ADR-064 상당 (discovery/Kubernetes) | `ADR-GRPC-006` 작성 |
|
||||
| xDS `bootstrap.json` | 테스트 리소스로 추가 + `GrpcXdsStartupGuard.bootstrapMismatches`가 실제로 대조 (namespace 불일치·비TLS control plane 검출) |
|
||||
| `control-plane-snapshot.json` | 테스트 리소스로 추가 + redactor가 실제 모양의 데이터로 검증됨 |
|
||||
| `DocumentClientFixture.java` + fixture `build.gradle.kts` | 추가. `GrpcConsumerFixture.fromJavaSource`가 fixture 소스에서 service·method·package 요구사항을 **역산**하므로 손으로 적은 목록이 아니다 |
|
||||
| `buf.gen.yaml`, `buf.lock` | 추가 + proto contract 테스트가 내용을 검증 |
|
||||
|
||||
### 6.6 테스트 클래스 17건 — 감사 후 계획대로 분리
|
||||
|
||||
감사 시점에 leaf별로 통합돼 있던 테스트를 계획이 명시한 이름으로 분리했다. 분리 전에도 모든
|
||||
타입이 실제로 검증되고 있었으나(통합 클래스가 해당 타입을 참조), 계획과의 추적성을 위해 나눴다:
|
||||
|
||||
`GrpcNettyVariantSelectorTest`, `GrpcReflectionPolicyTest`, `GrpcClientMetadataPolicyTest`,
|
||||
`GrpcKubernetesProfileTest`, `GrpcEdition2026GuardTest`, `GrpcClientStreamPolicyTest`,
|
||||
`GrpcClientMessageDeduplicatorTest`, `GrpcBidiSequenceTrackerTest`, `GrpcDemandControllerTest`,
|
||||
`GrpcHedgingEligibilityTest`, `GrpcResolverSafetyPolicyTest`, `GrpcLoadBalancerSafetyPolicyTest`,
|
||||
`GrpcXdsStartupGuardTest`, `GrpcWebCompatibilityGateTest`, `GrpcServletStartupValidatorTest`,
|
||||
`GrpcIntegrationBridgePolicyTest`, `GrpcReactorContextBridgeTest`, `GrpcKotlinCompatibilityGateTest`.
|
||||
Reference in New Issue
Block a user