Implement in-memory Store with typed Mutation interface and pre-validation #9

Closed
opened 2026-06-23 08:08:14 +00:00 by sscoble · 1 comment
Owner

Why

The hot store is in-memory (posts#1 decision). All writes go through a typed Mutation whose Validate() runs without holding a lock; only validated mutations acquire a lock to Apply(). The lock-region is adaptive (see #10).

Files

  • api/store.go (refactor — drop lib/pq; in-memory maps + indexes)
  • api/mutation.go (new — Mutation interface, Validate/Apply split)
  • api/mutations_*.go (one file per mutation: create_post.go, add_comment.go, bump_post.go, etc.)
  • api/index.go (new — sorted indexes, postsByBumpedAt, commentsByPostPath, mentionsByMentioned)

Mutation shape

type Mutation interface {
    Validate(s *Store) error         // preconditions, no lock
    Apply(s *Store, lock LockRegion) error  // holds the lock
    AffectedAuthors() []string       // for read_state, mentions
}

Each mutation is a typed struct (not a string-discriminated union). Validate is deterministic and side-effect-free. Apply is the only place that mutates state. The lock parameter is the actual lock acquired, so Apply can release it explicitly if needed (rare).

Store state (in-memory)

type Store struct {
    mu sync.RWMutex  // base lock; per-author/per-post locks layered on top
    posts     map[string]*Post
    comments  map[string]*Comment
    authors   map[string]*Author
    channels  map[string]*Channel
    postChannels map[string][]string  // post_id -> channels
    mentions  map[string][]*Mention   // mentioned -> mentions
    readState map[[2]string]time.Time

    // Indexes
    postsByBumpedAt  *Index[*Post]    // sorted DESC by bumped_at
    commentsByPostPath map[string]*Index[*Comment]  // post_id -> sorted by path

    // Adaptive lock state
    lockCtrl *LockController

    // Cold-flush plumbing
    flushCh chan struct{}
    flushInterval time.Duration

    // Injected for testability
    now func() time.Time
    newID func() string
}

Memory management (Scott: "we need to be very careful")

  • sync.Pool for *Post, *Comment — high churn on comment creation
  • Bounded slice growth on commentsByPostPath (cap at e.g. 10k comments per post; spill to cold)
  • unsafe.Sizeof walk for Store.MemUsage() — feeds #11's measurement
  • intern.Get for repeated values (channel slugs, author IDs) — fewer allocs

Acceptance

  • All 13 v0 endpoints in docs/API.md work against the in-memory store
  • Validate runs without acquiring any application lock (only the immutable types)
  • Apply is the only path that mutates state
  • VOPR tests: 1000 seeded runs, identical state
  • Store.MemUsage() reports live bytes per type
  • go test ./... passes; race detector clean (go test -race ./...)
## Why The hot store is in-memory (posts#1 decision). All writes go through a typed `Mutation` whose `Validate()` runs without holding a lock; only validated mutations acquire a lock to `Apply()`. The lock-region is adaptive (see #10). ## Files - `api/store.go` (refactor — drop lib/pq; in-memory maps + indexes) - `api/mutation.go` (new — `Mutation` interface, `Validate`/`Apply` split) - `api/mutations_*.go` (one file per mutation: `create_post.go`, `add_comment.go`, `bump_post.go`, etc.) - `api/index.go` (new — sorted indexes, `postsByBumpedAt`, `commentsByPostPath`, `mentionsByMentioned`) ## `Mutation` shape ```go type Mutation interface { Validate(s *Store) error // preconditions, no lock Apply(s *Store, lock LockRegion) error // holds the lock AffectedAuthors() []string // for read_state, mentions } ``` Each mutation is a typed struct (not a string-discriminated union). Validate is deterministic and side-effect-free. Apply is the only place that mutates state. The lock parameter is the actual lock acquired, so Apply can release it explicitly if needed (rare). ## Store state (in-memory) ```go type Store struct { mu sync.RWMutex // base lock; per-author/per-post locks layered on top posts map[string]*Post comments map[string]*Comment authors map[string]*Author channels map[string]*Channel postChannels map[string][]string // post_id -> channels mentions map[string][]*Mention // mentioned -> mentions readState map[[2]string]time.Time // Indexes postsByBumpedAt *Index[*Post] // sorted DESC by bumped_at commentsByPostPath map[string]*Index[*Comment] // post_id -> sorted by path // Adaptive lock state lockCtrl *LockController // Cold-flush plumbing flushCh chan struct{} flushInterval time.Duration // Injected for testability now func() time.Time newID func() string } ``` ## Memory management (Scott: "we need to be very careful") - `sync.Pool` for `*Post`, `*Comment` — high churn on comment creation - Bounded slice growth on `commentsByPostPath` (cap at e.g. 10k comments per post; spill to cold) - `unsafe.Sizeof` walk for `Store.MemUsage()` — feeds #11's measurement - `intern.Get` for repeated values (channel slugs, author IDs) — fewer allocs ## Acceptance - All 13 v0 endpoints in `docs/API.md` work against the in-memory store - `Validate` runs without acquiring any application lock (only the immutable types) - `Apply` is the only path that mutates state - VOPR tests: 1000 seeded runs, identical state - `Store.MemUsage()` reports live bytes per type - `go test ./...` passes; race detector clean (`go test -race ./...`)
sscoble added the
area/api
kind/feature
prio/p0
labels 2026-06-23 08:08:14 +00:00

✅ Acceptance Verification — c2326 (2026-07-30)

Cycle: c2326 cron dispatch closure check
Verdict: PASS — implementation complete, all tests pass.

In-memory Store + typed Mutation interface — mutation.go defines Mutation interface (Validate/Apply/AffectedAuthors/Kind). store.go implements in-memory store with sync.Pool, string interning, bounded comment index. go test ./... PASS. Implementation complete.

Closing as complete.

## ✅ Acceptance Verification — c2326 (2026-07-30) **Cycle:** c2326 cron dispatch closure check **Verdict:** PASS — implementation complete, all tests pass. In-memory Store + typed Mutation interface — mutation.go defines Mutation interface (Validate/Apply/AffectedAuthors/Kind). store.go implements in-memory store with sync.Pool, string interning, bounded comment index. go test ./... PASS. Implementation complete. Closing as complete.
Sign in to join this conversation.
No description provided.