Repository navigation
Replies: 2 comments
Revision 1: every question answeredQuestions 1, 2 and 4 are answered with (a), as proposed: a wallet and its ledger, Question 3 is answered with (b): the wallet's key set is a It costs synctest nothing. The post said WireMock would keep the shared cases out of synctest, but WireMock is needed only on kind. In process, the tests serve the key set themselves: an On kind, WireMock serves the key set, since no walkthrough writes a server for a dependency. This revision adds no questions. Each section below replaces or adds to the part of the same name in the post. Everything else is unchanged. SummaryThe bullet "It runs on kind with one script" becomes:
The applicationIn the tree:
The walletThe bullet "
Calling the key setThis section is added, after "Calling the ledger". The wallet builds one The key set isn't part of readiness. An outage of it answers The settingsIn the wallet's table, the row
The bullet "The key set is a file" is removed. Failure injectionThe wallet's points become TelemetryThe wallet's Client for the key set adds Production-ready, as a checklistThe row "Certificates renewed without a restart" becomes:
The tests
The The deploymentIn The kind overlay adds WireMock, serving the key set over HTTPS from the mapping
Trying it by hand
Alerts
The dashboard's breaker and retry panels are split by Design decisionsThe bullet "The key set is a file" becomes:
Out of scopeAdded:
Open questions
|
Uh oh!
There was an error while loading. Please reload this page.
Note
Part of the humus re-evaluation: wave 6 of Proposal: humus on the bedrock process model. Its revision 17 moved #763's handoff, a service that calls gRPC, from the API walkthrough to a gRPC walkthrough that a later proposal would plan. This is that proposal.
It keeps the points every walkthrough keeps, as the charter's revisions 13, 14 and 15 set them, so it doesn't ask them again:
up.shbrings up, and the LGTM stack;It also keeps what the job, RabbitMQ processor and Kafka processor walkthroughs found as they landed, which the API walkthrough's revision 1 lists.
It builds on proposals that are all done: gRPC services, outbound gRPC clients, authentication and authorization, exposing health probes, failure injection and humus's own metrics, with humus.Main and wave 1. It depends on no other walkthrough. One of its cases waits for the fix of a bug its prototype found in
grpcclient, #1176.Summary
A walkthrough is a production-ready application, kept in this repository, with pages that build it step by step. This one is gRPC's: a wallet and the ledger behind it, two gRPC services, one calling the other.
ledger.v1.Ledgerholds accounts, and the transfers between them, in PostgreSQL.wallet.v1.Walletlets the holder of a bearer token open their account, read its balance, send money from it, and watch it. The wallet keeps no data: it asks the ledger for everything, withgrpcclient.buf generate, the generated code checked in;grpcauthon every method, gRPC health probes, andMaxConnectionAgeto spread the wallet's calls across the ledger's pods.Unavailablewith why, and recovering.up.sh, in CI and for a reader, beside PostgreSQL and the LGTM stack.walletctlsigns a token and calls the wallet, to try it by hand.Background
grpcserver, called withgrpcclient, over mTLS. Its revision 2 moved the catalog to HTTP, since gRPC and protoc would have been needed there for the catalog alone. The charter's revision 17 sent Proposal: outbound gRPC clients #763's handoff here, where the server needs them anyway. Much of what Proposal: API walkthrough #948 first designed for the catalog holds for the ledger: mTLS,grpcauth, a gRPC probes port,Connected,ForwardScenario, andbuf generatewith checked-in code.grpcauth.Classify, the breaker, streams andConnected.interop/grpc_testingservice in for generated code. No page has a.protoof its own,buf generate, or two services that call each other.grpc.NewApicouldn't serve after #485.grpcclient's default retries every method onUnavailable. grpc-go also returnsUnavailablewhen the connection breaks after the request was sent, and the server may have acted on it (Calling gRPC services). So a method that moves money must be idempotent. A transfer's ID does that, as the billing walkthrough's idempotency key does over HTTP.grpcserverends every stream at shutdown withUnavailable, so its client reconnects to another pod (Streams at shutdown).grpcclientretries opening a stream, never a stream that opened (Streams). So the wallet opens the ledger's watch again itself.grpcclientcounts each stream thatgrpcserverends as a failure. Every call to the ledger was then rejected for 10s, though the other pod was healthy.MaxConnectionAgeGracedid the same. It is #1176. The walkthrough doesn't work around it: its case for a rollout of the ledger waits for the fix.Proposed API
A walkthrough adds no API to humus. What it adds is an application, its manifests, its tests and its pages.
The application
github.com/z5labs/humuswith../..andgithub.com/z5labs/humus/grpcwith../../grpc. It isn't released.genproto/googleapis/rpcfor the error details, and PostgreSQL's testcontainers module.internal/certfileis the walkthrough's own copy, as the mailer's and the order status processor's are. A walkthrough imports no other walkthrough's code.The code
//go:embedare directives, and stay. So does theCode generated ... DO NOT EDIT.header thatbuf generatewrites, which tools read to skip generated code..protofiles, the manifests, the script and the Dockerfile have no comments either. The pages say what each method answers.The contract
Two packages in
proto/, generated intowalletpbandledgerpb:Accountis its ID, its balance, and when the balance last changed. ATransferis its ID, the two accounts, the amount and when it was made. Amounts are whole cents, in one currency.STANDARDrules and checks breaking changes withFILE.buf.gen.yamlruns protoc-gen-go and protoc-gen-go-grpc asgo tools, so their versions are pinned by the module'sgo.mod, where Renovate updates them.buf lint,buf breakingagainst main, andbuf generate, failing on a diff.The ledger
OpenAccountGetAccountNotFoundDepositexternalaccount.NotFoundfor no such accountTransferFailedPreconditionwhen the source's balance is short,NotFoundfor either account,InvalidArgumentfor an amount that isn't positive or an account sent to itself, andAlreadyExistsfor a transfer's ID reused for another transferWatchAccountNotFoundfor no such accountEvery transfer is one transaction.
ON CONFLICT (id) DO NOTHING. A transfer already there is answered as it was made, or withAlreadyExistsif it differs.SELECT … FOR UPDATE, in the order of their IDs, so two transfers between the same accounts can't deadlock.A transfer refused for its balance isn't kept, so its ID can be sent again once the money is there.
No account but
externalgoes below zero. The transaction checks it, and aCHECKon the table refuses it too.external's balance is the negative of every deposit, so the ledger's balances always sum to zero.Details say why.
FailedPreconditioncarries agoogle.rpc.PreconditionFailurenaming the account, andInvalidArgumentagoogle.rpc.BadRequestnaming the field.A watch polls. It reads the account every
WATCH_INTERVAL, 1s by default, and sends it when its balance has changed, so a watch sends the balance it settles on rather than each transfer. Open question 4.It is served by
grpcserver, over TLS, withClientAuth: tls.VerifyClientCertIfGivenand the internal CA asClientCAs.grpcauthlets only the internal CA's certificates call it.grpcauth.Default(auth.Require(cert)), wherecertis anauth.MutualTLSscheme whose principal is the certificate's subject. A call without one isUnauthenticated, which says why, rather than a failed handshake.Its own injection point is
postgresql:ledger, evaluated before each transaction. An abort fails it as an unreachable database does, and the method answersUnavailable.Its probes are
grpchealth.NewService, on a plaintext port of its own, since the kubelet presents no certificate and itsgrpcprobes don't use TLS on most clusters. Its readiness is PostgreSQL, reached once: a monitor of the walkthrough's own that pings the pool until a ping first succeeds, and then stays healthy. Open question 2.The wallet
OpenAccountwallet:writeGetAccountwallet:readSendwallet:writeWatchAccountwallet:readgrpcauthchecks the token on every method.grpcauth.Default(auth.RequireScopes(bearer, "wallet:read")), andgrpcauth.MethodforOpenAccountandSend, which needwallet:write.beareris verified byauth/jwt. A handler reads the subject withbearer.From.SendRequestrequires arequest_id. The transfer's ID in the ledger is the subject and that ID together. So a repeat by the app, or a retry by the wallet's client, is the same transfer, and no caller can reuse another's.NotFound,FailedPrecondition,InvalidArgumentandAlreadyExists.Unavailable, neverInternal. That is the ledger'sUnavailableafter the client's last attempt, a call its breaker rejected, or a transport error.google.rpc.ErrorInfosays why:LEDGER_UNAVAILABLE, orLEDGER_SHEDfor a call the breaker rejected without making it.google.rpc.RetryInfo, from the rejection'sRetryAfter, so the app knows when to try again.Timeoutbounds it too, so a call never outlives either.WatchAccountopens the ledger's watch of the caller's account, through the client, and sends on what it sends.Unavailableafter it has sent at least one message, the wallet opens it again. That is the ledger's shutdown, or a lost connection.grpcserverends the caller's watch withUnavailable,server is shutting down, and the app watches again.walletctl watchdoes, and so do the cases.grpcserver, over TLS, with no client certificates. Its probes aregrpchealth.NewServiceon a plaintext port too.Calling the ledger
The wallet builds one
grpcclient.Clientfor the ledger:LEDGER_ADDRandLEDGER_CA_FILE, with its client certificate throughGetClientCertificate, frominternal/certfile;DefaultSettings()' retries, budget, breaker andClassify.Unavailableis retried for every method, which is safe becauseOpenAccountandTransferare idempotent by their IDs and the rest are reads.NotFound,FailedPrecondition,InvalidArgumentandAlreadyExistsare the ledger working, so the breaker counts them as successes;ForwardScenarioset, because the ledger is the walkthrough's own and reads the same codec.Its readiness is the Client's
Connected, which latches. A release that can't reach the ledger never turns ready, and an outage after that leaves every pod ready, so apps get the wallet's ownUnavailableand its reason.Spreading the calls. Both servers pass
grpc.KeepaliveParams(keepalive.ServerParameters{MaxConnectionAge: time.Minute}). Every connection is closed with a GOAWAY after about a minute, so each wallet pod's Client dials again through the ledger's Service and reaches whichever pod it picks, as Calling gRPC services describes. NoMaxConnectionAgeGraceis set: an open watch keeps its old connection until it ends, so watches move only at a rollout. A grace would end them every time it ran out, which #1176 also counts against the breaker.The settings
Every setting is sourced with
config.Structand checked, and a bad one exits 78, naming its variable. Every secret and certificate is a file, read throughs.Host.FS.The wallet:
GRPC_ADDRGRPC_TLS_PEM_FILEPROBES_ADDRLEDGER_ADDRhost:portLEDGER_CA_FILELEDGER_TLS_PEM_FILELEDGER_TIMEOUT,LEDGER_MAX_ATTEMPTSgrpcclient.Settings' fieldsDefaultSettings()'TOKEN_KEYS_FILETOKEN_ISSUER,TOKEN_AUDIENCEjwt.Settings' fieldsFAULT_TRUST_SCOPEFAULT_MAX_DELAYfault.Settings.MaxDelayOTEL_SERVICE_NAME,OTEL_*_EXPORTER,OTEL_EXPORTER_OTLP_ENDPOINTBuilder'swallet, andtelemetry.DefaultSettings()'The ledger:
GRPC_ADDR,GRPC_TLS_PEM_FILE,PROBES_ADDRGRPC_CLIENT_CA_FILEPOSTGRES_URLPOSTGRES_PASSWORD_FILE,POSTGRES_TLS_CA_FILEWATCH_INTERVALFAULT_MAX_DELAY, theOTEL_variablesledgerInitsetssslmode=verify-fulland the CA, so pgx reads nothing from the real environment or~/.pgpass.TOKEN_KEYS_FILEis read and parsed inInit, so a missing or bad key set exits 78. The wallet'sjwt.KeySetis the walkthrough's own, and reads the file again whenever the Verifier asks: for akidit lacks, and afterMaxKeyAge. So a rotated key reaches the wallet without a restart, as aKeySetof the end user's describes. Open question 3.Failure injection
Both services pass
humus.WithFaults(Faults), withjsonscenariounder the baggage memberacme.fault, as every walkthrough does.FAULT_TRUST_SCOPEgrpc:ledger.v1.Ledger/<method>, one for each method its Client callspostgresql:ledger, of its ownForwardScenarioTrustScenarioreads the token from the RPC's metadata and verifies it itself, only for a call that carries a scenario.credentials.TLSInfo, as Failure injection shows forgrpcserver.TransferanswersUnavailable, and the wallet's client retries it, meets the same fault, and gives up. The wallet answersUnavailable. The trace shows every attempt, and the ledger'shumus.fit.evaluationevents.Telemetry
Sendis one trace across both services. A watch is one span on each side, from its opening to its end.Unavailablefor the ledger, with the method and the client's error; the ledger when a transaction fails, with the method and the error. No record or span holds a token.ledger.transfers, a Counter, byledger.transfer.outcome:posted,replayedorrefused;ledger.watches, an UpDownCounter of the watches open;wallet.watches.reopened, a Counter of the ledger's watches that the wallet opened again.rpc.server.call.durationin both,rpc.client.call.durationin the wallet, and the wallet's Client'shumus.circuit.state,humus.circuit.transitions,humus.circuit.callsandhumus.retry.decisions.Production-ready, as a checklist
The walkthrough's pages close with this list, and the application meets each item. The pages give each row a Where column, linking it to the page and section that shows it, as every walkthrough's does.
config.Structand the packages' checks, in both servicess.Host.FS. The token is metadata, which otelgrpc doesn't record, and no handler logs itinternal/certfile, for both servers' certificates and the wallet's client certificate. The key set is read again for akidit lacksminAvailable: 1, and a rolling update ofmaxUnavailable: 0Connected, and the ledger's latch on PostgreSQLgrpcserver.Main's 5s drain. A unary call is bounded byLEDGER_TIMEOUT, 10s, inside the 15s grace period. Watches end withUnavailableand are opened again.terminationGracePeriodSecondsis 30, above the drain, the grace period and the telemetry's shutdownMaxConnectionAgeon both serversCHECKthat no account butexternalgoes below zerobuf breakingagainst main, in CISELECT,INSERTandUPDATEon its two tables. Only the internal CA's certificates call the ledger. A token can use only its subject's account. A non-root, read-only container, with no service account tokendeploy/alerts.yaml,deploy/dashboard.json, and alerts on each Deployment from the Collector's Kubernetes cluster receiverPreconditionFailure,BadRequest,ErrorInfoandRetryInfodetailsFAULT_TRUST_SCOPEempty. Server reflection off. The probes ports serve gRPC health and nothing elseThe tests
go test, in the Build job, insidesynctestwhere they take timeStore, and the watch's polling;internal/certfile, rewriting its files under a running server; the key set; the wallet'sTrustScenario; the store's injection point;walletctl's tokens, verified byauth/jwttestcontainerstagStoreagainst PostgreSQL: concurrent transfers from one account never overdraw it, a repeated ID moves money once, two transfers each way between two accounts don't deadlock, and theCHECKrefuses a negative balancego test, in the Build jobStore, which keeps the pointtestcontainerstag, in "Wallet walkthrough (testcontainers)", which isn't requiredhumus.OSHost(), against PostgreSQL in a container, with two ledger replicasfunctionaltag, in "Wallet walkthrough (kind)", which isn't required, and against an end user's stagingup.sh, and reached throughkubectl port-forwardfunctionaltag, kind onlySend's trace in Tempo holds spans of both services. Its records are in Loki, with its trace ID. Both services' metrics are in Mimirfunctionaltag, kind onlybuf lint,buf breakingagainst main, andbuf generatewith no diffTwo ledger replicas in process. The in-process harnesses run two ledger Apps, at addresses of their own. The wallet's Host dials the ledger's address to one of the replicas still in its endpoints, as a ClusterIP Service does. So a rollout is one replica at a time, as on kind. The dialer is the harness's own code, and the wallet's
Initdoesn't change.The shared cases are in
cases. It imports neither the application nor humustest, so the compiler keeps them black-box. It does importwalletpbandledgerpb, the services' published contracts, as any client would. Each case takes aTarget:RolloutWalletandRolloutLedger, which roll a Deployment, or restart each replica in turn in process, and return once it is done. On kind,RolloutWalletalso forwards the wallet's port again, sincekubectl port-forwardfollows one pod, which the rollout replaces;They poll with plain loops, which work in every harness. Each case makes its own subjects and accounts, named with
crypto/rand, so the cases run in parallel and against a shared staging environment. They mutate, so they never run against production.ParallelandAlone. The cases are two lists, as the mailer's and the shoppingcart's are.Parallelruns at once.Alonethen runs one case at a time, in every harness. A case is alone when it fails calls to the ledger, since a wallet replica's breaker and retry budget are shared by every call it serves, or when it rolls a service out.Parallel:AlreadyExists.FailedPrecondition, with aPreconditionFailurenaming the account, and moves nothing.FailedPrecondition, and the balance ends at 0.wallet:writegetsPermissionDeniedonSend. No token getsUnauthenticated. A call to the ledger without a client certificate isUnauthenticated.grpc:ledger.v1.Ledger/Transferby 2s: the send still succeeds, inside the client's 10sTimeout;Alone, in order:grpc:ledger.v1.Ledger/Transfer: the send isUnavailable,LEDGER_UNAVAILABLE, and moves nothing. The same request ID without the scenario then moves the money once, polled until the breaker, if it opened, lets it through.postgresql:ledger, forwarded to the ledger: the send isUnavailable, and moves nothing.RolloutWallet. The watch ends withUnavailable,server is shutting down. Watching again sees the balance, and a send made during the rollout either succeeds or answersUnavailableand succeeds once when repeated with its ID.RolloutLedger, while the case sends from each account. No watch ends. Each sees every balance its account settles on, and every send succeeds. This case waits for grpcclient: a rollout of a dependency served by grpcserver opens the breaker of a Client with five streams open to the pod #1176: until it is fixed, the first replica's shutdown opens the wallet's breaker.grpc:ledger.v1.Ledger/Transfer, the case sends until an answer isLEDGER_SHED, with aRetryInfo. Sends without the scenario are then polled until one succeeds. It records the breaker's transitions and the retry budget'sthrottleddecisions, which the alerts' check needs. It runs last, because it leaves a breaker open.The abort cases also spend the retry budget, which a success refills by a tenth of a token. So a later case may see fewer attempts than
MaxAttempts, and the cases check the answer, not the number of attempts. Against a shared staging environment, the lastAlonecase opens the breaker of the wallet replica it reaches, and every caller's calls there are shed until it closes.Other whole-App tests stay in each
app: a bad setting exits 78, readiness waits for the dependency, and the telemetry carries the evaluations and the strips, read withhumustest.Telemetry.The deployment
deploy/base/is a kustomization:automountServiceAccountToken: false;maxUnavailable: 0, liveness, readiness and startup probes ofgrpc:on the probes port, withservice: livenessandservice: readiness, resources, and a non-root, read-only security context;ClusterIP, with its port namedgrpc;minAvailable: 1;The kind overlay,
deploy/kind/, adds:schema.sqlapplied by its owner, and the ledger's role with the passwordup.shgenerates;k8s_attributesprocessor, which adds each pod's name, namespace and Deployment, and the Kubernetes cluster receiver, which reports the Deployments' replicas and the containers' restarts. A ClusterRole lets it read both. Mimir promotes the pod's attributes, and scope metadata, to labels;FAULT_TRUST_SCOPEset to the cases' scope.deploy/kind/up.shcreates a kind cluster if there is none. Then it:walletctl jwks;The kind job runs it, and so does a reader. No secret is kept in the repository.
The kind job builds the image, runs
up.sh, and runs the shared cases and the kind-only checks throughkubectl port-forward, as every walkthrough's does.The Dockerfile builds the wallet and the ledger as static binaries, onto a distroless, non-root base. Each Deployment runs its program as the container's command.
Trying it by hand
cmd/walletctlis a small CLI:jwksprints the public half of a private key, as a JSON Web Key Set whosekidis the key's thumbprint.up.shruns it to make the key set's ConfigMap.tokensigns a token with the same key andkid, for a subject, scopes, an issuer and an audience.open,balance,sendandwatchcall the wallet as a subject, with a token they sign, trusting the overlay's CA.watchwatches again whenever its watch ends withUnavailable.depositcalls the ledger with the cases' certificate, from the Secretup.shmade, so a reader can fund an account.It runs on the reader's machine, through
kubectl port-forward, so the image doesn't carry it.Alerts
deploy/alerts.yamlis a Grafana alerting provisioning file, anddeploy/dashboard.jsona Grafana dashboard. The rules query Mimir, on the names the instruments have there. The kind overlay's Grafana provisions both. The pages explain each alert, and show the dashboard.WalletErrorsrpc.server.call.duration's count, byrpc.response.status_code, withoutWatchAccountWalletSlowrpc.server.call.duration, withoutWatchAccountLedgerErrorsWalletErrors, for the ledgerWalletLedgerBreakerOpenhumus.circuit.stateWalletLedgerBreakerOpenedhumus.circuit.transitionsWalletRetriesThrottledhumus.retry.decisions,throttledWalletUnavailable,LedgerUnavailablek8s.deployment.available, from the Collector's Kubernetes cluster receiverWalletCrashLooping,LedgerCrashLoopingk8s.container.restarts, from the same receivergrpcservermarks its spansErrorfor:Unknown,DeadlineExceeded,Unimplemented,Internal,UnavailableandDataLoss.WatchAccount, byrpc_method. A watch's duration is how long it was open, not how long a call took. And every rollout ends every watch withUnavailable, which an error rule would count as an outage.job, which Mimir takes from each one'sservice.name.x unless x offset 5madded toincrease(x[5m]), as the mailer's rules do (#1089).The pages
They are under
docs/content/walkthroughs/wallet/. Each page is a list of steps. A step is a sentence or two, then the code it arrives at, quoted from the walkthrough's files. A reason is given only where the code would be wrong without it, and in one sentence, such as why a send carries its ID, or why the locks are taken in the order of the IDs. There is no history, no alternatives and no asides..protofiles, buf's lint and breaking rules, andbuf generate.schema.sql, a transfer's transaction, and theCHECK.grpcauth, its injection point and its probes.grpcclient, its retries and breaker, why every transfer is safe to retry,ForwardScenario,Connected, and turning the ledger's answers into the wallet's.Init: each service's settings, the secrets, certificates and key set as files,MaxConnectionAge, readiness,BuildTelemetry,FaultsandTrustScenario, andmain.Target,ParallelandAlone, and the failure injection cases.up.sh, and trying it withwalletctl.The pages add the walkthrough to the Walkthroughs section's index.
Changes to other files
.github/workflows/build.yaml:example/with both tags.docs_test.goneeds no change. Its rule for a walkthrough's samples already accepts protobuf, JSON and the walkthrough's own tests.Design decisions
deploy/base/beside the wallet, not a stand-in for a dependency. Open question 1.SERIALIZABLE, PostgreSQL aborts transactions it can't order, and the ledger would answerAbortedfor the client to retry.grpcclientnever retries an open stream, so the wallet opens it again itself. Requiring a message first bounds it: a ledger that keeps ending its watches at once ends the app's, rather than spinning.MaxConnectionAgeGrace. A grace ends open watches every time it runs out, and the watches would move every few minutes for nothing. Watches move at a rollout, which is when a pod goes.grpcprobes use TLS only under an alpha feature gate, and present no credentials, so gRPC health on a service's own port would also need its methods opened by name in each service'sgrpcauthrules. The probes ports serve gRPC health and nothing else, as Health recommends for most clusters. Open question 2.KeySetof the walkthrough's own reads a mounted file, which needs no HTTP client, and no WireMock, so the whole App runs inside synctest. Open question 3.Testing
The walkthrough is its own test: see The tests.
What was prototyped. A prototype ran against main at d4eadda, under
go test -race, insidesynctest, on one humustest Host. Agrpcserverfront relayed a backend's server stream, and called its unary method, through agrpcclientClient over mTLS. grpc-go's interop service stood in forwalletpbandledgerpb. It showed that:credentials.TLSInfo. WithForwardScenario, a scenario aborting its point aborted each of the 3 attemptsDefaultSettings().Classifymade, and the front answeredUnavailable. WithoutForwardScenario, the scenario was stripped at the client, and the call succeeded;Shutdownthen ended the stream withUnavailable,server is shutting down;Unavailable, and the app watched again until a new backend was up and the breaker had closed;Classifythat ignored a stream's end withserver is shutting downkept the breaker closed, and all six moved. A backend withMaxConnectionAgeandMaxConnectionAgeGraceopened the breaker the same way, when its grace closed the connection.Not prototyped: the
.protofiles and buf, PostgreSQL and the transfer's transaction, the watch's polling, the key set as a file,walletctl, the kind overlay and the LGTM stack.Migration from today
Nothing migrates. The walkthrough is new, and no release had a gRPC walkthrough.
Open questions
The application.
Recommendation: (a).
The probes.
grpchealth.NewServiceon a plaintext port, withgrpc:probes, as proposed.httphealth.NewServiceover HTTPS on a port of its own, withhttpGetprobes, as the processors' walkthroughs have. Every port is TLS, but a gRPC walkthrough then shows no gRPC health.grpchealth.NewServeron each Service's own port, withmode: TLSprobes and no second port. That needs the alphaGRPCContainerProbeTLSgate, on kind and on an end user's cluster, and each service'sgrpcauthrules open gRPC health's methods by name.Recommendation: (a).
The wallet's key set.
KeySetof the walkthrough's own, as proposed. Both Apps run inside synctest, and the kind overlay needs no WireMock.RemoteKeySetthat WireMock serves, as the API walkthrough's is. It is closer to an identity provider, but needshttpclientand WireMock, so the shared cases can't run inside synctest.Recommendation: (a).
How a watch learns of a change.
WATCH_INTERVAL, as proposed. It costs a query a second for each watch, and a watch sends the balance it settles on, not each transfer.LISTENandNOTIFY. Each transfer notifies, and each ledger replica holds a connection that listens and wakes its watches. Changes arrive at once, but the listening connection is a Service of its own, and a watch must read its account again whenever that connection is lost, since notifications aren't queued for it.Recommendation: (a).
Out of scope
grpcclient's, and its fix is its own.Depositis the ledger's method for money that enters it, and the cases andwalletctlcall it.MaxConnectionAgespreads the calls instead.walletctlcarries the generated code.schema.sqlis applied before the ledger runs, as the order status processor's is. The API walkthrough shows migrations.Deposit.up.shissues the certificates, and the base mounts the Secrets cert-manager would write.References
grpcserveropens the breaker of agrpcclientClient with five streams open to the podbuf generateLISTENAll reactions