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
15 changes: 13 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ The following emojis are used to highlight certain changes:

- `gateway`: `GET`/`HEAD /ipfs/bafkqaaa?format=raw` now always returns `200` with an empty body, so probing clients keep marking the gateway as functional even when its backend cannot serve identity CIDs. `bitswap/network/httpnet` sends this [trustless gateway probe](https://specs.ipfs.tech/http-gateways/trustless-gateway/#dedicated-probe-paths) to check providers, and a failed probe drops the provider. Exported as `gateway.EmptyIdentityCID`. [#1179](https://github.com/ipfs/boxo/pull/1179)
- `path`: added `NewPathFromURI`, which accepts native IPFS URIs (`ipfs://cid`, `ipns://name`, `ipld://cid`, and the schemeless `ipfs:`/`ipns:`/`ipld:` forms) and rewrites them to canonical content paths, so values copied from browsers and other tools parse as-is. `NewPath` stays strict and still rejects URI-shaped input, leaving untrusted parsing such as DNSLink records unchanged. [#1182](https://github.com/ipfs/boxo/pull/1182)
- `blockstore`: `CachedBlockstore` now returns a value implementing the
exported `BloomCacheStatus` interface (`Wait`/`BloomActive`/`Rebuild`) when a
Bloom filter is configured, so callers can wait for and observe the result of
the asynchronous filter build (previously these methods were unreachable on the
unexported cache type), and retry a failed build with `Rebuild`. While a
rebuild runs the filter is inactive (lookups fall through to the underlying
blockstore, so results stay correct but unaccelerated) and it is activated
again only on a complete enumeration. A new optional `AllKeysChanWithErrer`
capability lets a `Blockstore` report an error that truncates `AllKeysChan`
enumeration. [#1184](https://github.com/ipfs/boxo/pull/1184)

### Changed

Expand All @@ -34,8 +44,9 @@ answered "not present" conclusively for blocks that exist but were never
indexed, reporting present blocks as missing (`Has` returns false,
`Get`/`GetSize`/`View` return not-found, `DeleteBlock` becomes a silent no-op).
The build now activates the filter only when enumeration is known to have
completed; otherwise the cache degrades to correct pass-through. This also
fixes a race where a cancelled build could still mark the filter active.
completed; otherwise it records the error (observable via
`BloomCacheStatus.Wait`) and the cache degrades to correct pass-through. This
also fixes a race where a cancelled build could still mark the filter active.
[#1183](https://github.com/ipfs/boxo/pull/1183)

### Security
Expand Down
38 changes: 19 additions & 19 deletions blockstore/allkeyschanwitherr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ type cleanChanErrFnBS struct {
enumErr error
}

func (f *cleanChanErrFnBS) allKeysChanWithErr(ctx context.Context) (<-chan cid.Cid, func() error, error) {
func (f *cleanChanErrFnBS) AllKeysChanWithErr(ctx context.Context) (<-chan cid.Cid, func() error, error) {
ch, err := f.AllKeysChan(ctx)
if err != nil {
return nil, nil, err
Expand All @@ -76,8 +76,8 @@ func (f *cleanChanErrFnBS) allKeysChanWithErr(ctx context.Context) (<-chan cid.C
func TestAllKeysChanWithErrClean(t *testing.T) {
bs, keys := newBlockStoreWithKeys(t, nil, 100)

e := bs.(allKeysChanWithErrer)
ch, errFn, err := e.allKeysChanWithErr(t.Context())
e := bs.(AllKeysChanWithErrer)
ch, errFn, err := e.AllKeysChanWithErr(t.Context())
if err != nil {
t.Fatal(err)
}
Expand All @@ -100,8 +100,8 @@ func TestAllKeysChanWithErrMidIteration(t *testing.T) {
}
}

e := bs.(allKeysChanWithErrer)
ch, errFn, err := e.allKeysChanWithErr(t.Context())
e := bs.(AllKeysChanWithErrer)
ch, errFn, err := e.AllKeysChanWithErr(t.Context())
if err != nil {
t.Fatal(err)
}
Expand All @@ -125,10 +125,10 @@ func TestAllKeysChanWithErrContextCancel(t *testing.T) {
n := 2*dsq.KeysOnlyBufSize + 10
bs, _ := newBlockStoreWithKeys(t, nil, n)

e := bs.(allKeysChanWithErrer)
e := bs.(AllKeysChanWithErrer)
ctx, cancel := context.WithCancel(t.Context())
t.Cleanup(cancel)
ch, errFn, err := e.allKeysChanWithErr(ctx)
ch, errFn, err := e.AllKeysChanWithErr(ctx)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -295,11 +295,11 @@ func TestAllKeysChanWithErrForwardsThroughCacheStack(t *testing.T) {
t.Fatal(err)
}

e, ok := cbs.(allKeysChanWithErrer)
e, ok := cbs.(AllKeysChanWithErrer)
if !ok {
t.Fatal("CachedBlockstore result does not implement allKeysChanWithErrer")
t.Fatal("CachedBlockstore result does not implement AllKeysChanWithErrer")
}
ch, errFn, err := e.allKeysChanWithErr(ctx)
ch, errFn, err := e.AllKeysChanWithErr(ctx)
if err != nil {
t.Fatal(err)
}
Expand All @@ -309,7 +309,7 @@ func TestAllKeysChanWithErrForwardsThroughCacheStack(t *testing.T) {
}
}

// legacyBS implements only the Blockstore interface (no allKeysChanWithErrer),
// legacyBS implements only the Blockstore interface (no AllKeysChanWithErrer),
// to exercise the best-effort fallback in allKeysChanWithErrFor. It must NOT
// embed *blockstore, or the capability method would be promoted and the
// fallback branch would never run.
Expand Down Expand Up @@ -341,8 +341,8 @@ func TestAllKeysChanWithErrFallbackForLegacyBlockstore(t *testing.T) {

// Guard the premise: legacyBS must NOT advertise the capability, else the
// fallback branch is not exercised.
if _, ok := Blockstore(legacy).(allKeysChanWithErrer); ok {
t.Fatal("legacyBS unexpectedly implements allKeysChanWithErrer")
if _, ok := Blockstore(legacy).(AllKeysChanWithErrer); ok {
t.Fatal("legacyBS unexpectedly implements AllKeysChanWithErrer")
}

// The fallback yields a no-op errFn and a full enumeration.
Expand Down Expand Up @@ -400,11 +400,11 @@ func TestAllKeysChanWithErrForwardingWrappers(t *testing.T) {
for name, wrap := range wrappers {
t.Run(name, func(t *testing.T) {
w := wrap(newBaseWithErr())
e, ok := w.(allKeysChanWithErrer)
e, ok := w.(AllKeysChanWithErrer)
if !ok {
t.Fatalf("%s does not implement allKeysChanWithErrer", name)
t.Fatalf("%s does not implement AllKeysChanWithErrer", name)
}
ch, errFn, err := e.allKeysChanWithErr(t.Context())
ch, errFn, err := e.AllKeysChanWithErr(t.Context())
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -437,8 +437,8 @@ func TestAllKeysChanWithErrSkipsUnparseableKey(t *testing.T) {
t.Fatal(err)
}

e := bs.(allKeysChanWithErrer)
ch, errFn, err := e.allKeysChanWithErr(t.Context())
e := bs.(AllKeysChanWithErrer)
ch, errFn, err := e.AllKeysChanWithErr(t.Context())
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -476,7 +476,7 @@ type incompleteViewerBS struct {
enumErr error
}

func (f *incompleteViewerBS) allKeysChanWithErr(ctx context.Context) (<-chan cid.Cid, func() error, error) {
func (f *incompleteViewerBS) AllKeysChanWithErr(ctx context.Context) (<-chan cid.Cid, func() error, error) {
ch, err := f.AllKeysChan(ctx)
if err != nil {
return nil, nil, err
Expand Down
60 changes: 31 additions & 29 deletions blockstore/blockstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,32 @@ type Blockstore interface {
// cancelled context), the channel may be closed early without warning.
// Callers MUST NOT assume the returned error reflects enumeration
// completeness: it is only guaranteed to cover query setup, not
// mid-iteration failures. A consumer that requires a complete enumeration
// (such as building a Bloom filter) cannot rely on this method alone.
// mid-iteration failures. Consumers that require a complete enumeration
// (such as building a Bloom filter) should check whether the blockstore
// implements [AllKeysChanWithErrer].
AllKeysChan(ctx context.Context) (<-chan cid.Cid, error)
}

// AllKeysChanWithErrer is an optional capability a Blockstore may implement
// alongside AllKeysChan. AllKeysChanWithErr behaves like
// [Blockstore.AllKeysChan], but additionally returns a function that, once the
// returned channel has been fully drained, reports any error that terminated
// enumeration early.
//
// The reported error is nil if enumeration ran to completion without a
// mid-iteration error or cancellation; a non-nil error means enumeration was
// truncated and the delivered keys must not be treated as the complete set.
// Datastore keys that cannot be parsed as a block key are skipped without being
// treated as an error: such a key cannot correspond to a retrievable block, so
// omitting it cannot cause a Bloom-filter false negative.
//
// The function blocks until enumeration finishes, so it must be called only
// after the channel has been drained; calling it earlier deadlocks the caller
// once the producer fills the channel buffer.
type AllKeysChanWithErrer interface {
AllKeysChanWithErr(ctx context.Context) (<-chan cid.Cid, func() error, error)
}

// Viewer can be implemented by blockstores that offer zero-copy access to
// values.
//
Expand Down Expand Up @@ -292,35 +313,16 @@ func (bs *blockstore) DeleteBlock(ctx context.Context, k cid.Cid) error {
//
// AllKeysChan respects context.
func (bs *blockstore) AllKeysChan(ctx context.Context) (<-chan cid.Cid, error) {
ch, _, err := bs.allKeysChanWithErr(ctx)
ch, _, err := bs.AllKeysChanWithErr(ctx)
return ch, err
}

// allKeysChanWithErrer is an optional capability a Blockstore may implement
// alongside AllKeysChan. allKeysChanWithErr behaves like AllKeysChan, but
// additionally returns a function that, once the returned channel has been
// fully drained, reports any error that terminated enumeration early.
//
// The reported error is nil if enumeration ran to completion without a
// mid-iteration error or cancellation; a non-nil error means enumeration was
// truncated and the delivered keys must not be treated as the complete set.
// Datastore keys that cannot be parsed as a block key are skipped without being
// treated as an error: such a key cannot correspond to a retrievable block, so
// omitting it cannot cause a Bloom-filter false negative.
//
// The function blocks until enumeration finishes, so it must be called only
// after the channel has been drained; calling it earlier deadlocks the caller
// once the producer fills the channel buffer.
type allKeysChanWithErrer interface {
allKeysChanWithErr(ctx context.Context) (<-chan cid.Cid, func() error, error)
}

var _ allKeysChanWithErrer = (*blockstore)(nil)
var _ AllKeysChanWithErrer = (*blockstore)(nil)

// allKeysChanWithErr implements [allKeysChanWithErrer]. The returned function
// AllKeysChanWithErr implements [AllKeysChanWithErrer]. The returned function
// reports any error that ended key enumeration early (nil if every key was
// delivered) and must be called only after the channel has been drained.
func (bs *blockstore) allKeysChanWithErr(ctx context.Context) (<-chan cid.Cid, func() error, error) {
func (bs *blockstore) AllKeysChanWithErr(ctx context.Context) (<-chan cid.Cid, func() error, error) {
// KeysOnly, because that would be _a lot_ of data.
q := dsq.Query{KeysOnly: true}
res, err := bs.datastore.Query(ctx, q)
Expand Down Expand Up @@ -373,13 +375,13 @@ func (bs *blockstore) allKeysChanWithErr(ctx context.Context) (<-chan cid.Cid, f
return output, func() error { <-done; return iterErr }, nil
}

// allKeysChanWithErrFor returns bs.allKeysChanWithErr when bs implements
// [allKeysChanWithErrer]. Otherwise it falls back to [Blockstore.AllKeysChan]
// allKeysChanWithErrFor returns bs.AllKeysChanWithErr when bs implements
// [AllKeysChanWithErrer]. Otherwise it falls back to [Blockstore.AllKeysChan]
// with a no-op error function, preserving best-effort behavior for blockstores
// that cannot report enumeration errors.
func allKeysChanWithErrFor(ctx context.Context, bs Blockstore) (<-chan cid.Cid, func() error, error) {
if e, ok := bs.(allKeysChanWithErrer); ok {
return e.allKeysChanWithErr(ctx)
if e, ok := bs.(AllKeysChanWithErrer); ok {
return e.AllKeysChanWithErr(ctx)
}
ch, err := bs.AllKeysChan(ctx)
return ch, func() error { return nil }, err
Expand Down
Loading
Loading