Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,15 +233,7 @@ Use `RegisterHandlerServer` instead of `RegisterHandlerFromEndpoint` in your `In

## Can I use ColdBrew with gRPC streaming?

Yes. ColdBrew supports both **server streaming** and **bidirectional streaming** RPCs. The stream interceptor chain (response time logging, protovalidate, metrics, panic recovery) is applied automatically.

Define streaming RPCs in your `.proto` file as usual:

```protobuf
rpc StreamEvents(EventRequest) returns (stream Event) {}
```

Note: grpc-gateway v2 supports **server streaming** over HTTP by translating gRPC server streams to newline-delimited JSON. Client streaming and bidirectional streaming have limited support — they translate to HTTP but lack true concurrent interleaving over HTTP/1.1. Practical constraints: reverse proxies may buffer streamed responses (requiring `X-Accel-Buffering: no`), and errors in streams are handled via `runtime.WithStreamErrorHandler`. For high-frequency real-time push or bidirectional communication, consider a dedicated WebSocket or SSE endpoint alongside the gateway. See the [grpc-gateway streaming examples](https://github.com/grpc-ecosystem/grpc-gateway/tree/main/examples/internal/proto/examplepb) for details.
Yes — server-streaming, client-streaming, and bidirectional streaming are all supported, and the stream interceptor chain (response time logging, protovalidate, metrics, panic recovery) is applied automatically. See the [Streaming RPCs how-to](/howto/streaming-rpcs) for handler patterns, deadline propagation, backpressure, and the practical limits when serving the same methods through grpc-gateway over HTTP.

## How do I run background workers in my service?

Expand Down
1 change: 1 addition & 0 deletions Index.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ ColdBrew composes proven Go libraries — not replacements:
## Next Steps

- **[Getting Started](/getting-started)** — Create your first ColdBrew service
- **[Concepts](/concepts)** — One-paragraph definitions of the gRPC, observability, and resilience terms ColdBrew builds on
- **[How-To Guides](/howto)** — Step-by-step guides for common tasks
- **[Production Deployment](/howto/production)** — Kubernetes, health probes, tracing, and graceful shutdown
- **[Integrations](/integrations)** — Set up monitoring, tracing, and error tracking
Expand Down
81 changes: 81 additions & 0 deletions concepts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
layout: default
title: Concepts
nav_order: 9
description: "Glossary of the gRPC, observability, and resilience concepts that ColdBrew builds on. Single-paragraph definitions with links to the deeper guides."
permalink: /concepts/
---
# Concepts

A reader who is comfortable with REST but new to the gRPC ecosystem will hit unfamiliar terms in the first few pages — `interceptor`, `gateway`, `vtprotobuf`, `OTLP`, `span`. This page collects them in one place so you can scan a definition and follow the link for depth.

## Table of contents
{: .no_toc .text-delta }

1. TOC
{:toc}

---

## gRPC

A high-performance RPC framework from Google that uses HTTP/2 as the transport and Protocol Buffers as the schema/wire format. Methods are typed (request and response messages are defined in `.proto` files) and code is generated for both client and server, so the network call looks like a local function call. ColdBrew is gRPC-first — every service starts as a gRPC server, and the HTTP/JSON surface is generated from the same proto definition. See [grpc.io](https://grpc.io/) for the upstream docs and [APIs how-to](/howto/APIs) for how ColdBrew wires it up.

## gRPC reflection

Server reflection is a gRPC feature that lets a client discover the available methods and message types at runtime, without needing the `.proto` files locally. Tools like [grpcurl], [grpcui], and Postman use reflection to call services interactively. ColdBrew enables it by default; disable it on public-facing services with `DISABLE_GRPC_REFLECTION=true` (see [Production Deployment](/howto/production)).

## grpc-gateway

[grpc-gateway] is a code generator that produces a reverse-proxy HTTP/1.1+JSON server in front of your gRPC service, mapping HTTP routes to gRPC methods using `google.api.http` annotations in the proto. ColdBrew runs the gateway in-process alongside the gRPC server so a single binary speaks both protocols. See [APIs how-to](/howto/APIs) for the routing annotations and [HTTP Gateway Extensions](/howto/gateway-extensions) for adding custom marshalers or middleware.

## Interceptors

Interceptors are gRPC's middleware. They wrap each unary or streaming RPC, running code before and after the handler — logging, tracing, metrics, validation, panic recovery, auth. ColdBrew ships a default chain (response-time logging → trace ID → OpenTelemetry → Prometheus → error notification → New Relic → panic recovery) and exposes hooks to insert your own. See [Interceptors how-to](/howto/interceptors) for the chain order and how to add custom interceptors, and [Authentication](/howto/auth) for an auth-interceptor example.

## vtprotobuf

[vtprotobuf] is a code generator that produces faster Marshal/Unmarshal methods for Protocol Buffer messages — typically 2–3× faster than the reflection-based standard implementation. ColdBrew uses vtprotobuf as its default gRPC codec with automatic fallback to standard protobuf when a message type doesn't have generated VT methods. See [vtprotobuf how-to](/howto/vtproto) for the generator setup.

## Protovalidate

[Protovalidate](https://buf.build/docs/protovalidate/overview) defines validation rules as proto annotations (`buf.validate.field`) and enforces them at runtime. ColdBrew applies validation automatically on both gRPC and HTTP requests, so a malformed payload is rejected with `InvalidArgument` before it reaches your handler. See [Interceptors how-to — Proto Validation](/howto/interceptors#proto-validation).

## OTLP

OpenTelemetry Protocol — the wire format used by [OpenTelemetry] to ship traces, metrics, and logs from your service to a collector or backend. Most modern observability stacks (Jaeger, Tempo, Honeycomb, Datadog, New Relic) accept OTLP, so configuring ColdBrew with `OTLP_ENDPOINT` keeps you portable. See [Tracing how-to](/howto/Tracing) and [Production Deployment — Distributed tracing](/howto/production#distributed-tracing).

## Trace ID

A unique identifier attached to a request that follows it across every service, log line, and span. ColdBrew generates one per request (or accepts one from the `TRACE_HEADER_NAME` HTTP header / proto `trace_id` field), propagates it through context, and adds it to every log line and span automatically. The trace ID is what makes "find every log for this user's failed checkout" tractable in a centralized log sink. See [Tracing how-to](/howto/Tracing) and [Debugging](/howto/Debugging).

## Span

A span represents one unit of work inside a trace — a function call, a database query, an outbound HTTP call. Spans nest inside each other, so a single trace becomes a tree showing where time was spent. ColdBrew exposes three helpers — `tracing.NewInternalSpan`, `tracing.NewDatastoreSpan`, `tracing.NewExternalSpan` — that create the right span type for the operation and put it in `context.Context`. See [Tracing how-to](/howto/Tracing).

## Circuit breaker

A circuit breaker watches the failure rate of an outbound call (a downstream gRPC service, a database, an external API) and "opens" — fast-fails subsequent calls — once failures exceed a threshold, giving the dependency time to recover instead of being hammered. ColdBrew exposes `interceptors.SetDefaultExecutor` so you can plug in any resilience library; [failsafe-go](https://github.com/failsafe-go/failsafe-go) is the recommended one. See [Circuit Breaker / Resilience](/integrations/#circuit-breaker--resilience) for setup and [gRPC how-to — Calling other services](/howto/gRPC#calling-other-services) for context.

## Healthcheck vs readycheck

Two HTTP endpoints with different jobs. `/healthcheck` (liveness) answers *is the process alive?* — if it fails, Kubernetes restarts the pod. `/readycheck` (readiness) answers *can it accept traffic right now?* — if it fails, Kubernetes stops routing traffic to the pod but does not restart it. During graceful shutdown, ColdBrew fails `/readycheck` first, waits the drain period, then exits — so in-flight requests finish without new ones being routed in. See [Production Deployment — Health probes](/howto/production#health-probes) and [Readiness Patterns](/howto/readiness).

## Lifecycle hooks

Optional interfaces a service can implement to run code at well-defined points in startup and shutdown: `CBPreStarter` (before servers listen), `CBPostStarter` (after they listen), `CBPreStopper` / `CBStopper` / `CBPostStopper` (during graceful shutdown), and `CBGracefulStopper.FailCheck` (toggle readiness). Use them to open and drain database pools, register/deregister with service discovery, flush buffers, and so on — without touching `core` itself. See [Shutdown Lifecycle](/howto/signals#service-lifecycle-interfaces) for the full table.

---

## Where to go next

- **[Quick Start](/getting-started)** — Generate a service from the cookiecutter and run it locally.
- **[How-To Guides](/howto)** — Task-oriented guides grouped by Build / Operate / Integrate / Advanced.
- **[Architecture](/architecture)** — How the pieces fit together end-to-end.

---
[grpc-gateway]: https://grpc-ecosystem.github.io/grpc-gateway/
[grpcurl]: https://github.com/fullstorydev/grpcurl
[grpcui]: https://github.com/fullstorydev/grpcui
[OpenTelemetry]: https://opentelemetry.io/
[vtprotobuf]: https://github.com/planetscale/vtprotobuf
221 changes: 221 additions & 0 deletions howto/cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
---
layout: default
title: "Cache"
parent: "How To"
nav_order: 23
description: "How to wire a Redis or Valkey cache into a ColdBrew service: client init in PreStart, drain in Stop, cache-aside, and tracing via NewDatastoreSpan"
---
# Cache

## Table of contents
{: .no_toc .text-delta }

1. TOC
{:toc}

---

ColdBrew is cache-agnostic — `core` does not import a Redis client and the cookiecutter does not pick one. The same lifecycle pattern as [Database](/howto/database) applies: open the client in `PreStart`, close it in `Stop`, and wrap each call with `tracing.NewDatastoreSpan`.

This page shows the framework pattern with a Redis / Valkey example using [go-redis]. Memcached or any other cache works the same way — swap the client.

## The pattern

```text
PreStart → open the client, run a Ping
Stop → close the client
NewDatastoreSpan around each call → tracing
```

Same three interfaces as the database page:

- [`CBPreStarter.PreStart(ctx) error`](https://pkg.go.dev/github.com/go-coldbrew/core#CBPreStarter)
- [`CBStopper.Stop()`](https://pkg.go.dev/github.com/go-coldbrew/core#CBStopper)
- [`tracing.NewDatastoreSpan(ctx, datastore, operation, collection)`](https://pkg.go.dev/github.com/go-coldbrew/tracing#NewDatastoreSpan)

## Redis / Valkey with go-redis

[Valkey](https://valkey.io/) is the open-source fork of Redis 7; the wire protocol is identical, so the same client library works against either.

Start the container:

```bash
make local-stack PROFILES=redis
# or, identically:
make local-stack PROFILES=valkey
```

The `redis` profile exposes port `6379`; the `valkey` profile exposes port `6380` so the two can run side by side. See [Local Development](/howto/local-dev#cache) for the full port list.

### Add the config field

The cookiecutter `config/` package embeds the framework's `cbConfig.Config` and lets you add fields with `envconfig` tags. Add the cache fields there:

```go
// config/config.go
type Config struct {
cbConfig.Config
auth.AuthConfig

RedisAddr string `envconfig:"REDIS_ADDR" required:"true"`
RedisPoolSize int `envconfig:"REDIS_POOL_SIZE" default:"20"`
}
```

Set the value the same way as any other env var:

```bash
export REDIS_ADDR=localhost:6379
```

### Wire the client

```go
package svc

import (
"context"
"fmt"
"time"

"github.com/go-coldbrew/core"
"github.com/redis/go-redis/v9"

"myapp/config" // import path of your service's config package
)

type Service struct {
cache *redis.Client
}

var (
_ core.CBPreStarter = (*Service)(nil)
_ core.CBStopper = (*Service)(nil)
)

func (s *Service) PreStart(ctx context.Context) error {
cfg := config.Get()
s.cache = redis.NewClient(&redis.Options{
Addr: cfg.RedisAddr,
DialTimeout: 2 * time.Second,
ReadTimeout: 500 * time.Millisecond,
WriteTimeout: 500 * time.Millisecond,
PoolSize: cfg.RedisPoolSize,
MinIdleConns: 2,
})
if err := s.cache.Ping(ctx).Err(); err != nil {
s.cache.Close()
return fmt.Errorf("redis ping: %w", err)
}
return nil
}

func (s *Service) Stop() {
if s.cache != nil {
s.cache.Close()
}
}
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

A failed Ping at startup returns from `PreStart`, which aborts the whole service — exactly the right behaviour when the cache is required. If the cache is **optional** (cache-aside, see below), log the error and proceed; treat cache misses as the empty case.

### Trace each call

Same `NewDatastoreSpan` helper as a database query, with `"redis"` as the datastore:

```go
func (s *Service) GetSession(ctx context.Context, sid string) (*Session, error) {
span, ctx := tracing.NewDatastoreSpan(ctx, "redis", "GET", "session")
defer span.End()
span.SetTag("session_id", sid)

raw, err := s.cache.Get(ctx, "session:"+sid).Bytes()
if err == redis.Nil {
return nil, nil // miss — caller decides what "not found" means
}
if err != nil {
span.SetError(err)
return nil, err
}
var sess Session
if err := proto.Unmarshal(raw, &sess); err != nil {
span.SetError(err)
return nil, err
}
return &sess, nil
}
```

`redis.Nil` is the **expected** "key not found" sentinel — surface it as a miss, not an error, so a cache miss doesn't show up as a failure in your error rate.

## Cache-aside pattern

Cache-aside (lazy population) is the right default for most read-heavy workloads: try the cache first, fall back to the source of truth on a miss, populate the cache on the way back, and tolerate the cache being down.

```go
func (s *Service) GetUser(ctx context.Context, id int64) (*User, error) {
key := fmt.Sprintf("user:%d", id)

// 1. Try the cache. A failure here is logged and treated as a miss —
// the source of truth still works.
if u, err := s.cacheGetUser(ctx, key); err == nil && u != nil {
return u, nil
} else if err != nil {
log.GetLogger(ctx).Warn("cache get failed", "err", err)
}

// 2. Source of truth.
u, err := s.dbGetUser(ctx, id)
if err != nil || u == nil {
return u, err
}

// 3. Populate the cache. Failure to populate is non-fatal.
if err := s.cacheSetUser(ctx, key, u); err != nil {
log.GetLogger(ctx).Warn("cache set failed", "err", err)
}
return u, nil
}
```

Two principles to keep in mind:

- **Cache failures must not fail the request.** A degraded cache should turn into higher database load, not 5xx errors.
- **Pick a TTL up front, not by accident.** `SET key value EX 300` (5 minutes) for human-scale data; longer for immutable data; explicit `Del` on writes for anything that *must* invalidate. Avoid relying on memory pressure for eviction — the data you need evicted first is rarely the data Redis evicts first.

## Invalidation

Cache invalidation is the hard part. Two patterns work in practice for ColdBrew services:

- **Write-through invalidation.** When the source of truth changes, the same handler explicitly `Del`s the cache key. Simple, correct as long as you remember to do it everywhere.
- **TTL-based.** Set a short TTL and accept stale data within that window. Trivially correct; the trade-off is staleness.

Pub/sub-based invalidation across replicas is possible but brittle in a microservice setting. If you find yourself reaching for it, consider whether your service should own the cache at all, or whether a CDN / fronting service is the better tool.

## Local stack profiles

| Profile | Service | Port |
|---|---|---|
| `redis` | Redis 8 | 6379 |
| `valkey` | Valkey 8 (Redis-compatible) | 6380 |
| `memcached` | Memcached | 11211 |

Comment thread
ankurs marked this conversation as resolved.
See [Local Development](/howto/local-dev) for the full profile list.

## Other caches

- **Memcached** — use [bradfitz/gomemcache] or [rainycape/memcache]. Same `PreStart`/`Stop` pattern; client is a value, no explicit pool. Use `tracing.NewDatastoreSpan(ctx, "memcached", "GET", key)`.
- **In-process / LRU** — [hashicorp/golang-lru] or stdlib `sync.Map` with eviction. No `PreStart` needed (just construct in your service factory). Tracing is optional since the call doesn't cross process boundaries.
- **Multi-tier (in-process → Redis)** — wrap two clients behind one interface. Trace each tier separately so you can see the hit ratio at each level.

## Related

- [Database](/howto/database) — Same lifecycle pattern, different client.
- [Tracing](/howto/Tracing) — How `NewDatastoreSpan` fits into the broader tracing model.
- [Local Development](/howto/local-dev) — All local-stack profiles.
- [Shutdown Lifecycle](/howto/signals) — Full lifecycle interface table.

[go-redis]: https://github.com/redis/go-redis
[bradfitz/gomemcache]: https://github.com/bradfitz/gomemcache
[rainycape/memcache]: https://github.com/rainycape/memcache
[hashicorp/golang-lru]: https://github.com/hashicorp/golang-lru
Loading
Loading