-
Notifications
You must be signed in to change notification settings - Fork 65
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
12 changed files
with
3,183 additions
and
86 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
package datastore | ||
|
||
import ( | ||
"context" | ||
"reflect" | ||
"time" | ||
) | ||
|
||
const ( | ||
FeatureNameBatching = "Batching" | ||
FeatureNameChecked = "Checked" | ||
FeatureNameGC = "GC" | ||
FeatureNamePersistent = "Persistent" | ||
FeatureNameScrubbed = "Scrubbed" | ||
FeatureNameTTL = "TTL" | ||
FeatureNameTransaction = "Transaction" | ||
) | ||
|
||
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 { | ||
Name string | ||
// Interface is the nil interface of the feature. | ||
Interface interface{} | ||
// DatastoreInterface is the nil interface of the feature's corresponding datastore interface. | ||
DatastoreInterface interface{} | ||
} | ||
|
||
var featuresByName map[string]Feature | ||
|
||
func init() { | ||
featuresByName = map[string]Feature{} | ||
for _, f := range Features() { | ||
featuresByName[f.Name] = f | ||
} | ||
} | ||
|
||
// Features returns a list of all known datastore features. | ||
// This serves both to provide an authoritative list of features, | ||
// and to define a canonical ordering of features. | ||
func Features() []Feature { | ||
// for backwards compatibility, only append to this list | ||
return []Feature{ | ||
{ | ||
Name: FeatureNameBatching, | ||
Interface: (*BatchingFeature)(nil), | ||
DatastoreInterface: (*Batching)(nil), | ||
}, | ||
{ | ||
Name: FeatureNameChecked, | ||
Interface: (*CheckedFeature)(nil), | ||
DatastoreInterface: (*CheckedDatastore)(nil), | ||
}, | ||
{ | ||
Name: FeatureNameGC, | ||
Interface: (*GCFeature)(nil), | ||
DatastoreInterface: (*GCDatastore)(nil), | ||
}, | ||
{ | ||
Name: FeatureNamePersistent, | ||
Interface: (*PersistentFeature)(nil), | ||
DatastoreInterface: (*PersistentDatastore)(nil), | ||
}, | ||
{ | ||
Name: FeatureNameScrubbed, | ||
Interface: (*ScrubbedFeature)(nil), | ||
DatastoreInterface: (*ScrubbedDatastore)(nil), | ||
}, | ||
{ | ||
Name: FeatureNameTTL, | ||
Interface: (*TTL)(nil), | ||
DatastoreInterface: (*TTLDatastore)(nil), | ||
}, | ||
{ | ||
Name: FeatureNameTransaction, | ||
Interface: (*TxnFeature)(nil), | ||
DatastoreInterface: (*TxnDatastore)(nil), | ||
}, | ||
} | ||
} | ||
|
||
// FeaturesByNames returns the features with the given names, if they are known. | ||
func FeaturesByNames(names ...string) (features []Feature) { | ||
for _, n := range names { | ||
if feat, ok := featuresByName[n]; ok { | ||
features = append(features, feat) | ||
} | ||
} | ||
return | ||
} | ||
|
||
// FeatureByName returns the feature with the given name, if known. | ||
func FeatureByName(name string) (Feature, bool) { | ||
feat, known := featuresByName[name] | ||
return feat, known | ||
} | ||
|
||
// FeaturesForDatastore returns the features supported by the given datastore. | ||
func FeaturesForDatastore(dstore Datastore) (features []Feature) { | ||
if dstore == nil { | ||
return nil | ||
} | ||
dstoreType := reflect.ValueOf(dstore).Type() | ||
for _, f := range Features() { | ||
fType := reflect.TypeOf(f.Interface).Elem() | ||
if dstoreType.Implements(fType) { | ||
features = append(features, f) | ||
} | ||
} | ||
return | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package datastore | ||
|
||
import ( | ||
"reflect" | ||
"testing" | ||
) | ||
|
||
func TestFeaturesByNames(t *testing.T) { | ||
feats := FeaturesByNames() | ||
if feats != nil { | ||
t.Fatalf("expected nil features, got %v", feats) | ||
} | ||
|
||
feats = FeaturesByNames(FeatureNameBatching) | ||
if len(feats) != 1 || | ||
feats[0].Name != FeatureNameBatching || | ||
feats[0].Interface != (*BatchingFeature)(nil) || | ||
feats[0].DatastoreInterface != (*Batching)(nil) { | ||
t.Fatalf("expected a batching feature, got %v", feats) | ||
} | ||
|
||
feats = FeaturesByNames(FeatureNameBatching, "UnknownFeature") | ||
if len(feats) != 1 || feats[0].Name != FeatureNameBatching { | ||
t.Fatalf("expected a batching feature, got %v", feats) | ||
} | ||
} | ||
|
||
func TestFeatureByName(t *testing.T) { | ||
feat, ok := FeatureByName(FeatureNameBatching) | ||
if !ok { | ||
t.Fatalf("expected a batching feature") | ||
} | ||
if feat.Name != FeatureNameBatching || | ||
feat.Interface != (*BatchingFeature)(nil) || | ||
feat.DatastoreInterface != (*Batching)(nil) { | ||
t.Fatalf("expected a batching feature, got %v", feat) | ||
} | ||
|
||
feat, ok = FeatureByName("UnknownFeature") | ||
if ok { | ||
t.Fatalf("expected UnknownFeature not to be found") | ||
} | ||
} | ||
|
||
func TestFeaturesForDatastore(t *testing.T) { | ||
cases := []struct { | ||
name string | ||
d Datastore | ||
expectedFeatures []string | ||
}{ | ||
{ | ||
name: "MapDatastore", | ||
d: &MapDatastore{}, | ||
expectedFeatures: []string{"Batching"}, | ||
}, | ||
{ | ||
name: "NullDatastore", | ||
d: &NullDatastore{}, | ||
expectedFeatures: []string{"Batching", "Checked", "GC", "Persistent", "Scrubbed", "Transaction"}, | ||
}, | ||
{ | ||
name: "LogDatastore", | ||
d: &LogDatastore{}, | ||
expectedFeatures: []string{"Batching", "Checked", "GC", "Persistent", "Scrubbed"}, | ||
}, | ||
} | ||
|
||
for _, c := range cases { | ||
t.Run(c.name, func(t *testing.T) { | ||
feats := FeaturesForDatastore(c.d) | ||
if len(feats) != len(c.expectedFeatures) { | ||
t.Fatalf("expected %d features, got %v", len(c.expectedFeatures), feats) | ||
} | ||
expectedFeats := FeaturesByNames(c.expectedFeatures...) | ||
if !reflect.DeepEqual(expectedFeats, feats) { | ||
t.Fatalf("expected features %v, got %v", c.expectedFeatures, feats) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.