Skip to content

therenotomorrow/pusher

Repository files navigation

pusher

pusher is a Go library for simple and quick load testing, see more in the examples folder.

The library is built around the Worker concept — the entity who works! Key elements:

  • Worker — the main character
  • Target — what we want to test
  • Gossiper — interface for listening to something interesting
  • Gossip — task lifecycle events
  • Offer — options for hire the Worker

Quick Start

package main

import (
	"context"
	"time"

	"github.com/therenotomorrow/pusher"
)

func main() {
	target := func(_ context.Context) (pusher.Result, error) {
		return time.Now(), nil // time.Time support pusher.Result interface
	}

	// run target with 50 RPS for 1 minute
	_ = pusher.Work(50, time.Minute, target)
}

Slow Start

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/therenotomorrow/pusher"
)

func main() {
	// Your function to test
	target := func(ctx context.Context) (pusher.Result, error) {
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		default:
		}

		now := time.Now().UTC()
		if now.Second()%2 == 0 {
			time.Sleep(100 * time.Millisecond)
		}

		return Result(now.String()), nil
	}

	// Some of your gossips listeners
	observers := make([]*Observer, 0)

	gossipers := make([]pusher.Gossiper, 0)
	for _, when := range []pusher.When{pusher.Canceled, pusher.BeforeTarget, pusher.AfterTarget} {
		observer := &Observer{done: make(chan struct{}), when: when, count: 0}

		observers = append(observers, observer)
		gossipers = append(gossipers, observer)
	}

	// run target with 100 RPS for 1 minute with max 10 requests concurrent
	// and add 3 listeners for collect statistics
	err := pusher.Work(
		100,                              // rps
		time.Minute,                      // duration
		target,                           // target
		pusher.WithOvertime(10),          // concurrent limit
		pusher.WithGossips(gossipers...), // our gossipers
	)

	// check what was done and collect
	fmt.Println("We're done with error:", err)
	fmt.Println("Canceled:", observers[0].count)
	fmt.Println("Received:", observers[1].count)
	fmt.Println("Processed:", observers[2].count)

	// Output:
	// We're done with error: context deadline exceeded
	// Canceled: 256
	// Received: 5744
	// Processed: 5744
}

type Result string

func (r Result) String() string {
	return string(r)
}

type Observer struct {
	done  chan struct{}
	when  pusher.When
	count int
}

func (o *Observer) Listen(_ context.Context, _ *pusher.Worker, gossips <-chan *pusher.Gossip) {
	defer close(o.done)

	for gossip := range gossips {
		if gossip.When == o.when {
			o.count++
		}
	}
}

func (o *Observer) Stop() {
	<-o.done
}

Development

System Requirements

go version
# go version go1.25.2 or higher

just --version
# just 1.42.4 or higher (https://just.systems/)

Download sources

PROJECT_ROOT=pusher
git clone https://github.com/therenotomorrow/pusher.git "$PROJECT_ROOT"
cd "$PROJECT_ROOT"

Setup dependencies

# install dependencies
go mod tidy

# check code integrity
just code test # see other recipes by calling `just`

# setup safe development (optional)
git config --local core.hooksPath .githooks

Project Structure

pusher/
├── config.go   # Configuration and functional options
├── errors.go   # Error definitions
├── gossip.go   # Event system and telemetry
├── pusher.go   # Main API and high-level functions
└── worker.go   # Worker implementation and execution logic

Testing

# run quick checks
just test smoke # or just test

# run with coverage
just test cover

License

MIT License. See the LICENSE file for details.