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
89 changes: 89 additions & 0 deletions .agents/skills/go-memoize-package/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
name: go-memoize-package
description: Work on the go_memoize Go package. Use when changing direct memoization, the root cache engine, stores, background refresh, loader snapshots, metrics, benchmarks, profiling docs, package docs, package README, or package examples.
license: Apache-2.0
compatibility: Requires Go and git. Run verification from the repository root.
---

# go_memoize Package

## Purpose

Use this skill to make correct package-level decisions in `go_memoize`. The root package is the only public module and contains direct function memoization plus the cache engine with stores, stale refresh, metrics, background values, and loaders.

## First Decision

Choose the API and store before editing:

| User Need | Use |
|---|---|
| Simple function memoization with comparable args | Root package `Memoize`, `Memoize1` ... `Memoize7`, or `E`/`Ctx` variants |
| Explicit cache key, TTL, stale refresh, metrics | `memoize.New[K,V]` and `memoize.Cache[K,V]` |
| One logical value or one hot key | `memory.NewSingle[K,V]()` or `background.Keep` |
| Many bounded in-memory keys | `memory.New[K,V](capacity)` |
| Many concurrent supported primitive keys | `memory.NewSharded[K,V](capacity)` |
| Two-tier cache | `chain.New[K,V](l1, l2)` |
| File-backed local cache | `local.New[V](dir)` |
| One writer publishes a full snapshot to many readers | Writer: `background.Keep` + `background.WriteThrough`; readers: `background.Mirror` or `background.MustMirror` |
| Readiness-gated periodic local value | `loader.New` |

## Public API Rules

- Root public module only: `github.com/agkloop/go_memoize`. Do not add public import examples using legacy module paths or helper packages.
- Direct memoizers use non-generic `memoize.Opts()` and return `(func, error)`.
- Direct memoizers hash comparable function arguments to `uint64`; custom direct stores must be `memoize.Store[uint64,V]`.
- Explicit caches use typed keys through `memoize.New[K,V]`; production examples usually use `K=string` for business keys.
- Error-returning direct memoizers use the root cache engine and support stale-on-error through `WithStaleTTL(...).KeepStaleOnError()`.
- `memoize.New[K,V]` has no default store and no default expiration policy. Choose `WithStore` plus `WithTTL`, `NoExpiration`, or `Bypass`.
- Direct memoizers create an internal unbounded `Store[uint64,V]` when `WithStore` is omitted, but still require `WithTTL`, `NoExpiration`, or `Bypass`.

## Cache Invariants

- `memory.New[K,V](capacity)` is exact LRU by default and stores key type `K` directly.
- `memory.NewSharded[K,V](capacity)` improves distributed-key concurrency for supported primitive key types. It does not reduce contention for one hot key; one key maps to one shard.
- `memory.NewSingle[K,V]()` is read-mostly and avoids LRU/hash overhead for one logical value.
- Stores persist raw `memoize.Stored[V]` envelopes. The cache engine owns fresh, stale, and expired decisions.
- Built-in stores may expose private fast paths used by the cache engine. Do not document those as public extension points.
- `WithGetRecencySample(n)` makes direct `Store.Get` recency approximate when `n > 1`.
- Metrics use one public method: `RecordMetric(memoize.MetricEvent)`.
- `background.Keep` and `loader.New` share internal periodic refresh-loop infrastructure. Cache stale refresh uses cache flight machinery, not the shared refresh loop.
- Source compatibility may change for performance or clarity; users can pin module versions.
- Keep core code standard-library-only unless the user explicitly approves a dependency.

## Background And Loader Semantics

- `background.Keep` is producer-side: it calls `fn` immediately, stores the value in local process memory, then refreshes on the interval.
- `background.WriteThrough(key, store)` publishes every successful `Keep` refresh to a shared `memoize.Store[string,V]` as a no-expiration snapshot.
- `background.Mirror` is reader-side: it reads one shared `Store[string,V]` key immediately, copies `entry.Value` into local process memory, then polls the store on the interval.
- `background.MustMirror` is `Mirror` for startup paths; it panics when the initial remote read fails.
- `background.Value.Get()` is an atomic local memory read. It does not call Redis, SQL, MySQL, S3, or any remote dependency; it does not block and does not return an error.
- Values returned by `Value.Get()` are shared memory. Keep them immutable, or copy maps, slices, and pointer-heavy fields before mutation.
- `loader.New` is readiness-oriented: callers use `Value(ctx)` to block until the first successful load, then read the latest loaded value after readiness.

## Docs Sync Rule

- When changing public docs, examples, README, API behavior, store behavior, background/loader semantics, metrics, benchmarks, or production recommendations, update this skill in the same change if future agents need to know the new rule.
- Run `scripts/check-docs-skill-sync.sh` before finishing docs-heavy work. The repo also includes `.githooks/pre-commit` for teams that opt in with `git config core.hooksPath .githooks`.

## Verification

Run checks from the repository root:

```sh
go test ./... -count=1
go test ./... -race -count=1
```

Benchmark cache/store work with an explicit command and record the result:

```sh
go test ./benchmarks/ -bench=. -benchmem -benchtime=1s -count=1
```

## Gotchas

- Do not optimize single-hot-key workloads by adding more LRU tuning; use `memory.NewSingle`.
- Do not claim sharding fixes single-key contention; one key maps to one shard.
- Do not document legacy module paths or helper-package imports in public examples.
- Do not update profiling docs without the exact benchmark command and observed numbers.
- Do not add behavior changes without tests first.
68 changes: 68 additions & 0 deletions .agents/skills/go-performance-optimization/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
name: go-performance-optimization
description: Use when optimizing Go hot paths, reducing allocations, investigating slow benchmarks, reading pprof output, checking escape analysis, or applying goperf.dev/go-optimization-guide patterns to Go code.
license: Apache-2.0
compatibility: Requires Go tooling, benchmarks, pprof, and git.
---

# Go Performance Optimization

## Overview

Optimize from measurements, not folklore. Apply `goperf.dev` patterns only when they fit the observed bottleneck and keep the code simpler or measurably faster.

## Workflow

1. Establish a focused benchmark with `go test -bench ... -benchmem -count=1`.
2. Profile before changing code when the cause is not obvious: add `-cpuprofile` and `-memprofile`, then inspect with `go tool pprof -top`.
3. Use escape analysis for allocation questions: `go test -gcflags=-m=2 ./path`.
4. Rank hypotheses by expected impact and risk.
5. Change one variable at a time.
6. Re-run the same benchmark and compare `ns/op`, `B/op`, and `allocs/op`.
7. Run the package tests, then the repository verification command.

## Applicable Patterns

| Symptom | Prefer | Avoid |
|---|---|---|
| Hot-path allocations | Remove closure/interface/string formatting escapes | `sync.Pool` by default |
| Growing slices/maps | Preallocate with known or bounded capacity | Letting hot buffers resize repeatedly |
| Interface boxing in loops | Generics or concrete types | `any`, `interface{}`, `fmt` conversions on hot paths |
| Large dense structs | Field alignment and locality checks | Reordering public structs without compatibility review |
| Lock contention | Sharding, read-mostly stores, atomics for simple counters | Atomics for multi-step invariants |
| Context overhead | Keep context at API boundaries and miss/IO paths | Creating timeout contexts on hot hits |
| GC pressure | Fewer heap objects and shorter object lifetimes | Pooling tiny or long-lived objects |

## Decision Rules

- Keep benchmarks representative: one-hot-key, distributed-key, hit, miss, stale, and parallel workloads have different bottlenecks.
- Do not optimize exact LRU by replacing its mutex with atomics; linked-list/index/map updates are one invariant.
- Use `sync.Pool` only for reusable temporary objects that allocate heavily, such as buffers or encoders.
- Remove `fmt.Sprint`, `fmt.Sprintf`, and `any` conversions from hot paths when a typed alternative exists.
- Keep error handling and singleflight off hot-hit paths when behavior allows a separate fast path.
- Prefer shallow, explicit APIs over hidden conversions.

## Verification

Run the benchmark that motivated the change and record before/after numbers:

```sh
go test ./benchmarks/ -bench='BenchmarkName$' -benchmem -benchtime=1s -count=1 -run '^$'
```

Run repository checks from the root:

```sh
git diff --check
go test ./... -count=1
go test ./... -race -count=1
```

## Common Mistakes

- Claiming performance improved without fresh benchmark output.
- Optimizing a benchmark-only workload while slowing the production path.
- Adding `sync.Pool` when the real allocation is closure escape or interface boxing.
- Using `fmt` for generic keys in cache/store hot paths.
- Treating sharding as a fix for one hot key; one key still maps to one shard.
- Ignoring `B/op` and `allocs/op` because `ns/op` moved slightly.
4 changes: 4 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
set -eu

scripts/pre-commit.sh
32 changes: 19 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ permissions:
contents: read

jobs:
build:
verify:
runs-on: ubuntu-latest

steps:
Expand All @@ -22,15 +22,21 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: stable

- name: Run tests
run: go test -v ./...

- name: Check code formatting
run: gofmt -l .

- name: Run static analysis
uses: golangci/golangci-lint-action@v6
with:
version: v1.64
# Use a patched toolchain for security scanning; go.mod still defines
# the module compatibility target.
go-version: "1.25.10"
check-latest: true
cache: true
cache-dependency-path: |
go.sum
adapters/redis/go.sum

- name: Install security and lint tools
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest

- name: Run full verification suite
env:
REQUIRE_OPTIONAL_TOOLS: "1"
run: scripts/verify-ci-local.sh
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,6 @@ debug/ # Common directory for debug artifacts
__debug_bin*
qodana.yaml
cmd
testing
testing
.opencode
docs/superpowers
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Agent Skills

Use `.agents/skills/go-memoize-package/SKILL.md` when working on this repository.
Use `.agents/skills/go-performance-optimization/SKILL.md` when optimizing Go hot paths, benchmarks, allocation profiles, pprof output, or escape analysis.

Key reminders:

- Root package is the only public module and contains both direct memoization and the cache engine.
- Do not reintroduce `/v2` or `helpers` imports; users can pin module versions.
- Run verification from the repository root: `go test ./... -count=1` and `go test ./... -race -count=1`.
- Pick the memory store by workload: `memory.New` for many-key LRU, `memory.NewSharded` for distributed-key concurrency, `memory.NewSingle` for one logical value.
25 changes: 25 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Package Context

This package has one public Module, `github.com/agkloop/go_memoize`. It contains two primary user-facing Modules: direct memoization for functions and an explicit cache engine for keyed caching.

## Domain Vocabulary

Direct memoization is the Shallow Interface for common function caching. Users pass a function plus a TTL, and the Implementation derives keys from comparable arguments. Use it when Locality matters more than policy control: in-process values, no custom Store Adapter, no stale-while-revalidate, and no explicit metrics wiring.

The cache engine is the Deep Module. Its public Interface is small: `New`, `Cache.Get`, `Cache.Set`, `Cache.Delete`, `Cache.Clear`, `Cache.GetOrCompute`, and `Cache.Stop`. Its Implementation owns TTL policy, expiration ownership, stale-while-revalidate, flight coalescing, fallback on refresh error, and metrics emission. This Depth gives users Leverage because Store Adapters can stay simple while cache behavior remains consistent.

A Store Adapter implements the `Store[K,V]` Interface: `Get`, `Set`, `Delete`, and `Clear`. Stores persist and return raw `Stored[V]` entries. They do not decide whether an entry is fresh, stale, or expired for public cache reads. That expiration ownership belongs to the cache engine. Store-specific cleanup TTLs, such as Redis key expiry, are backend cleanup, not the public freshness policy.

`Stored[V]` is the cache entry envelope. It carries `Value`, `CreatedAt`, `FreshUntil`, `StaleUntil`, `NoExpire`, `Version`, and `Tags`. The cache engine interprets the envelope; stores preserve it.

Stale-while-revalidate means a cache hit can return an entry after `FreshUntil` but before `StaleUntil`, then refresh in the background. If configured with stale-on-error behavior, the cache can keep serving the stale value when recompute fails.

A background value is one locally served value refreshed on a schedule through `background.Keep` or mirrored from a store through `background.Mirror`. It is best for config snapshots, feature flags, rates, and other one-value workloads.

A loader is a periodic refresh Module exposed by `loader.New`. It retries until the first successful load, then `Value(ctx)` returns the latest successful value. Loader and background share an internal periodic refresh Implementation, but expose different public Interfaces for different use cases.

Metrics use one event Interface: `Metrics.RecordMetric(MetricEvent)`. `MetricEventKind` identifies hits, misses, stale hits, refresh start/success/error, set, and delete. `Duration` is meaningful for refresh success latency. `Err` is meaningful for refresh errors.

Private cache/store fast paths are Seams inside the Implementation. They let optimized stores provide fresh-value or peek behavior without making the public Store Interface larger. These Seams are tested through observable behavior and deletion tests: removing a private fast path test should reveal whether the Seam still protects policy Locality and avoids accidental coupling.

Test surface should follow Module boundaries. Public behavior tests cover direct memoization, cache policy, and Store Adapter contracts. Private fast-path tests should stay narrow and verify observable effects, not leak private names into user documentation.
Loading
Loading