Skip to content

Commit

Permalink
Configure options to be passed through as variadic functions
Browse files Browse the repository at this point in the history
Passing variadic option functions allows for any number of options to be
added in the future without significant changes to the api and allows
for sane configuration of default values
  • Loading branch information
andy9775 committed Aug 11, 2018
1 parent 0bfdd91 commit 1e0d7f4
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 106 deletions.
29 changes: 18 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ then returns the values for the keys it is attached to.
> DataLoader is the basic interface for the library. It contains a provided
> strategy and cache strategy.
**`NewDataLoader(int, BatchFunction, func(int, BatchFunction) Strategy, Cache, Tracer) DataLoader`**<br>
**`NewDataLoader(int, BatchFunction, func(int, BatchFunction) Strategy, ...Option) DataLoader`**<br>
NewDataLoader returns a new instance of a DataLoader tracking to the capacity
provided and using the provided execution and cache strategy. The second argument
should return a strategy which accepts a capacity value and the BatchFunction
Expand All @@ -87,7 +87,16 @@ called returns the values for the provided keys. Load does not block callers.
**`LoadMany(context.Context, ...Key) ThunkMany`**<br>
Returns a ThunkMany for the specified keys. Internally LoadMany adds the
provided keys to the keys array and returns a callback function which when
called returns the values for the provided keys. LoadMany does not block callers.
called returns the values for the provided keys. LoadMany does not block
callers.

The options include:

**`WithCache(Cache) Option`**<br>
WithCache sets the provided cache strategy on the loader

**`WithTracer(Cache) Option`**<br>
WithTracer sets the provided tracer on the loader

#### Strategy

Expand Down Expand Up @@ -119,8 +128,8 @@ strategy for the provided capacity.

The Options values include:

- Timeout `time.Duration` - the time after which the batch function will be called if the
capacity is not reached. `Default: 6 milliseconds`
**`WithTimeout(time.Duration) Option`**<br>
WithTimeout sets the configured timeout on the strategy. `Default to 6 milliseconds`

#### Standard Strategy

Expand All @@ -136,8 +145,8 @@ standard strategy for the provided capacity.

The Options include:

- Timeout `time.Duration` - the time after which the batch function will be
called if the capacity is not hit. `Default: 6 milliseconds`
**`WithTimeout(time.Duration) Option`**<br>
WithTimeout sets the configured timeout on the strategy. `Default to 6 milliseconds`

#### Once Strategy

Expand All @@ -154,11 +163,9 @@ strategy ignoring the provided capacity value.

The Options include:

- InBackground `bool` - if true the batch function will be invoked in a
background go routine as soon as Load or LoadMany is called. If the batch
function executes in the background before calling Thunk/ThunkMany, calls
will not block and return data. Else calls to Thunk/ThunkMany will block until
the batch function executes.
**`WithInBackground() Option`**<br>
WithInBackground enables the batch function to execute in background on calls to
Load/LoadMany

#### ResultMap

Expand Down
46 changes: 39 additions & 7 deletions dataloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ type Thunk func() Result
// Calling ThunkMany will block until the result is returned from the batch function.
type ThunkMany func() ResultMap

// Option accepts the dataloader and sets an option on it.
type Option func(*dataloader)

// NewDataLoader returns a new DataLoader with a count capacity of `capacity`.
// The capacity value determines when the batch loader function will execute.
// The dataloader requires a strategy to execute and a cache strategy to use for
Expand All @@ -40,24 +43,53 @@ func NewDataLoader(
capacity int,
batch BatchFunction,
fn func(int /* capacity */, BatchFunction) Strategy,
cacheStrategy Cache,
tracer Tracer,
opts ...Option,
) DataLoader {

loader := dataloader{}

// set the options
for _, apply := range opts {
apply(&loader)
}

// default options
if loader.cache == nil {
loader.cache = NewNoOpCache()
}

if loader.tracer == nil {
loader.tracer = NewNoOpTracer()
}

// wrap the batch function and implement tracing around it
batchFunc := func(ogCtx context.Context, keys Keys) *ResultMap {
ctx, finish := tracer.Batch(ogCtx)
ctx, finish := loader.tracer.Batch(ogCtx)

r := batch(ctx, keys)

finish(*r)
return r
}

return &dataloader{
strategy: fn(capacity, batchFunc),
cache: cacheStrategy,
tracer: tracer,
loader.strategy = fn(capacity, batchFunc)

return &loader
}

// ============================================= options setters =============================================

// WithCache adds a cache strategy to the dataloader
func WithCache(cache Cache) Option {
return func(l *dataloader) {
l.cache = cache
}
}

// WithTracer adds a tracer to the dataloader
func WithTracer(tracer Tracer) Option {
return func(l *dataloader) {
l.tracer = tracer
}
}

Expand Down
8 changes: 4 additions & 4 deletions dataloader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func TestLoadCacheHit(t *testing.T) {

batch := getBatchFunction(cb, result)
strategy := newMockStrategy()
loader := dataloader.NewDataLoader(1, batch, strategy, cache, dataloader.NewNoOpTracer())
loader := dataloader.NewDataLoader(1, batch, strategy, dataloader.WithCache(cache))

// invoke / assert

Expand All @@ -168,7 +168,7 @@ func TestLoadManyCacheHit(t *testing.T) {

batch := getBatchFunction(cb, result)
strategy := newMockStrategy()
loader := dataloader.NewDataLoader(1, batch, strategy, cache, dataloader.NewNoOpTracer())
loader := dataloader.NewDataLoader(1, batch, strategy, dataloader.WithCache(cache))

// invoke / assert

Expand Down Expand Up @@ -196,7 +196,7 @@ func TestLoadCacheMiss(t *testing.T) {

batch := getBatchFunction(cb, result)
strategy := newMockStrategy()
loader := dataloader.NewDataLoader(1, batch, strategy, cache, dataloader.NewNoOpTracer())
loader := dataloader.NewDataLoader(1, batch, strategy, dataloader.WithCache(cache))

// invoke / assert

Expand All @@ -217,7 +217,7 @@ func TestLoadManyCacheMiss(t *testing.T) {

batch := getBatchFunction(cb, result)
strategy := newMockStrategy()
loader := dataloader.NewDataLoader(1, batch, strategy, cache, dataloader.NewNoOpTracer())
loader := dataloader.NewDataLoader(1, batch, strategy, dataloader.WithCache(cache))

// invoke / assert

Expand Down
44 changes: 36 additions & 8 deletions strategies/once/once.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,55 @@ import (
)

// Options contains the strategy configuration
type Options struct {
// InBackground specifies if the batch function should be executed in a background thread.
InBackground bool // default to false
type options struct {
inBackground bool
}

// Option accepts the dataloader and sets an option on it.
type Option func(*options)

// NewOnceStrategy returns a new instance of the once strategy.
// The Once strategy calls the batch function for each call to the Thunk if InBackground is false.
// Otherwise it runs the batch function in a background go routine and blocks calls to Thunk or
// ThunkMany if the result is not yet fetched.
func NewOnceStrategy(opts Options) func(int, dataloader.BatchFunction) dataloader.Strategy {
func NewOnceStrategy(opts ...Option) func(int, dataloader.BatchFunction) dataloader.Strategy {
return func(_ int, batch dataloader.BatchFunction) dataloader.Strategy {
o := options{}
formatOptions(&o)

// format options
for _, apply := range opts {
apply(&o)
}

return &onceStrategy{
batchFunc: batch,
options: opts,
options: o,
}
}
}

type onceStrategy struct {
batchFunc dataloader.BatchFunction

options Options
options options
}

// ============================================== option setters =============================================

// WithInBackground configures the strategy to load in the background
func WithInBackground() Option {
return func(o *options) {
o.inBackground = true
}
}

// ===========================================================================================================

// Load returns a Thunk which either calls the batch function when invoked or waits for a result from a
// background go routine (blocking if no data is available).
func (s *onceStrategy) Load(ctx context.Context, key dataloader.Key) dataloader.Thunk {
if s.options.InBackground {
if s.options.inBackground {
resultChan := make(chan dataloader.Result)

go func() {
Expand All @@ -65,7 +86,7 @@ func (s *onceStrategy) Load(ctx context.Context, key dataloader.Key) dataloader.
// LoadMany returns a ThunkMany which either calls the batch function when invoked or waits for a result from
// a background go routine (blocking if no data is available).
func (s *onceStrategy) LoadMany(ctx context.Context, keyArr ...dataloader.Key) dataloader.ThunkMany {
if s.options.InBackground {
if s.options.inBackground {
resultChan := make(chan dataloader.ResultMap)

go func() {
Expand All @@ -88,3 +109,10 @@ func (s *onceStrategy) LoadMany(ctx context.Context, keyArr ...dataloader.Key) d
// LoadNoOp has no internal implementation since the once strategy doesn't track the number of calls to
// Load or Loadmany
func (*onceStrategy) LoadNoOp(context.Context) {}

// ================================================= helpers =================================================

// formatOptions configures the default values for the loader
func formatOptions(opts *options) {
opts.inBackground = false
}
12 changes: 4 additions & 8 deletions strategies/once/once_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,8 @@ func TestBatchLoadInForegroundCalled(t *testing.T) {
key := PrimaryKey(1)
result := dataloader.Result{Result: expectedResult, Err: nil}

opts := once.Options{InBackground: false}
batch := getBatchFunction(cb, result)
strategy := once.NewOnceStrategy(opts)(5, batch)
strategy := once.NewOnceStrategy()(5, batch)

// invoke/assert

Expand All @@ -96,9 +95,8 @@ func TestBatchLoadManyInForegroundCalled(t *testing.T) {
key := PrimaryKey(1)
result := dataloader.Result{Result: expectedResult, Err: nil}

opts := once.Options{InBackground: false}
batch := getBatchFunction(cb, result)
strategy := once.NewOnceStrategy(opts)(5, batch)
strategy := once.NewOnceStrategy()(5, batch)

// invoke/assert

Expand Down Expand Up @@ -136,9 +134,8 @@ func TestBatchLoadInBackgroundCalled(t *testing.T) {
key := PrimaryKey(1)
result := dataloader.Result{Result: expectedResult, Err: nil}

opts := once.Options{InBackground: true}
batch := getBatchFunction(cb, result)
strategy := once.NewOnceStrategy(opts)(5, batch)
strategy := once.NewOnceStrategy(once.WithInBackground())(5, batch)

// invoke/assert

Expand Down Expand Up @@ -172,9 +169,8 @@ func TestBatchLoadManyInBackgroundCalled(t *testing.T) {
key := PrimaryKey(1)
result := dataloader.Result{Result: expectedResult, Err: nil}

opts := once.Options{InBackground: true}
batch := getBatchFunction(cb, result)
strategy := once.NewOnceStrategy(opts)(5, batch)
strategy := once.NewOnceStrategy(once.WithInBackground())(5, batch)

// invoke/assert

Expand Down
39 changes: 30 additions & 9 deletions strategies/sozu/sozu.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@ import (
)

// Options contains the strategy configuration
type Options struct {
Timeout time.Duration
type options struct {
timeout time.Duration
}

// Option accepts the dataloader and sets an option on it.
type Option func(*options)

// go routine status values
// Ensure that only one worker go routine is working to call the batch function
const (
Expand All @@ -37,9 +40,16 @@ const (
// a specified timeout duration (see Options) to ensure it doesn't block for too long.
// It attempts to ensure that each call to the batch function includes an array of keys
// whose length is >= 1 and <= the capacity.
func NewSozuStrategy(opts Options) func(int, dataloader.BatchFunction) dataloader.Strategy {
func NewSozuStrategy(opts ...Option) func(int, dataloader.BatchFunction) dataloader.Strategy {
return func(capacity int, batch dataloader.BatchFunction) dataloader.Strategy {
formatOptions(&opts)
// default options
o := options{}
formatOptions(&o)

// format options
for _, apply := range opts {
apply(&o)
}

return &sozuStrategy{
batchFunc: batch,
Expand All @@ -48,13 +58,24 @@ func NewSozuStrategy(opts Options) func(int, dataloader.BatchFunction) dataloade
workerMutex: &sync.Mutex{},
keyChan: make(chan workerMessage, capacity),
goroutineStatus: notRunning,
options: opts,
options: o,

keys: dataloader.NewKeys(capacity),
}
}
}

// ============================================== option setters =============================================

// WithTimeout sets the timeout value for the strategy
func WithTimeout(t time.Duration) Option {
return func(o *options) {
o.timeout = t
}
}

// ===========================================================================================================

type sozuStrategy struct {
counter strategies.Counter
// Track the keys to pass to the batch function. Once len(keys) == cap(keys),
Expand All @@ -69,7 +90,7 @@ type sozuStrategy struct {

goroutineStatus int

options Options
options options
}

type workerMessage struct {
Expand Down Expand Up @@ -230,7 +251,7 @@ func (s *sozuStrategy) startWorker(ctx context.Context) {
if s.counter.Increment() { // hit capacity
r = s.batchFunc(ctx, s.keys)
}
case <-time.After(s.options.Timeout):
case <-time.After(s.options.timeout):
r = s.batchFunc(ctx, s.keys)
}
}
Expand All @@ -246,6 +267,6 @@ func (s *sozuStrategy) startWorker(ctx context.Context) {
// ============================================== helpers =============================================

// formatOptions configures default values for the loader options
func formatOptions(opts *Options) {
opts.Timeout |= 6 * time.Millisecond
func formatOptions(opts *options) {
opts.timeout = 6 * time.Millisecond
}
Loading

0 comments on commit 1e0d7f4

Please sign in to comment.