Skip to content
Closed
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
38 changes: 38 additions & 0 deletions docs/design/2026-09-02-refactor-bitmap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Refactor ModeBitmap layout (no contract change)

**Status:** approved (chat: continue after HLL)
**Branch:** `feat/refactor-bitmap`
**Date:** 2026-09-02

## Problem

Bitmap store methods still live in `pkg/store/memory.go`. Engine Bitmap is one ~250-line file mixing public verbs, owner apply, and cluster hops.

## Non-goals

- No API / proto / flag / version / fan-out / hintID change
- No `bitmapx` algorithm change
- Not extracting other modes (HLL is a separate PR)

## Contract

Unchanged: `BitSet` / `BitGet` / `BitCount` / `BitPos` / `Delete(name)`, `FlagBitmap` + inbox `FlagBitmapSet`, snapshot fan-out, version under store mutex, present-bit, empty-until-delete.

## Approach

Same file layout as HLL / TopK / CMS:

| Before | After |
|--------|--------|
| `pkg/store/memory.go` B* | `pkg/store/bitmap.go` |
| `pkg/engine/bitmap.go` (all) | `bitmap.go` public verbs; `bitmap_apply.go` owner write; `bitmap_cluster.go` inbox/fan-out/GetOrLoad |

Rejected: rewrite bit packing; split `bitmapx` (150 lines is one file).

## Tests (already exist; keep them)

Existing `pkg/bitmapx`, `pkg/store` bitmap, `pkg/engine` bitmap unit + cluster + hint-after-down.

## Bench risk

No new Get/Peek logic. Local `BenchmarkEngineGetHit` / `BenchmarkStoreGetHit` allocs/op must stay 15 / 2.
1 change: 1 addition & 0 deletions docs/design/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Do not implement from a draft.
| [2026-08-25-mode-hll.md](./2026-08-25-mode-hll.md) | `ModeHLL` |
| [2026-08-31-mode-topk.md](./2026-08-31-mode-topk.md) | `ModeTopK` |
| [2026-09-01-mode-cms.md](./2026-09-01-mode-cms.md) | `ModeCMS` |
| [2026-09-02-refactor-bitmap.md](./2026-09-02-refactor-bitmap.md) | ModeBitmap file layout (no contract) |
| [2026-08-25-list-counter-version.md](./2026-08-25-list-counter-version.md) | List/Counter snapshot version |
| [2026-08-13-unify-grpc-error-map.md](./2026-08-13-unify-grpc-error-map.md) | grpcmap |

Expand Down
104 changes: 1 addition & 103 deletions pkg/engine/bitmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import (

"github.com/Code0987/supercache/pkg/bitmapx"
"github.com/Code0987/supercache/pkg/keyspace"
"github.com/Code0987/supercache/pkg/store"
)

// BitSet writes a bit on a ModeBitmap (Redis SETBIT). ACK-only.
// Non-owners forward an inbox FlagBitmapSet; the owner applies and fans a snapshot.
func (e *Engine) BitSet(ctx context.Context, keyspaceName, name string, offset uint64, bit bool) error {
if err := ctx.Err(); err != nil {
return err
Expand Down Expand Up @@ -144,105 +144,3 @@ func (e *Engine) bitmapNeed(ks *ksRuntime, offset uint64) error {
}
return nil
}

func (e *Engine) bMutViaOwner(ctx context.Context, ks *ksRuntime, name string, value []byte) error {
c := e.clusterSnapshot()
owner, _ := c.Ring.Owner(name)
ent := store.Entry{Value: value, Flags: store.FlagBitmapSet, Version: 1}
pctx, cancel := e.peerCtx(ctx, ks)
defer cancel()
applied, err := c.Transport.ApplyPut(pctx, owner.Addr, ks.cfg.Name, name, ent, c.Ring.Generation())
if err != nil {
return err
}
if !applied {
return fmt.Errorf("%w: bitmap set rejected", ErrInvalidArgument)
}
return nil
}

func (e *Engine) bSetLocal(ks *ksRuntime, name string, offset uint64, bit bool) error {
expire := e.expireAt(ks.cfg.TTL)
max := e.maxValueSize
if ks.cfg.MaxValueSize > 0 {
max = ks.cfg.MaxValueSize
}
cur, _ := ks.store.PeekVersion(name)
gate := cur + 1
applied, tooLarge := ks.store.BSet(name, offset, bit, gate, expire, max)
if tooLarge {
return ErrValueTooLarge
}
if !applied {
return fmt.Errorf("%w: bitmap set rejected", ErrInvalidArgument)
}
ver, _ := ks.store.PeekVersion(name)
ks.observeVersion(name, ver)
e.bReplicateSnapshot(ks, name, ver, expire)
return nil
}

func (e *Engine) bReplicateSnapshot(ks *ksRuntime, name string, ver uint64, expire int64) {
ent, ok := ks.store.Peek(name)
if !ok || !ent.IsBitmap() {
return
}
e.replicate(ks.cfg.Name, name, store.Entry{
Value: ent.Value,
Version: ver,
ExpireAt: expire,
Flags: store.FlagBitmap,
}, false)
}

func (e *Engine) bFetchOwner(ctx context.Context, ks *ksRuntime, name string) (store.Entry, bool, error) {
c := e.clusterSnapshot()
if c == nil || c.Ring == nil || c.Transport == nil {
return store.Entry{}, false, nil
}
owner, ok := c.Ring.Owner(name)
if !ok || owner.ID == "" || owner.ID == c.SelfID || owner.Addr == "" {
return store.Entry{}, false, nil
}
pctx, cancel := e.peerCtx(ctx, ks)
defer cancel()
res, err := c.Transport.GetOrLoad(pctx, owner.Addr, ks.cfg.Name, name)
if err != nil || !res.Found || !res.Entry.IsBitmap() {
return store.Entry{}, false, nil
}
if e.holdsReplica(c, ks, name) {
_ = ks.store.BInstall(name, res.Entry.Value, res.Entry.Version, res.Entry.ExpireAt)
}
return res.Entry, true, nil
}

func (e *Engine) applyBitmapSet(ks *ksRuntime, name string, inbox []byte, expireAt int64) bool {
offset, bit, err := bitmapx.DecodeSet(inbox)
if err != nil {
return false
}
if expireAt == 0 {
expireAt = e.expireAt(ks.cfg.TTL)
}
max := e.maxValueSize
if ks.cfg.MaxValueSize > 0 {
max = ks.cfg.MaxValueSize
}
cur, _ := ks.store.PeekVersion(name)
gate := cur + 1
ok, tooLarge := ks.store.BSet(name, offset, bit, gate, expireAt, max)
if !ok || tooLarge {
return false
}
ver, _ := ks.store.PeekVersion(name)
ks.observeVersion(name, ver)
e.bReplicateSnapshot(ks, name, ver, expireAt)
return true
}

func (e *Engine) applyBitmapInstall(ks *ksRuntime, name string, blob []byte, version uint64, expireAt int64) bool {
if expireAt == 0 {
expireAt = e.expireAt(ks.cfg.TTL)
}
return ks.store.BInstall(name, blob, version, expireAt)
}
64 changes: 64 additions & 0 deletions pkg/engine/bitmap_apply.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package engine

import (
"fmt"

"github.com/Code0987/supercache/pkg/bitmapx"
)

// bSetLocal is the owner / single-node write path.
// The store assigns the stored version; we fan PeekVersion after the write.
func (e *Engine) bSetLocal(ks *ksRuntime, name string, offset uint64, bit bool) error {
expire := e.expireAt(ks.cfg.TTL)
max := e.maxValueSize
if ks.cfg.MaxValueSize > 0 {
max = ks.cfg.MaxValueSize
}
cur, _ := ks.store.PeekVersion(name)
gate := cur + 1
applied, tooLarge := ks.store.BSet(name, offset, bit, gate, expire, max)
if tooLarge {
return ErrValueTooLarge
}
if !applied {
return fmt.Errorf("%w: bitmap set rejected", ErrInvalidArgument)
}
ver, _ := ks.store.PeekVersion(name)
ks.observeVersion(name, ver)
e.bReplicateSnapshot(ks, name, ver, expire)
return nil
}

// applyBitmapSet is the owner-inbox ApplyPut of FlagBitmapSet.
// Non-owners must not reach here (ApplyPut returns applied=false first).
func (e *Engine) applyBitmapSet(ks *ksRuntime, name string, inbox []byte, expireAt int64) bool {
offset, bit, err := bitmapx.DecodeSet(inbox)
if err != nil {
return false
}
if expireAt == 0 {
expireAt = e.expireAt(ks.cfg.TTL)
}
max := e.maxValueSize
if ks.cfg.MaxValueSize > 0 {
max = ks.cfg.MaxValueSize
}
cur, _ := ks.store.PeekVersion(name)
gate := cur + 1
ok, tooLarge := ks.store.BSet(name, offset, bit, gate, expireAt, max)
if !ok || tooLarge {
return false
}
ver, _ := ks.store.PeekVersion(name)
ks.observeVersion(name, ver)
e.bReplicateSnapshot(ks, name, ver, expireAt)
return true
}

// applyBitmapInstall is replica / handoff ApplyPut of FlagBitmap (LWW replace).
func (e *Engine) applyBitmapInstall(ks *ksRuntime, name string, blob []byte, version uint64, expireAt int64) bool {
if expireAt == 0 {
expireAt = e.expireAt(ks.cfg.TTL)
}
return ks.store.BInstall(name, blob, version, expireAt)
}
65 changes: 65 additions & 0 deletions pkg/engine/bitmap_cluster.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package engine

import (
"context"
"fmt"

"github.com/Code0987/supercache/pkg/store"
)

// bMutViaOwner sends an inbox FlagBitmapSet to the ring owner.
// ACK-only: a rejected apply surfaces as InvalidArgument (no return payload).
func (e *Engine) bMutViaOwner(ctx context.Context, ks *ksRuntime, name string, value []byte) error {
c := e.clusterSnapshot()
owner, _ := c.Ring.Owner(name)
ent := store.Entry{Value: value, Flags: store.FlagBitmapSet, Version: 1}
pctx, cancel := e.peerCtx(ctx, ks)
defer cancel()
applied, err := c.Transport.ApplyPut(pctx, owner.Addr, ks.cfg.Name, name, ent, c.Ring.Generation())
if err != nil {
return err
}
if !applied {
return fmt.Errorf("%w: bitmap set rejected", ErrInvalidArgument)
}
return nil
}

// bReplicateSnapshot fans the post-write FlagBitmap blob to RF−1 replicas.
// hintID is (ks, name), so a later snapshot replaces a pending hint — both bits stay.
func (e *Engine) bReplicateSnapshot(ks *ksRuntime, name string, ver uint64, expire int64) {
ent, ok := ks.store.Peek(name)
if !ok || !ent.IsBitmap() {
return
}
e.replicate(ks.cfg.Name, name, store.Entry{
Value: ent.Value,
Version: ver,
ExpireAt: expire,
Flags: store.FlagBitmap,
}, false)
}

// bFetchOwner loads a missing local name from the owner (GetOrLoad).
// RPC / !Found / wrong type → miss + nil error (do not return Unavailable).
// Replicas may install the snapshot; non-replicas do not.
func (e *Engine) bFetchOwner(ctx context.Context, ks *ksRuntime, name string) (store.Entry, bool, error) {
c := e.clusterSnapshot()
if c == nil || c.Ring == nil || c.Transport == nil {
return store.Entry{}, false, nil
}
owner, ok := c.Ring.Owner(name)
if !ok || owner.ID == "" || owner.ID == c.SelfID || owner.Addr == "" {
return store.Entry{}, false, nil
}
pctx, cancel := e.peerCtx(ctx, ks)
defer cancel()
res, err := c.Transport.GetOrLoad(pctx, owner.Addr, ks.cfg.Name, name)
if err != nil || !res.Found || !res.Entry.IsBitmap() {
return store.Entry{}, false, nil
}
if e.holdsReplica(c, ks, name) {
_ = ks.store.BInstall(name, res.Entry.Value, res.Entry.Version, res.Entry.ExpireAt)
}
return res.Entry, true, nil
}
Loading
Loading