batch

package module
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: May 3, 2022 License: MIT Imports: 6 Imported by: 3

README

Build Go Reference Go Report Card codecov

What it can be used for?

To speed up application performance without sacrificing data consistency and data durability or making source code/architecture complex.

The batch package simplifies writing Go applications that process incoming requests (HTTP, GRPC etc.) in a batch manner: instead of processing each request separately, they group incoming requests to a batch and run whole group at once. This method of processing can significantly speed up the application and reduce the consumption of disk, network or CPU.

The batch package can be used to write any type of servers that handle thousands of requests per second. Thanks to this small library, you can create relatively simple code without the need to use low-level data structures.

Why batch processing improves performance?

Normally a web application is using following pattern to modify data in the database:

  1. Load resource from database. Resource is some portion of data such as set of records from relational database, document from Document-oriented database or value from KV store. Lock the entire resource pessimistically or optimistically (by reading version number).
  2. Apply change to data
  3. Save resource to database. Release the pessimistic lock. Or run atomic update with version check (optimistic lock).

But such architecture does not scale well if number of requests for a single resource is very high (meaning hundreds or thousands of requests per second). The lock contention in such case is very high and database is significantly overloaded. Practically, the number of concurrent requests is limited.

One solution to this problem is to reduce the number of costly operations. Because a single resource is loaded and saved thousands of times per second we can instead:

  1. Load the resource once (let's say once per second)
  2. Execute all the requests from this period of time on an already loaded resource. Run them all sequentially to keep things simple and data consistent.
  3. Save the resource and send responses to all clients if data was stored successfully.

Such solution could improve the performance by a factor of 1000. And resource is still stored in a consistent state.

The batch package does exactly that. You configure the duration of window, provide functions to load and save resource and once the request comes in - you run a function:

// Set up the batch processor:
processor := batch.StartProcessor(
    batch.Options[*YourResource]{ // YourResource is your own Go struct
        MinDuration:  100 * time.Millisecond,
        LoadResource: func(ctx context.Context, resourceKey string) (*YourResource, error){
            // resourceKey uniquely identifies the resource
            ...
        },
        SaveResource: ...,
    },
)

// And use the processor inside http/grpc handler or technology-agnostic service.
// ResourceKey can be taken from request parameter.
err := processor.Run(resourceKey, func(r *YourResource) {
    // Here you put the code which will executed sequentially inside batch  
})

For real-life example see example web application.

Installation

# Add batch to your Go module:
go get github.com/elgopher/batch

Please note that at least Go 1.18 is required. The package is using generics, which was added in 1.18.

Scaling out

Single Go http server is able to handle up to tens of thousands of requests per second on a commodity hardware. This is a lot, but very often you also need:

  • high availability (if one server goes down you want other to handle the traffic)
  • you want to handle hundred-thousands or millions of requests per second

For both cases you need to deploy multiple servers and put a load balancer in front of them. Please note though, that you have to carefully configure the load balancing algorithm. Round-robin is not an option here, because sooner or later you will have problems with locking (multiple server instances will run batches on the same resource). Ideal solution is to route requests based on parameters or URL. For example some http parameter could be a resource key. You can instruct load balancer to calculate hash on this parameter value and always route requests with this param value to the same backend (of course if all backends are still available).

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ProcessorStopped = errors.New("run failed: processor is stopped")

Functions

func GoroutineNumberForKey

func GoroutineNumberForKey(key string, goroutines int) int

Types

type Options

type Options[Resource any] struct {
	// All batches will be run for at least MinDuration.
	//
	// By default, 100ms.
	MinDuration time.Duration
	// Batch will have timeout with MaxDuration. Context with this timeout will be passed to
	// LoadResource and SaveResource functions, which can abort the batch by returning an error.
	//
	// By default, 2*MinDuration.
	MaxDuration time.Duration
	// LoadResource loads resource with given key from a database. Returning an error aborts the batch.
	// This function is called in the beginning of each new batch. Context passed as a first parameter
	// has a timeout calculated using batch MaxDuration. You can use this information to abort loading resource
	// if it takes too long.
	//
	// By default, returns zero-value Resource.
	LoadResource func(_ context.Context, key string) (Resource, error)
	// SaveResource saves resource with given key to a database. Returning an error aborts the batch.
	// This function is called at the end of each batch. Context passed as a first parameter
	// has a timeout calculated using batch MaxDuration. You can use this information to abort saving resource
	// if it takes too long.
	//
	// By default, does nothing.
	SaveResource func(_ context.Context, key string, _ Resource) error
	// GoRoutines specifies how many goroutines should be used to run batch operations.
	//
	// By default, 16 * number of CPUs.
	GoRoutines int
	// GoRoutineNumberForKey returns go-routine number which will be used to run operation on
	// a given resource key. This function is crucial to properly serialize requests.
	//
	// This function must be deterministic - it should always return the same go-routine number
	// for given combination of key and goroutines parameters.
	//
	// By default, GoroutineNumberForKey function is used. This implementation calculates hash
	// on a given key and use modulo to calculate go-routine number.
	GoRoutineNumberForKey func(key string, goroutines int) int
}

Options represent parameters for batch.Processor. They should be passed to StartProcessor function. All options (as the name suggest) are optional and have default values.

type Processor

type Processor[Resource any] struct {
	// contains filtered or unexported fields
}

Processor represents instance of batch processor which can be used to issue operations which run in a batch manner.

func StartProcessor

func StartProcessor[Resource any](options Options[Resource]) *Processor[Resource]

StartProcessor starts batch processor which will run operations in batches.

Please note that Processor is a go-routine pool internally and should be stopped when no longer needed. Please use Processor.Stop method to stop it.

func (*Processor[Resource]) Run

func (p *Processor[Resource]) Run(key string, _operation func(Resource)) error

Run lets you run an operation on a resource with given key. Operation will run along other operations in batches. If there is no pending batch then the new batch will be started and will run for at least MinDuration. After the MinDuration no new operations will be accepted and SaveResource function will be called.

Operations are run sequentially. No manual locking is required inside operation. Operation should be fast, which basically means that any I/O should be avoided at all cost.

Run ends when the entire batch has ended. It returns error when batch is aborted or processor is stopped. Only LoadResource and SaveResource functions can abort the batch by returning an error. If error was reported for a batch all Run calls assigned to this batch will get this error.

func (*Processor[Resource]) Stop

func (p *Processor[Resource]) Stop()

Stop ends all running batches. No new operations will be accepted. Stop blocks until all pending batches are ended and resources saved.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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