ref: 1cd58d8ec3f0f4e5ac84ba6c021d0ce098802f31
parent: 6470fe1e49f8cdec0feba02d70c3329113562f17
author: Runxi Yu <me@runxiyu.org>
date: Sun Feb 22 06:28:10 EST 2026
objectstore/chain: MRU especially to reduce loose object syscall cost
--- a/objectstore/chain/chain.go
+++ b/objectstore/chain/chain.go
@@ -1,4 +1,4 @@
-// Package chain provides a wrapper object storage backend to query a chain of
+// Package chain provides an adaptive wrapper over multiple object storage
// backends.
package chain
@@ -6,6 +6,7 @@
"errors"
"fmt"
"io"
+ "sync"
"codeberg.org/lindenii/furgit/objectid"
"codeberg.org/lindenii/furgit/objectstore"
@@ -12,26 +13,111 @@
"codeberg.org/lindenii/furgit/objecttype"
)
-// Chain queries multiple object databases in order.
+// Chain queries multiple object databases with an MRU backend preference.
type Chain struct {- backends []objectstore.Store
+ mu sync.RWMutex
+
+ backendHead *backendNode
+ backendTail *backendNode
+ backendNodeByStore map[objectstore.Store]*backendNode
}
-// New creates an ordered object database chain.
+type backendNode struct {+ backend objectstore.Store
+ prev *backendNode
+ next *backendNode
+}
+
+// New creates a Chain from backends.
func New(backends ...objectstore.Store) *Chain {+ nodeByStore := make(map[objectstore.Store]*backendNode, len(backends))
+ var head *backendNode
+ var tail *backendNode
+
+ for _, backend := range backends {+ if backend == nil {+ continue
+ }
+ node := &backendNode{+ backend: backend,
+ prev: tail,
+ }
+ if tail != nil {+ tail.next = node
+ }
+ if head == nil {+ head = node
+ }
+ tail = node
+ nodeByStore[backend] = node
+ }
+
return &Chain{- backends: append([]objectstore.Store(nil), backends...),
+ backendHead: head,
+ backendTail: tail,
+ backendNodeByStore: nodeByStore,
}
}
-// ReadBytesFull reads a full serialized object from the first backend that has it.
+func (chain *Chain) firstBackend() objectstore.Store {+ chain.mu.RLock()
+ defer chain.mu.RUnlock()
+ if chain.backendHead == nil {+ return nil
+ }
+ return chain.backendHead.backend
+}
+
+func (chain *Chain) nextBackend(current objectstore.Store) objectstore.Store {+ chain.mu.RLock()
+ defer chain.mu.RUnlock()
+ node := chain.backendNodeByStore[current]
+ if node == nil || node.next == nil {+ return nil
+ }
+ return node.next.backend
+}
+
+func (chain *Chain) touchBackend(backend objectstore.Store) {+ if backend == nil {+ return
+ }
+ if !chain.mu.TryLock() {+ return
+ }
+ defer chain.mu.Unlock()
+
+ node := chain.backendNodeByStore[backend]
+ if node == nil || node == chain.backendHead {+ return
+ }
+ if node.prev != nil {+ node.prev.next = node.next
+ }
+ if node.next != nil {+ node.next.prev = node.prev
+ }
+ if chain.backendTail == node {+ chain.backendTail = node.prev
+ }
+
+ node.prev = nil
+ node.next = chain.backendHead
+ if chain.backendHead != nil {+ chain.backendHead.prev = node
+ }
+ chain.backendHead = node
+ if chain.backendTail == nil {+ chain.backendTail = node
+ }
+}
+
+// ReadBytesFull reads a full serialized object from one backend that has it.
func (chain *Chain) ReadBytesFull(id objectid.ObjectID) ([]byte, error) {- for i, backend := range chain.backends {- if backend == nil {- continue
- }
+ for i, backend := 0, chain.firstBackend(); backend != nil; i, backend = i+1, chain.nextBackend(backend) {full, err := backend.ReadBytesFull(id)
if err == nil {+ chain.touchBackend(backend)
return full, nil
}
if errors.Is(err, objectstore.ErrObjectNotFound) {@@ -42,14 +128,13 @@
return nil, objectstore.ErrObjectNotFound
}
-// ReadBytesContent reads an object's type and content bytes from the first backend that has it.
+// ReadBytesContent reads an object's type and content bytes from one backend
+// that has it.
func (chain *Chain) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) {- for i, backend := range chain.backends {- if backend == nil {- continue
- }
+ for i, backend := 0, chain.firstBackend(); backend != nil; i, backend = i+1, chain.nextBackend(backend) {ty, content, err := backend.ReadBytesContent(id)
if err == nil {+ chain.touchBackend(backend)
return ty, content, nil
}
if errors.Is(err, objectstore.ErrObjectNotFound) {@@ -60,14 +145,13 @@
return objecttype.TypeInvalid, nil, objectstore.ErrObjectNotFound
}
-// ReadReaderFull reads a full serialized object stream from the first backend that has it.
+// ReadReaderFull reads a full serialized object stream from one backend that
+// has it.
func (chain *Chain) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) {- for i, backend := range chain.backends {- if backend == nil {- continue
- }
+ for i, backend := 0, chain.firstBackend(); backend != nil; i, backend = i+1, chain.nextBackend(backend) {reader, err := backend.ReadReaderFull(id)
if err == nil {+ chain.touchBackend(backend)
return reader, nil
}
if errors.Is(err, objectstore.ErrObjectNotFound) {@@ -78,14 +162,13 @@
return nil, objectstore.ErrObjectNotFound
}
-// ReadReaderContent reads an object's type, declared content length, and content stream from the first backend that has it.
+// ReadReaderContent reads an object's type, declared content length, and
+// content stream from one backend that has it.
func (chain *Chain) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) {- for i, backend := range chain.backends {- if backend == nil {- continue
- }
+ for i, backend := 0, chain.firstBackend(); backend != nil; i, backend = i+1, chain.nextBackend(backend) {ty, size, reader, err := backend.ReadReaderContent(id)
if err == nil {+ chain.touchBackend(backend)
return ty, size, reader, nil
}
if errors.Is(err, objectstore.ErrObjectNotFound) {@@ -96,14 +179,12 @@
return objecttype.TypeInvalid, 0, nil, objectstore.ErrObjectNotFound
}
-// ReadSize reads object content length from the first backend that has it.
+// ReadSize reads object content length from one backend that has it.
func (chain *Chain) ReadSize(id objectid.ObjectID) (int64, error) {- for i, backend := range chain.backends {- if backend == nil {- continue
- }
+ for i, backend := 0, chain.firstBackend(); backend != nil; i, backend = i+1, chain.nextBackend(backend) {size, err := backend.ReadSize(id)
if err == nil {+ chain.touchBackend(backend)
return size, nil
}
if errors.Is(err, objectstore.ErrObjectNotFound) {@@ -114,14 +195,12 @@
return 0, objectstore.ErrObjectNotFound
}
-// ReadHeader reads object header data from the first backend that has it.
+// ReadHeader reads object header data from one backend that has it.
func (chain *Chain) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) {- for i, backend := range chain.backends {- if backend == nil {- continue
- }
+ for i, backend := 0, chain.firstBackend(); backend != nil; i, backend = i+1, chain.nextBackend(backend) {ty, size, err := backend.ReadHeader(id)
if err == nil {+ chain.touchBackend(backend)
return ty, size, nil
}
if errors.Is(err, objectstore.ErrObjectNotFound) {@@ -134,11 +213,15 @@
// Close closes all backends and joins close errors.
func (chain *Chain) Close() error {+ chain.mu.RLock()
+ backends := make([]objectstore.Store, 0, len(chain.backendNodeByStore))
+ for node := chain.backendHead; node != nil; node = node.next {+ backends = append(backends, node.backend)
+ }
+ chain.mu.RUnlock()
+
var errs []error
- for _, backend := range chain.backends {- if backend == nil {- continue
- }
+ for _, backend := range backends { if err := backend.Close(); err != nil {errs = append(errs, err)
}
--
⑨