Skip to content

Commit 4ec7e14

Browse files
guseggertJorropo
authored andcommitted
feat: add Features + datastore scoping
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. We also want this to be backwards-compatible with existing code using type assertions. 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. These are backwards-compatible with existing "features", which are: * Batching * CheckedDatastore * GCDatastore * PersistentDatastore * ScrubbedDatastore * TTLDatastore * TxnDatastore New features can also be added in a backwards-compatible way. E.g. if datastore A is scoped down to datastore B, a new feature F is added, and then implemented on B, then A will continue to implement the same set of features since it hasn't implemented F yet (and vice versa if F is implemented on A but not B). Examples of things this enables: * Allow us to deprecate ErrBatchUnsupported * Allow existing dispatching datastores to support all features (keytransform, retrystore, MutexDatastore, autobatch, etc.) * Features supported by a Mount datastore could be scoped down to the intersection of all children * Communication with data about what functionality a datastore supports (e.g. for cross-language/RPC support) Some related issues: * #160 * #88
1 parent c5c0470 commit 4ec7e14

File tree

12 files changed

+3179
-86
lines changed

12 files changed

+3179
-86
lines changed

basic_ds.go

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -89,62 +89,6 @@ func (d *MapDatastore) Close() error {
8989
return nil
9090
}
9191

92-
// NullDatastore stores nothing, but conforms to the API.
93-
// Useful to test with.
94-
type NullDatastore struct {
95-
}
96-
97-
var _ Datastore = (*NullDatastore)(nil)
98-
var _ Batching = (*NullDatastore)(nil)
99-
100-
// NewNullDatastore constructs a null datastore
101-
func NewNullDatastore() *NullDatastore {
102-
return &NullDatastore{}
103-
}
104-
105-
// Put implements Datastore.Put
106-
func (d *NullDatastore) Put(ctx context.Context, key Key, value []byte) (err error) {
107-
return nil
108-
}
109-
110-
// Sync implements Datastore.Sync
111-
func (d *NullDatastore) Sync(ctx context.Context, prefix Key) error {
112-
return nil
113-
}
114-
115-
// Get implements Datastore.Get
116-
func (d *NullDatastore) Get(ctx context.Context, key Key) (value []byte, err error) {
117-
return nil, ErrNotFound
118-
}
119-
120-
// Has implements Datastore.Has
121-
func (d *NullDatastore) Has(ctx context.Context, key Key) (exists bool, err error) {
122-
return false, nil
123-
}
124-
125-
// Has implements Datastore.GetSize
126-
func (d *NullDatastore) GetSize(ctx context.Context, key Key) (size int, err error) {
127-
return -1, ErrNotFound
128-
}
129-
130-
// Delete implements Datastore.Delete
131-
func (d *NullDatastore) Delete(ctx context.Context, key Key) (err error) {
132-
return nil
133-
}
134-
135-
// Query implements Datastore.Query
136-
func (d *NullDatastore) Query(ctx context.Context, q dsq.Query) (dsq.Results, error) {
137-
return dsq.ResultsWithEntries(q, nil), nil
138-
}
139-
140-
func (d *NullDatastore) Batch(ctx context.Context) (Batch, error) {
141-
return NewBasicBatch(d), nil
142-
}
143-
144-
func (d *NullDatastore) Close() error {
145-
return nil
146-
}
147-
14892
// LogDatastore logs all accesses through the datastore.
14993
type LogDatastore struct {
15094
Name string

basic_ds_test.go

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,18 @@ import (
55
"log"
66
"testing"
77

8-
dstore "github.com/ipfs/go-datastore"
8+
"github.com/ipfs/go-datastore"
99
dstest "github.com/ipfs/go-datastore/test"
1010
)
1111

1212
func TestMapDatastore(t *testing.T) {
13-
ds := dstore.NewMapDatastore()
13+
ds := datastore.NewMapDatastore()
1414
dstest.SubtestAll(t, ds)
1515
}
1616

17-
func TestNullDatastore(t *testing.T) {
18-
ds := dstore.NewNullDatastore()
19-
// The only test that passes. Nothing should be found.
20-
dstest.SubtestNotFounds(t, ds)
21-
}
22-
2317
func TestLogDatastore(t *testing.T) {
2418
defer log.SetOutput(log.Writer())
2519
log.SetOutput(ioutil.Discard)
26-
ds := dstore.NewLogDatastore(dstore.NewMapDatastore(), "")
20+
ds := datastore.NewLogDatastore(datastore.NewMapDatastore(), "")
2721
dstest.SubtestAll(t, ds)
2822
}

datastore.go

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"errors"
66
"io"
7-
"time"
87

98
query "github.com/ipfs/go-datastore/query"
109
)
@@ -103,8 +102,7 @@ type Read interface {
103102
// capabilities of a `Batch`, but the reverse is NOT true.
104103
type Batching interface {
105104
Datastore
106-
107-
Batch(ctx context.Context) (Batch, error)
105+
BatchingFeature
108106
}
109107

110108
// ErrBatchUnsupported is returned if the by Batch if the Datastore doesn't
@@ -115,34 +113,29 @@ var ErrBatchUnsupported = errors.New("this datastore does not support batching")
115113
// which may need checking on-disk data integrity.
116114
type CheckedDatastore interface {
117115
Datastore
118-
119-
Check(ctx context.Context) error
116+
CheckedFeature
120117
}
121118

122119
// ScrubbedDatastore is an interface that should be implemented by datastores
123120
// which want to provide a mechanism to check data integrity and/or
124121
// error correction.
125122
type ScrubbedDatastore interface {
126123
Datastore
127-
128-
Scrub(ctx context.Context) error
124+
ScrubbedFeature
129125
}
130126

131127
// GCDatastore is an interface that should be implemented by datastores which
132128
// don't free disk space by just removing data from them.
133129
type GCDatastore interface {
134130
Datastore
135-
136-
CollectGarbage(ctx context.Context) error
131+
GCFeature
137132
}
138133

139134
// PersistentDatastore is an interface that should be implemented by datastores
140135
// which can report disk usage.
141136
type PersistentDatastore interface {
142137
Datastore
143-
144-
// DiskUsage returns the space used by a datastore, in bytes.
145-
DiskUsage(ctx context.Context) (uint64, error)
138+
PersistentFeature
146139
}
147140

148141
// DiskUsage checks if a Datastore is a
@@ -163,13 +156,6 @@ type TTLDatastore interface {
163156
TTL
164157
}
165158

166-
// TTL encapulates the methods that deal with entries with time-to-live.
167-
type TTL interface {
168-
PutWithTTL(ctx context.Context, key Key, value []byte, ttl time.Duration) error
169-
SetTTL(ctx context.Context, key Key, ttl time.Duration) error
170-
GetExpiration(ctx context.Context, key Key) (time.Time, error)
171-
}
172-
173159
// Txn extends the Datastore type. Txns allow users to batch queries and
174160
// mutations to the Datastore into atomic groups, or transactions. Actions
175161
// performed on a transaction will not take hold until a successful call to
@@ -194,8 +180,7 @@ type Txn interface {
194180
// support transactions.
195181
type TxnDatastore interface {
196182
Datastore
197-
198-
NewTransaction(ctx context.Context, readOnly bool) (Txn, error)
183+
TxnFeature
199184
}
200185

201186
// Errors

features.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package datastore
2+
3+
import (
4+
"context"
5+
"reflect"
6+
"time"
7+
)
8+
9+
const (
10+
FeatureNameBatching = "Batching"
11+
FeatureNameChecked = "Checked"
12+
FeatureNameGC = "GC"
13+
FeatureNamePersistent = "Persistent"
14+
FeatureNameScrubbed = "Scrubbed"
15+
FeatureNameTTL = "TTL"
16+
FeatureNameTransaction = "Transaction"
17+
)
18+
19+
type BatchingFeature interface {
20+
Batch(ctx context.Context) (Batch, error)
21+
}
22+
23+
type CheckedFeature interface {
24+
Check(ctx context.Context) error
25+
}
26+
27+
type ScrubbedFeature interface {
28+
Scrub(ctx context.Context) error
29+
}
30+
31+
type GCFeature interface {
32+
CollectGarbage(ctx context.Context) error
33+
}
34+
35+
type PersistentFeature interface {
36+
// DiskUsage returns the space used by a datastore, in bytes.
37+
DiskUsage(ctx context.Context) (uint64, error)
38+
}
39+
40+
// TTL encapulates the methods that deal with entries with time-to-live.
41+
type TTL interface {
42+
PutWithTTL(ctx context.Context, key Key, value []byte, ttl time.Duration) error
43+
SetTTL(ctx context.Context, key Key, ttl time.Duration) error
44+
GetExpiration(ctx context.Context, key Key) (time.Time, error)
45+
}
46+
47+
type TxnFeature interface {
48+
NewTransaction(ctx context.Context, readOnly bool) (Txn, error)
49+
}
50+
51+
// Feature contains metadata about a datastore Feature.
52+
type Feature struct {
53+
Name string
54+
// Interface is the nil interface of the feature.
55+
Interface interface{}
56+
// DatastoreInterface is the nil interface of the feature's corresponding datastore interface.
57+
DatastoreInterface interface{}
58+
}
59+
60+
var featuresByName map[string]Feature
61+
62+
func init() {
63+
featuresByName = map[string]Feature{}
64+
for _, f := range Features() {
65+
featuresByName[f.Name] = f
66+
}
67+
}
68+
69+
// Features returns a list of all known datastore features.
70+
// This serves both to provide an authoritative list of features,
71+
// and to define a canonical ordering of features.
72+
func Features() []Feature {
73+
// for backwards compatibility, only append to this list
74+
return []Feature{
75+
{
76+
Name: FeatureNameBatching,
77+
Interface: (*BatchingFeature)(nil),
78+
DatastoreInterface: (*Batching)(nil),
79+
},
80+
{
81+
Name: FeatureNameChecked,
82+
Interface: (*CheckedFeature)(nil),
83+
DatastoreInterface: (*CheckedDatastore)(nil),
84+
},
85+
{
86+
Name: FeatureNameGC,
87+
Interface: (*GCFeature)(nil),
88+
DatastoreInterface: (*GCDatastore)(nil),
89+
},
90+
{
91+
Name: FeatureNamePersistent,
92+
Interface: (*PersistentFeature)(nil),
93+
DatastoreInterface: (*PersistentDatastore)(nil),
94+
},
95+
{
96+
Name: FeatureNameScrubbed,
97+
Interface: (*ScrubbedFeature)(nil),
98+
DatastoreInterface: (*ScrubbedDatastore)(nil),
99+
},
100+
{
101+
Name: FeatureNameTTL,
102+
Interface: (*TTL)(nil),
103+
DatastoreInterface: (*TTLDatastore)(nil),
104+
},
105+
{
106+
Name: FeatureNameTransaction,
107+
Interface: (*TxnFeature)(nil),
108+
DatastoreInterface: (*TxnDatastore)(nil),
109+
},
110+
}
111+
}
112+
113+
// FeatureByName returns the feature with the given name, if known.
114+
func FeatureByName(name string) (Feature, bool) {
115+
feat, known := featuresByName[name]
116+
return feat, known
117+
}
118+
119+
// FeaturesForDatastore returns the features supported by the given datastore.
120+
func FeaturesForDatastore(dstore Datastore) (features []Feature) {
121+
if dstore == nil {
122+
return nil
123+
}
124+
dstoreType := reflect.TypeOf(dstore)
125+
for _, f := range Features() {
126+
fType := reflect.TypeOf(f.Interface).Elem()
127+
if dstoreType.Implements(fType) {
128+
features = append(features, f)
129+
}
130+
}
131+
return
132+
}

features_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package datastore
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"testing"
7+
)
8+
9+
func TestFeatureByName(t *testing.T) {
10+
feat, ok := FeatureByName(FeatureNameBatching)
11+
if !ok {
12+
t.Fatalf("expected a batching feature")
13+
}
14+
if feat.Name != FeatureNameBatching ||
15+
feat.Interface != (*BatchingFeature)(nil) ||
16+
feat.DatastoreInterface != (*Batching)(nil) {
17+
t.Fatalf("expected a batching feature, got %v", feat)
18+
}
19+
20+
feat, ok = FeatureByName("UnknownFeature")
21+
if ok {
22+
t.Fatalf("expected UnknownFeature not to be found")
23+
}
24+
}
25+
26+
func featuresByNames(names []string) (fs []Feature) {
27+
for _, n := range names {
28+
f, ok := FeatureByName(n)
29+
if !ok {
30+
panic(fmt.Sprintf("unknown feature %s", n))
31+
}
32+
fs = append(fs, f)
33+
}
34+
return
35+
}
36+
37+
func TestFeaturesForDatastore(t *testing.T) {
38+
cases := []struct {
39+
name string
40+
d Datastore
41+
expectedFeatures []string
42+
}{
43+
{
44+
name: "MapDatastore",
45+
d: &MapDatastore{},
46+
expectedFeatures: []string{"Batching"},
47+
},
48+
{
49+
name: "NullDatastore",
50+
d: &NullDatastore{},
51+
expectedFeatures: []string{"Batching", "Checked", "GC", "Persistent", "Scrubbed", "Transaction"},
52+
},
53+
{
54+
name: "LogDatastore",
55+
d: &LogDatastore{},
56+
expectedFeatures: []string{"Batching", "Checked", "GC", "Persistent", "Scrubbed"},
57+
},
58+
{
59+
name: "nil datastore",
60+
d: nil,
61+
expectedFeatures: nil,
62+
},
63+
}
64+
65+
for _, c := range cases {
66+
t.Run(c.name, func(t *testing.T) {
67+
feats := FeaturesForDatastore(c.d)
68+
if len(feats) != len(c.expectedFeatures) {
69+
t.Fatalf("expected %d features, got %v", len(c.expectedFeatures), feats)
70+
}
71+
expectedFeats := featuresByNames(c.expectedFeatures)
72+
if !reflect.DeepEqual(expectedFeats, feats) {
73+
t.Fatalf("expected features %v, got %v", c.expectedFeatures, feats)
74+
}
75+
})
76+
}
77+
}

0 commit comments

Comments
 (0)