diff --git a/internal/datastore/crdb/options.go b/internal/datastore/crdb/options.go index b4c823c679..4301841902 100644 --- a/internal/datastore/crdb/options.go +++ b/internal/datastore/crdb/options.go @@ -2,6 +2,7 @@ package crdb import ( "fmt" + "github.com/authzed/spicedb/internal/datastore" "time" "github.com/alecthomas/units" @@ -26,6 +27,9 @@ type crdbOptions struct { overlapKey string } +// Option satisfies the DatastoreOptions interface +func (o *crdbOptions) Option() {} + const ( errQuantizationTooLarge = "revision quantization (%s) must be less than GC window (%s)" @@ -45,7 +49,19 @@ const ( // Option provides the facility to configure how clients within the CRDB // datastore interact with the running CockroachDB database. -type Option func(*crdbOptions) +type Option interface { + apply(datastore.DatastoreOptions) +} + +type OptionFunc func(options *crdbOptions) + +func (f OptionFunc) apply(o datastore.DatastoreOptions) { + co, ok := o.(*crdbOptions) + if !ok { + panic("wrong type of option") + } + f(co) +} func generateConfig(options []Option) (crdbOptions, error) { computed := crdbOptions{ @@ -61,7 +77,7 @@ func generateConfig(options []Option) (crdbOptions, error) { } for _, option := range options { - option(&computed) + option.apply(&computed) } // Run any checks on the config that need to be done @@ -81,9 +97,9 @@ func generateConfig(options []Option) (crdbOptions, error) { // // This value defaults to `common.DefaultSplitAtEstimatedQuerySize`. func SplitAtEstimatedQuerySize(splitAtEstimatedQuerySize units.Base2Bytes) Option { - return func(po *crdbOptions) { + return OptionFunc(func(po *crdbOptions) { po.splitAtEstimatedQuerySize = splitAtEstimatedQuerySize - } + }) } // ConnMaxIdleTime is the duration after which an idle connection will be @@ -91,9 +107,9 @@ func SplitAtEstimatedQuerySize(splitAtEstimatedQuerySize units.Base2Bytes) Optio // // This value defaults to having no maximum. func ConnMaxIdleTime(idle time.Duration) Option { - return func(po *crdbOptions) { + return OptionFunc(func(po *crdbOptions) { po.connMaxIdleTime = &idle - } + }) } // ConnMaxLifetime is the duration since creation after which a connection will @@ -101,9 +117,9 @@ func ConnMaxIdleTime(idle time.Duration) Option { // // This value defaults to having no maximum. func ConnMaxLifetime(lifetime time.Duration) Option { - return func(po *crdbOptions) { + return OptionFunc(func(po *crdbOptions) { po.connMaxLifetime = &lifetime - } + }) } // MinOpenConns is the minimum size of the connection pool. @@ -112,18 +128,18 @@ func ConnMaxLifetime(lifetime time.Duration) Option { // // This value defaults to zero. func MinOpenConns(conns int) Option { - return func(po *crdbOptions) { + return OptionFunc(func(po *crdbOptions) { po.minOpenConns = &conns - } + }) } // MaxOpenConns is the maximum size of the connection pool. // // This value defaults to having no maximum. func MaxOpenConns(conns int) Option { - return func(po *crdbOptions) { + return OptionFunc(func(po *crdbOptions) { po.maxOpenConns = &conns - } + }) } // WatchBufferLength is the number of entries that can be stored in the watch @@ -131,9 +147,9 @@ func MaxOpenConns(conns int) Option { // // This value defaults to 128. func WatchBufferLength(watchBufferLength uint16) Option { - return func(po *crdbOptions) { + return OptionFunc(func(po *crdbOptions) { po.watchBufferLength = watchBufferLength - } + }) } // RevisionQuantization is the time bucket size to which advertised revisions @@ -141,18 +157,18 @@ func WatchBufferLength(watchBufferLength uint16) Option { // // This value defaults to 5 seconds. func RevisionQuantization(bucketSize time.Duration) Option { - return func(po *crdbOptions) { + return OptionFunc(func(po *crdbOptions) { po.revisionQuantization = bucketSize - } + }) } // FollowerReadDelay is the time delay to apply to enable historial reads. // // This value defaults to 0 seconds. func FollowerReadDelay(delay time.Duration) Option { - return func(po *crdbOptions) { + return OptionFunc(func(po *crdbOptions) { po.followerReadDelay = delay - } + }) } // MaxRevisionStalenessPercent is the amount of time, expressed as a percentage of @@ -161,9 +177,9 @@ func FollowerReadDelay(delay time.Duration) Option { // // This value defaults to 0.1 (10%). func MaxRevisionStalenessPercent(stalenessPercent float64) Option { - return func(po *crdbOptions) { + return OptionFunc(func(po *crdbOptions) { po.maxRevisionStalenessPercent = stalenessPercent - } + }) } // GCWindow is the maximum age of a passed revision that will be considered @@ -171,32 +187,32 @@ func MaxRevisionStalenessPercent(stalenessPercent float64) Option { // // This value defaults to 24 hours. func GCWindow(window time.Duration) Option { - return func(po *crdbOptions) { + return OptionFunc(func(po *crdbOptions) { po.gcWindow = window - } + }) } // MaxRetries is the maximum number of times a retriable transaction will be // client-side retried. // Default: 50 func MaxRetries(maxRetries int) Option { - return func(po *crdbOptions) { + return OptionFunc(func(po *crdbOptions) { po.maxRetries = maxRetries - } + }) } // OverlapStrategy is the strategy used to generate overlap keys on write. // Default: 'static' func OverlapStrategy(strategy string) Option { - return func(po *crdbOptions) { + return OptionFunc(func(po *crdbOptions) { po.overlapStrategy = strategy - } + }) } // OverlapKey is a key touched on every write if OverlapStrategy is "static" // Default: 'key' func OverlapKey(key string) Option { - return func(po *crdbOptions) { + return OptionFunc(func(po *crdbOptions) { po.overlapKey = key - } + }) } diff --git a/internal/datastore/datastore.go b/internal/datastore/datastore.go index d1df172907..a5f8fb61bd 100644 --- a/internal/datastore/datastore.go +++ b/internal/datastore/datastore.go @@ -2,6 +2,9 @@ package datastore import ( "context" + "fmt" + "github.com/authzed/spicedb/internal/datastore/crdb" + "github.com/authzed/spicedb/internal/datastore/postgres" v0 "github.com/authzed/authzed-go/proto/authzed/api/v0" v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" @@ -150,3 +153,57 @@ type Revision = decimal.Decimal // revision type in the future a bit easier if necessary. Implementations // should use any time they want to signal an empty/error revision. var NoRevision Revision + +type DatastoreOptions interface { + Option() +} + +type Option interface { + apply(DatastoreOptions) +} + +type OptionFunc func(DatastoreOptions) + +func (f OptionFunc) apply(o DatastoreOptions) { + f(o) +} + +type Kind string + +const ( + CockroachKind Kind = "cockroach" + PostgresKind Kind = "postgres" + MemoryKind Kind = "memory" +) + +// NewDatastore initializes a datastore given the options +func NewDatastore(kind Kind, options ...Option) (Datastore, error) { + // TODO: make this a map lookup so that you can add your own DatastoreKinds? + switch kind { + case CockroachKind: + crdbOpts := make([]crdb.Option, 0, len(options)) + for _, o := range options { + crdbOpt, ok := o.(crdb.Option) + if !ok { + return nil, fmt.Errorf("incompatible option") + } + crdbOpts = append(crdbOpts, crdbOpt) + } + // TODO: url should be an option + return crdb.NewCRDBDatastore("", crdbOpts...) + case PostgresKind: + pgOpts:= make([]postgres.Option, 0, len(options)) + for _, o := range options { + pgOpt, ok := o.(postgres.Option) + if !ok { + return nil, fmt.Errorf("incompatible option") + } + pgOpts = append(pgOpts, pgOpt) + } + // TODO: url should be an option + return postgres.NewPostgresDatastore("", pgOpts...) + case MemoryKind: + // TODO: memory datastore needs options + } + return nil, fmt.Errorf("unknown datastore kind") +} diff --git a/internal/datastore/postgres/options.go b/internal/datastore/postgres/options.go index 4e3e7cb7b7..cb8dde2b94 100644 --- a/internal/datastore/postgres/options.go +++ b/internal/datastore/postgres/options.go @@ -2,6 +2,7 @@ package postgres import ( "fmt" + "github.com/authzed/spicedb/internal/datastore" "time" "github.com/alecthomas/units" @@ -28,6 +29,9 @@ type postgresOptions struct { logger *tracingLogger } +// Option satisfies the datastoreOptions interface +func (o *postgresOptions) Option()() {} + const ( errFuzzingTooLarge = "revision fuzzing timedelta (%s) must be less than GC window (%s)" @@ -39,7 +43,19 @@ const ( // Option provides the facility to configure how clients within the // Postgres datastore interact with the running Postgres database. -type Option func(*postgresOptions) +type Option interface { + apply(datastore.DatastoreOptions) +} + +type OptionFunc func(*postgresOptions) + +func (f OptionFunc) apply(o datastore.DatastoreOptions) { + po, ok := o.(*postgresOptions) + if !ok { + panic("wrong type of option") + } + f(po) +} func generateConfig(options []Option) (postgresOptions, error) { computed := postgresOptions{ @@ -51,7 +67,7 @@ func generateConfig(options []Option) (postgresOptions, error) { } for _, option := range options { - option(&computed) + option.apply(&computed) } // Run any checks on the config that need to be done @@ -71,9 +87,9 @@ func generateConfig(options []Option) (postgresOptions, error) { // // This value defaults to `common.DefaultSplitAtEstimatedQuerySize`. func SplitAtEstimatedQuerySize(splitAtEstimatedQuerySize units.Base2Bytes) Option { - return func(po *postgresOptions) { + return OptionFunc(func(po *postgresOptions) { po.splitAtEstimatedQuerySize = splitAtEstimatedQuerySize - } + }) } // ConnMaxIdleTime is the duration after which an idle connection will be @@ -81,9 +97,9 @@ func SplitAtEstimatedQuerySize(splitAtEstimatedQuerySize units.Base2Bytes) Optio // // This value defaults to having no maximum. func ConnMaxIdleTime(idle time.Duration) Option { - return func(po *postgresOptions) { + return OptionFunc(func(po *postgresOptions) { po.connMaxIdleTime = &idle - } + }) } // ConnMaxLifetime is the duration since creation after which a connection will @@ -91,26 +107,26 @@ func ConnMaxIdleTime(idle time.Duration) Option { // // This value defaults to having no maximum. func ConnMaxLifetime(lifetime time.Duration) Option { - return func(po *postgresOptions) { + return OptionFunc(func(po *postgresOptions) { po.connMaxLifetime = &lifetime - } + }) } // HealthCheckPeriod is the interval by which idle Postgres client connections // are health checked in order to keep them alive in a connection pool. func HealthCheckPeriod(period time.Duration) Option { - return func(po *postgresOptions) { + return OptionFunc(func(po *postgresOptions) { po.healthCheckPeriod = &period - } + }) } // MaxOpenConns is the maximum size of the connection pool. // // This value defaults to having no maximum. func MaxOpenConns(conns int) Option { - return func(po *postgresOptions) { + return OptionFunc(func(po *postgresOptions) { po.maxOpenConns = &conns - } + }) } // MinOpenConns is the minimum size of the connection pool. @@ -119,9 +135,9 @@ func MaxOpenConns(conns int) Option { // // This value defaults to zero. func MinOpenConns(conns int) Option { - return func(po *postgresOptions) { + return OptionFunc(func(po *postgresOptions) { po.minOpenConns = &conns - } + }) } // WatchBufferLength is the number of entries that can be stored in the watch @@ -129,9 +145,9 @@ func MinOpenConns(conns int) Option { // // This value defaults to 128. func WatchBufferLength(watchBufferLength uint16) Option { - return func(po *postgresOptions) { + return OptionFunc(func(po *postgresOptions) { po.watchBufferLength = watchBufferLength - } + }) } // RevisionFuzzingTimedelta is the time bucket size to which advertised @@ -139,9 +155,9 @@ func WatchBufferLength(watchBufferLength uint16) Option { // // This value defaults to 5 seconds. func RevisionFuzzingTimedelta(delta time.Duration) Option { - return func(po *postgresOptions) { + return OptionFunc(func(po *postgresOptions) { po.revisionFuzzingTimedelta = delta - } + }) } // GCWindow is the maximum age of a passed revision that will be considered @@ -149,18 +165,18 @@ func RevisionFuzzingTimedelta(delta time.Duration) Option { // // This value defaults to 24 hours. func GCWindow(window time.Duration) Option { - return func(po *postgresOptions) { + return OptionFunc(func(po *postgresOptions) { po.gcWindow = window - } + }) } // GCInterval is the the interval at which garbage collection will occur. // // This value defaults to 3 minutes. func GCInterval(interval time.Duration) Option { - return func(po *postgresOptions) { + return OptionFunc(func(po *postgresOptions) { po.gcInterval = interval - } + }) } // GCMaxOperationTime is the maximum operation time of a garbage collection @@ -168,9 +184,9 @@ func GCInterval(interval time.Duration) Option { // // This value defaults to 1 minute. func GCMaxOperationTime(time time.Duration) Option { - return func(po *postgresOptions) { + return OptionFunc(func(po *postgresOptions) { po.gcMaxOperationTime = time - } + }) } // EnablePrometheusStats enables Prometheus metrics provided by the Postgres @@ -178,9 +194,9 @@ func GCMaxOperationTime(time time.Duration) Option { // // Prometheus metrics are disable by default. func EnablePrometheusStats() Option { - return func(po *postgresOptions) { + return OptionFunc(func(po *postgresOptions) { po.enablePrometheusStats = true - } + }) } // EnableTracing enables trace-level logging for the Postgres clients being @@ -188,7 +204,7 @@ func EnablePrometheusStats() Option { // // Tracing is disabled by default. func EnableTracing() Option { - return func(po *postgresOptions) { + return OptionFunc(func(po *postgresOptions) { po.logger = &tracingLogger{} - } + }) }