···11+# Carstore
22+33+Store a zillion users of PDS-like repo, with more limited operations (mainly: firehose in, firehose out).
44+55+## [ScyllaStore](scylla.go)
66+77+Blocks stored in ScyllaDB.
88+User and PDS metadata stored in gorm (PostgreSQL or sqlite3).
99+1010+## [FileCarStore](bs.go)
1111+1212+Store 'car slices' from PDS source subscribeRepo firehose streams to filesystem.
1313+Store metadata to gorm postgresql (or sqlite3).
1414+Periodic compaction of car slices into fewer larger car slices.
1515+User and PDS metadata stored in gorm (PostgreSQL or sqlite3).
1616+FileCarStore was the first production carstore and used through at least 2024-11.
1717+1818+## [SQLiteStore](sqlite_store.go)
1919+2020+Experimental/demo.
2121+Blocks stored in trivial local sqlite3 schema.
2222+Minimal reference implementation from which fancy scalable/performant implementations may be derived.
2323+2424+```sql
2525+CREATE TABLE IF NOT EXISTS blocks (uid int, cid blob, rev varchar, root blob, block blob, PRIMARY KEY(uid,cid))
2626+CREATE INDEX IF NOT EXISTS blocx_by_rev ON blocks (uid, rev DESC)
2727+2828+INSERT INTO blocks (uid, cid, rev, root, block) VALUES (?, ?, ?, ?, ?) ON CONFLICT (uid,cid) DO UPDATE SET rev=excluded.rev, root=excluded.root, block=excluded.block
2929+3030+SELECT rev, root FROM blocks WHERE uid = ? ORDER BY rev DESC LIMIT 1
3131+3232+SELECT cid,rev,root,block FROM blocks WHERE uid = ? AND rev > ? ORDER BY rev DESC
3333+3434+DELETE FROM blocks WHERE uid = ?
3535+3636+SELECT rev, root FROM blocks WHERE uid = ? AND cid = ? LIMIT 1
3737+3838+SELECT block FROM blocks WHERE uid = ? AND cid = ? LIMIT 1
3939+4040+SELECT length(block) FROM blocks WHERE uid = ? AND cid = ? LIMIT 1
4141+```
+54-81
carstore/bs.go
···1010 "os"
1111 "path/filepath"
1212 "sort"
1313- "sync"
1413 "sync/atomic"
1514 "time"
1615···20192120 blockformat "github.com/ipfs/go-block-format"
2221 "github.com/ipfs/go-cid"
2323- "github.com/ipfs/go-datastore"
2422 blockstore "github.com/ipfs/go-ipfs-blockstore"
2523 cbor "github.com/ipfs/go-ipld-cbor"
2624 ipld "github.com/ipfs/go-ipld-format"
···4745const BigShardThreshold = 2 << 20
48464947type CarStore interface {
4848+ // TODO: not really part of general interface
5049 CompactUserShards(ctx context.Context, user models.Uid, skipBigShards bool) (*CompactionStats, error)
5050+ // TODO: not really part of general interface
5151 GetCompactionTargets(ctx context.Context, shardCount int) ([]CompactionTarget, error)
5252+5253 GetUserRepoHead(ctx context.Context, user models.Uid) (cid.Cid, error)
5354 GetUserRepoRev(ctx context.Context, user models.Uid) (string, error)
5455 ImportSlice(ctx context.Context, uid models.Uid, since *string, carslice []byte) (cid.Cid, *DeltaSession, error)
···6364 meta *CarStoreGormMeta
6465 rootDirs []string
65666666- lscLk sync.Mutex
6767- lastShardCache map[models.Uid]*CarShard
6767+ lastShardCache lastShardCache
68686969 log *slog.Logger
7070}
···8888 return nil, err
8989 }
90909191- return &FileCarStore{
9292- meta: &CarStoreGormMeta{meta: meta},
9393- rootDirs: roots,
9494- lastShardCache: make(map[models.Uid]*CarShard),
9595- log: slog.Default().With("system", "carstore"),
9696- }, nil
9191+ gormMeta := &CarStoreGormMeta{meta: meta}
9292+ out := &FileCarStore{
9393+ meta: gormMeta,
9494+ rootDirs: roots,
9595+ lastShardCache: lastShardCache{
9696+ source: gormMeta,
9797+ },
9898+ log: slog.Default().With("system", "carstore"),
9999+ }
100100+ out.lastShardCache.Init()
101101+ return out, nil
97102}
98103104104+// userView needs these things to get into the underlying block store
105105+// implemented by CarStoreGormMeta
106106+type userViewSource interface {
107107+ HasUidCid(ctx context.Context, user models.Uid, k cid.Cid) (bool, error)
108108+ LookupBlockRef(ctx context.Context, k cid.Cid) (path string, offset int64, user models.Uid, err error)
109109+}
110110+111111+// wrapper into a block store that keeps track of which user we are working on behalf of
99112type userView struct {
100100- cs CarStore
113113+ cs userViewSource
101114 user models.Uid
102115103116 cache map[cid.Cid]blockformat.Block
···115128 if have {
116129 return have, nil
117130 }
118118-119119- fcd, ok := uv.cs.(*FileCarStore)
120120- if !ok {
121121- return false, nil
122122- }
123123-124124- return fcd.meta.HasUidCid(ctx, uv.user, k)
131131+ return uv.cs.HasUidCid(ctx, uv.user, k)
125132}
126133127134var CacheHits int64
···143150 }
144151 atomic.AddInt64(&CacheMiss, 1)
145152146146- fcd, ok := uv.cs.(*FileCarStore)
147147- if !ok {
148148- return nil, ipld.ErrNotFound{Cid: k}
149149- }
150150-151151- path, offset, user, err := fcd.meta.LookupBlockRef(ctx, k)
153153+ path, offset, user, err := uv.cs.LookupBlockRef(ctx, k)
152154 if err != nil {
153155 return nil, err
154156 }
···279281 return len(blk.RawData()), nil
280282}
281283284284+// subset of blockstore.Blockstore that we actually use here
285285+type minBlockstore interface {
286286+ Get(ctx context.Context, bcid cid.Cid) (blockformat.Block, error)
287287+ Has(ctx context.Context, bcid cid.Cid) (bool, error)
288288+ GetSize(ctx context.Context, bcid cid.Cid) (int, error)
289289+}
290290+282291type DeltaSession struct {
283283- fresh blockstore.Blockstore
284292 blks map[cid.Cid]blockformat.Block
285293 rmcids map[cid.Cid]bool
286286- base blockstore.Blockstore
294294+ base minBlockstore
287295 user models.Uid
288296 baseCid cid.Cid
289297 seq int
290298 readonly bool
291291- cs CarStore
299299+ cs shardWriter
292300 lastRev string
293301}
294302295303func (cs *FileCarStore) checkLastShardCache(user models.Uid) *CarShard {
296296- cs.lscLk.Lock()
297297- defer cs.lscLk.Unlock()
298298-299299- ls, ok := cs.lastShardCache[user]
300300- if ok {
301301- return ls
302302- }
303303-304304- return nil
304304+ return cs.lastShardCache.check(user)
305305}
306306307307func (cs *FileCarStore) removeLastShardCache(user models.Uid) {
308308- cs.lscLk.Lock()
309309- defer cs.lscLk.Unlock()
310310-311311- delete(cs.lastShardCache, user)
308308+ cs.lastShardCache.remove(user)
312309}
313310314311func (cs *FileCarStore) putLastShardCache(ls *CarShard) {
315315- cs.lscLk.Lock()
316316- defer cs.lscLk.Unlock()
317317-318318- cs.lastShardCache[ls.Usr] = ls
312312+ cs.lastShardCache.put(ls)
319313}
320314321315func (cs *FileCarStore) getLastShard(ctx context.Context, user models.Uid) (*CarShard, error) {
322322- ctx, span := otel.Tracer("carstore").Start(ctx, "getLastShard")
323323- defer span.End()
324324-325325- maybeLs := cs.checkLastShardCache(user)
326326- if maybeLs != nil {
327327- return maybeLs, nil
328328- }
329329-330330- lastShard, err := cs.meta.GetLastShard(ctx, user)
331331- if err != nil {
332332- return nil, err
333333- }
334334-335335- cs.putLastShardCache(lastShard)
336336- return lastShard, nil
316316+ return cs.lastShardCache.get(ctx, user)
337317}
338318339319var ErrRepoBaseMismatch = fmt.Errorf("attempted a delta session on top of the wrong previous head")
···354334 }
355335356336 return &DeltaSession{
357357- fresh: blockstore.NewBlockstore(datastore.NewMapDatastore()),
358358- blks: make(map[cid.Cid]blockformat.Block),
337337+ blks: make(map[cid.Cid]blockformat.Block),
359338 base: &userView{
360339 user: user,
361361- cs: cs,
340340+ cs: cs.meta,
362341 prefetch: true,
363342 cache: make(map[cid.Cid]blockformat.Block),
364343 },
···374353 return &DeltaSession{
375354 base: &userView{
376355 user: user,
377377- cs: cs,
356356+ cs: cs.meta,
378357 prefetch: false,
379358 cache: make(map[cid.Cid]blockformat.Block),
380359 },
···385364}
386365387366// TODO: incremental is only ever called true, remove the param
388388-func (cs *FileCarStore) ReadUserCar(ctx context.Context, user models.Uid, sinceRev string, incremental bool, w io.Writer) error {
367367+func (cs *FileCarStore) ReadUserCar(ctx context.Context, user models.Uid, sinceRev string, incremental bool, shardOut io.Writer) error {
389368 ctx, span := otel.Tracer("carstore").Start(ctx, "ReadUserCar")
390369 defer span.End()
391370···398377 }
399378 }
400379401401- // TODO: Why does ReadUserCar want shards seq DESC but CompactUserShards wants seq ASC ?
402380 shards, err := cs.meta.GetUserShardsDesc(ctx, user, earlySeq)
403381 if err != nil {
404382 return err
···418396 if err := car.WriteHeader(&car.CarHeader{
419397 Roots: []cid.Cid{shards[0].Root.CID},
420398 Version: 1,
421421- }, w); err != nil {
399399+ }, shardOut); err != nil {
422400 return err
423401 }
424402425403 for _, sh := range shards {
426426- if err := cs.writeShardBlocks(ctx, &sh, w); err != nil {
404404+ if err := cs.writeShardBlocks(ctx, &sh, shardOut); err != nil {
427405 return err
428406 }
429407 }
···433411434412// inner loop part of ReadUserCar
435413// copy shard blocks from disk to Writer
436436-func (cs *FileCarStore) writeShardBlocks(ctx context.Context, sh *CarShard, w io.Writer) error {
414414+func (cs *FileCarStore) writeShardBlocks(ctx context.Context, sh *CarShard, shardOut io.Writer) error {
437415 ctx, span := otel.Tracer("carstore").Start(ctx, "writeShardBlocks")
438416 defer span.End()
439417···448426 return err
449427 }
450428451451- _, err = io.Copy(w, fi)
429429+ _, err = io.Copy(shardOut, fi)
452430 if err != nil {
453431 return err
454432 }
···603581 return nil, fmt.Errorf("cannot write to readonly deltaSession")
604582 }
605583606606- switch ocs := ds.cs.(type) {
607607- case *FileCarStore:
608608- return ocs.writeNewShard(ctx, root, rev, ds.user, ds.seq, ds.blks, ds.rmcids)
609609- case *NonArchivalCarstore:
610610- slice, err := blocksToCar(ctx, root, rev, ds.blks)
611611- if err != nil {
612612- return nil, err
613613- }
614614- return slice, ocs.updateLastCommit(ctx, ds.user, rev, root)
615615- default:
616616- return nil, fmt.Errorf("unsupported carstore type")
617617- }
584584+ return ds.cs.writeNewShard(ctx, root, rev, ds.user, ds.seq, ds.blks, ds.rmcids)
618585}
619586620587func WriteCarHeader(w io.Writer, root cid.Cid) (int64, error) {
···633600 }
634601635602 return hnw, nil
603603+}
604604+605605+// shardWriter.writeNewShard called from inside DeltaSession.CloseWithRoot
606606+type shardWriter interface {
607607+ // writeNewShard stores blocks in `blks` arg and creates a new shard to propagate out to our firehose
608608+ writeNewShard(ctx context.Context, root cid.Cid, rev string, user models.Uid, seq int, blks map[cid.Cid]blockformat.Block, rmcids map[cid.Cid]bool) ([]byte, error)
636609}
637610638611func blocksToCar(ctx context.Context, root cid.Cid, rev string, blks map[cid.Cid]blockformat.Block) ([]byte, error) {
···912912 return err
913913 }
914914915915+ if rev != nil && *rev == "" {
916916+ rev = nil
917917+ }
915918 if rev == nil {
916919 // if 'rev' is nil, this implies a fresh sync.
917920 // in this case, ignore any existing blocks we have and treat this like a clean import.