dataloadgen

package module
v0.0.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 13, 2025 License: MIT Imports: 7 Imported by: 16

README

dataloadgen

Go Reference

dataloadgen is an implementation of a pattern popularized by Facebook's DataLoader.

It works as follows:

  • A Loader object is created per graphql request.
  • Each of many concurrently executing graphql resolver functions call Load() on the Loader object with different keys. Let's say K1, K2, K3
  • Each call to Load() with a new key is delayed slightly (a few milliseconds) so that the Loader can load them together.
  • The customizable fetch function of the loader takes a list of keys and loads data for all of them in a single batched request to the data storage layer. It might send [K1,K2,K3] and get back [V1,V2,V3]. The order of the keys must match the order of the values.
    • Alternatively, the mappedFetch function of the loader takes a list of keys and returns a map instead of a list. It might send [K1, K2, K3] and get back {K1: V1, K2: V2, K3: V3}.
  • The Loader takes care of sending the right result to the right caller and the result is cached for the duration of the graphql request.

[!NOTE] The fetch method expects the returned list to correspond to the provided keys in the same order. Alternatively, the mappedFetch allows returning a map, ensuring correct ordering and automatic creation of the ErrNotFound error for missing values.

Usage:

go get github.com/vikstrous/dataloadgen

See the usage example in the documentation:

package main

import (
	"context"
	"fmt"
	"strconv"

	"github.com/vikstrous/dataloadgen"
)

// fetchFn/mappedFetchFn is shown as a function here, but it might work better as a method
// ctx is the context from the first call to Load for the current batch
func fetchFn(ctx context.Context, keys []string) (ret []int, errs []error) {
    for _, key := range keys {
        num, err := strconv.ParseInt(key, 10, 32)
        ret = append(ret, int(num))
        errs = append(errs, err)
    }
    return
}
func mappedFetchFn(ctx context.Context, keys []string) (ret map[string]int, err error) {
	ret = make(map[string]int, len(keys))
	errs := make(map[string]error, len(keys))
    for _, key := range keys {
        num, err := strconv.ParseInt(key, 10, 32)
        ret[key] = int(num)
        errs[key] = err
    }
	// You can also return a single error, returned for every key's load invocation, instead of this MappedFetchError.
    err = dataloadgen.MappedFetchError[string](errs)
    return
}

func main() {
    ctx := context.Background()
	
    // Per-request setup code. Either:
    loader := dataloadgen.NewLoader(fetchFn)
    // or
    loader := dataloadgen.NewMappedLoader(mappedFetchFn)
	
    // In every graphql resolver:
    result, err := loader.Load(ctx, "1")
    if err != nil {
        panic(err)
    }
    fmt.Println(result)
}

Comparison to others

  • dataloaden uses code generation and has similar performance
  • dataloader does not use code generation but has much worse performance and is more difficult to use
  • yckao/go-dataloader does not use code generation but has much worse performance and is very similar to dataloader.

The benchmarks in this repo show that this package is faster than all of the above and I also find it easier to use.

Benchmark data as CSV
Benchmark,Package,iterations,ns/op,B/op,allocs/op
init-8,graph-gophers/dataloader,"9,242,047.00",130.50,208.00,3.00
init-8,vektah/dataloaden,"1,000,000,000.00",0.27,0.00,0.00
init-8,yckao/go-dataloader,"3,153,999.00",402.10,400.00,10.00
init-8,vikstrous/dataloadgen,"10,347,595.00",114.90,128.00,3.00
cached-8,graph-gophers/dataloader,"4,669.00","222,072.00","25,307.00",522.00
cached-8,vektah/dataloaden,"1,243.00","1,037,044.00","5,234.00",110.00
cached-8,yckao/go-dataloader,"2,312.00","580,860.00","2,273.00",130.00
cached-8,vikstrous/dataloadgen,"1,552.00","824,939.00",776.00,15.00
unique_keys-8,graph-gophers/dataloader,"12,334.00","97,118.00","56,314.00",945.00
unique_keys-8,vektah/dataloaden,"36,489.00","32,507.00","37,514.00",227.00
unique_keys-8,yckao/go-dataloader,"8,055.00","133,224.00","50,180.00",747.00
unique_keys-8,vikstrous/dataloadgen,"42,943.00","27,257.00","22,255.00",230.00
10_concurrently-8,graph-gophers/dataloader,326.00,"11,119,367.00","5,574,460.00","164,247.00"
10_concurrently-8,vektah/dataloaden,100.00,"194,627,574.00","898,977.00","19,502.00"
10_concurrently-8,yckao/go-dataloader,278.00,"10,972,399.00","314,963.00","29,558.00"
10_concurrently-8,vikstrous/dataloadgen,643.00,"8,249,158.00","43,474.00",806.00
all_in_one_request-8,graph-gophers/dataloader,28.00,"39,954,324.00","27,475,136.00","158,321.00"
all_in_one_request-8,vektah/dataloaden,328.00,"3,713,407.00","3,533,086.00","41,368.00"
all_in_one_request-8,yckao/go-dataloader,132.00,"9,060,571.00","4,886,722.00","102,564.00"
all_in_one_request-8,vikstrous/dataloadgen,375.00,"3,206,175.00","2,518,498.00","41,582.00"

To run the benchmarks, run go test -bench=. . -run BenchmarkAll -benchmem from the benchmark directory.

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrNotFound = errors.New("dataloadgen: not found")

ErrNotFound is generated for you when using NewMappedLoader and not returning any data for a given key

Functions

This section is empty.

Types

type ErrorSlice added in v0.0.5

type ErrorSlice []error

ErrorSlice represents a list of errors that contains at least one error

func (ErrorSlice) Error added in v0.0.5

func (e ErrorSlice) Error() string

Error implements the error interface

type Loader

type Loader[KeyT comparable, ValueT any] struct {
	// contains filtered or unexported fields
}

Loader batches and caches requests

Example
package main

import (
	"context"
	"fmt"
	"strconv"
	"time"

	"github.com/vikstrous/dataloadgen"
)

func main() {
	ctx := context.Background()

	loader := dataloadgen.NewLoader(func(ctx context.Context, keys []string) (ret []int, errs []error) {
		for _, key := range keys {
			num, err := strconv.ParseInt(key, 10, 32)
			ret = append(ret, int(num))
			errs = append(errs, err)
		}
		return
	},
		dataloadgen.WithBatchCapacity(1),
		dataloadgen.WithWait(16*time.Millisecond),
	)
	one, err := loader.Load(ctx, "1")
	if err != nil {
		panic(err)
	}

	mappedLoader := dataloadgen.NewMappedLoader(func(ctx context.Context, keys []string) (ret map[string]int, err error) {
		ret = make(map[string]int, len(keys))
		errs := make(map[string]error, len(keys))
		for _, key := range keys {
			num, err := strconv.ParseInt(key, 10, 32)
			ret[key] = int(num)
			errs[key] = err
		}
		err = dataloadgen.MappedFetchError[string](errs)
		return
	},
		dataloadgen.WithBatchCapacity(1),
		dataloadgen.WithWait(16*time.Millisecond),
	)
	two, err := mappedLoader.Load(ctx, "2")
	if err != nil {
		panic(err)
	}
	fmt.Println(one, ",", two)
}
Output:

1 , 2

func NewLoader

func NewLoader[KeyT comparable, ValueT any](fetch func(ctx context.Context, keys []KeyT) ([]ValueT, []error), options ...Option) *Loader[KeyT, ValueT]

NewLoader creates a new GenericLoader given a fetch, wait, and maxBatch

func NewMappedLoader added in v0.0.7

func NewMappedLoader[KeyT comparable, ValueT any](mappedFetch func(ctx context.Context, keys []KeyT) (map[KeyT]ValueT, error), options ...Option) *Loader[KeyT, ValueT]

NewMappedLoader creates a new GenericLoader given a mappedFetch, wait and maxBatch

func (*Loader[KeyT, ValueT]) Clear

func (l *Loader[KeyT, ValueT]) Clear(key KeyT)

Clear the value at key from the cache, if it exists

func (*Loader[KeyT, ValueT]) Load

func (l *Loader[KeyT, ValueT]) Load(ctx context.Context, key KeyT) (ValueT, error)

Load a ValueT by key, batching and caching will be applied automatically

func (*Loader[KeyT, ValueT]) LoadAll

func (l *Loader[KeyT, ValueT]) LoadAll(ctx context.Context, keys []KeyT) ([]ValueT, error)

LoadAll fetches many keys at once. It will be broken into appropriate sized sub batches depending on how the loader is configured

func (*Loader[KeyT, ValueT]) LoadAllThunk

func (l *Loader[KeyT, ValueT]) LoadAllThunk(ctx context.Context, keys []KeyT) func() ([]ValueT, error)

LoadAllThunk returns a function that when called will block waiting for a ValueT. This method should be used if you want one goroutine to make requests to many different data loaders without blocking until the thunk is called.

func (*Loader[KeyT, ValueT]) LoadThunk

func (l *Loader[KeyT, ValueT]) LoadThunk(ctx context.Context, key KeyT) func() (ValueT, error)

LoadThunk returns a function that when called will block waiting for a ValueT. This method should be used if you want one goroutine to make requests to many different data loaders without blocking until the thunk is called.

func (*Loader[KeyT, ValueT]) Prime

func (l *Loader[KeyT, ValueT]) Prime(key KeyT, value ValueT) bool

Prime the cache with the provided key and value. If the key already exists, no change is made and false is returned. (To forcefully prime the cache, clear the key first with loader.Clear(key).Prime(key, value).)

type MappedFetchError added in v0.0.7

type MappedFetchError[KeyT comparable] map[KeyT]error

func (MappedFetchError[KeyT]) Error added in v0.0.7

func (e MappedFetchError[KeyT]) Error() string

type Option added in v0.0.2

type Option func(*loaderConfig)

Option allows for configuration of loader fields.

func WithBatchCapacity added in v0.0.2

func WithBatchCapacity(c int) Option

WithBatchCapacity sets the batch capacity. Default is 0 (unbounded)

func WithTracer added in v0.0.4

func WithTracer(tracer trace.Tracer) Option

func WithWait added in v0.0.2

func WithWait(d time.Duration) Option

WithWait sets the amount of time to wait before triggering a batch. Default duration is 16 milliseconds.

Directories

Path Synopsis
benchmark module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL