Skip to content

Commit

Permalink
feat: add Features + datastore scoping
Browse files Browse the repository at this point in the history
The motivation for this is to enable "dispatching" datastores that
dynamically implement the type of the datastore they are dispatching
to, so that type assertions behave equivalently on the dispatcher
as on the dispatchee.

At a high level, this works by generating a concrete implementation of
every possible combination of "features", and then picking the right
implementation at runtime. This is necessary due to language
constraints in Go--it is currently impossible to create a concrete
type dynamically with reflection that implements an interface.

"Features" are introduced here, which are supplemental, optional
interfaces that datastores may implement, such as batching. This is
backwards-compatible with existing "features".

Some related issues:

* #160
* #88
  • Loading branch information
guseggert committed Mar 23, 2022
1 parent 1e32606 commit 1568906
Show file tree
Hide file tree
Showing 7 changed files with 2,623 additions and 21 deletions.
27 changes: 6 additions & 21 deletions datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"errors"
"io"
"time"

query "github.com/ipfs/go-datastore/query"
)
Expand Down Expand Up @@ -103,8 +102,7 @@ type Read interface {
// capabilities of a `Batch`, but the reverse is NOT true.
type Batching interface {
Datastore

Batch(ctx context.Context) (Batch, error)
BatchingFeature
}

// ErrBatchUnsupported is returned if the by Batch if the Datastore doesn't
Expand All @@ -115,34 +113,29 @@ var ErrBatchUnsupported = errors.New("this datastore does not support batching")
// which may need checking on-disk data integrity.
type CheckedDatastore interface {
Datastore

Check(ctx context.Context) error
CheckedFeature
}

// ScrubbedDatastore is an interface that should be implemented by datastores
// which want to provide a mechanism to check data integrity and/or
// error correction.
type ScrubbedDatastore interface {
Datastore

Scrub(ctx context.Context) error
ScrubbedFeature
}

// GCDatastore is an interface that should be implemented by datastores which
// don't free disk space by just removing data from them.
type GCDatastore interface {
Datastore

CollectGarbage(ctx context.Context) error
GCFeature
}

// PersistentDatastore is an interface that should be implemented by datastores
// which can report disk usage.
type PersistentDatastore interface {
Datastore

// DiskUsage returns the space used by a datastore, in bytes.
DiskUsage(ctx context.Context) (uint64, error)
PersistentFeature
}

// DiskUsage checks if a Datastore is a
Expand All @@ -163,13 +156,6 @@ type TTLDatastore interface {
TTL
}

// TTL encapulates the methods that deal with entries with time-to-live.
type TTL interface {
PutWithTTL(ctx context.Context, key Key, value []byte, ttl time.Duration) error
SetTTL(ctx context.Context, key Key, ttl time.Duration) error
GetExpiration(ctx context.Context, key Key) (time.Time, error)
}

// Txn extends the Datastore type. Txns allow users to batch queries and
// mutations to the Datastore into atomic groups, or transactions. Actions
// performed on a transaction will not take hold until a successful call to
Expand All @@ -194,8 +180,7 @@ type Txn interface {
// support transactions.
type TxnDatastore interface {
Datastore

NewTransaction(ctx context.Context, readOnly bool) (Txn, error)
TxnFeature
}

// Errors
Expand Down
82 changes: 82 additions & 0 deletions features.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package datastore

import (
"context"
"time"
)

type BatchingFeature interface {
Batch(ctx context.Context) (Batch, error)
}

type CheckedFeature interface {
Check(ctx context.Context) error
}

type ScrubbedFeature interface {
Scrub(ctx context.Context) error
}

type GCFeature interface {
CollectGarbage(ctx context.Context) error
}

type PersistentFeature interface {
// DiskUsage returns the space used by a datastore, in bytes.
DiskUsage(ctx context.Context) (uint64, error)
}

// TTL encapulates the methods that deal with entries with time-to-live.
type TTL interface {
PutWithTTL(ctx context.Context, key Key, value []byte, ttl time.Duration) error
SetTTL(ctx context.Context, key Key, ttl time.Duration) error
GetExpiration(ctx context.Context, key Key) (time.Time, error)
}

type TxnFeature interface {
NewTransaction(ctx context.Context, readOnly bool) (Txn, error)
}

// Feature contains metadata about a datastore feature.
type Feature struct {
// Interface is the nil interface of the feature.
Interface interface{}
// DatastoreInterface is the nil interface of the feature's corresponding datastore interface.
DatastoreInterface interface{}
}

// Features returns a list of all datastore features.
// This serves both to provide an authoritative list of features,
// and to define a canonical ordering of features.
func Features() []Feature {
return []Feature{
{
Interface: (*BatchingFeature)(nil),
DatastoreInterface: (*Batching)(nil),
},
{
Interface: (*CheckedFeature)(nil),
DatastoreInterface: (*CheckedDatastore)(nil),
},
{
Interface: (*GCFeature)(nil),
DatastoreInterface: (*GCDatastore)(nil),
},
{
Interface: (*PersistentFeature)(nil),
DatastoreInterface: (*PersistentDatastore)(nil),
},
{
Interface: (*ScrubbedFeature)(nil),
DatastoreInterface: (*ScrubbedDatastore)(nil),
},
{
Interface: (*TTL)(nil),
DatastoreInterface: (*TTLDatastore)(nil),
},
{
Interface: (*TxnFeature)(nil),
DatastoreInterface: (*TxnDatastore)(nil),
},
}
}
40 changes: 40 additions & 0 deletions scoped/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Package scoped introduces a Datastore Shim that scopes down a source datastore
// to the features supported by a target datastore. This is useful e.g. for dispatching
// datastores, where the dispatcher needs to dynamically implement the same features
// as the dispatchee, without knowing them statically.
//
// Use the Wrap function to wrap a datastore so that its interface is scoped down to
// only those features supported both by it and its target datastore. Note that this
// is a set intersection--if the target implements a feature not supported by the
// wrapped datastore, then the resulting shim will not implement them either.
//
// For example:
//
// import (
// "context"
// scopedds "github.com/ipfs/go-datastore/scoped"
// ds "github.com/ipfs/go-datastore"
// )
//
// type BatchingDS struct { ds.Datastore }
//
// func (b *BatchingDS) Batch(ctx context.Context) (ds.Batch, error) {
// // custom batching
// return nil, nil
// }
//
// type BoringDS struct { ds.Datastore }
//
// func Dispatcher(dstore ds.Datastore) ds.Datastore {
// dispatcher := &BatchingDS{Datastore: dstore}
// dispatchee := &BoringDS{Datastore: dstore}
//
// // the dispatcher supports batching, but since the dispatchee
// // doesn't, the returned dispatcher does NOT implement ds.Batching
//
// return scoped.Wrap(dispatcher, dispatchee)
// }

package scoped

//go:generate go run generate/main.go
Loading

0 comments on commit 1568906

Please sign in to comment.