@h12.io

Run: Graceful Goroutine Orchestration

14 December 2018

GoDoc

Overview

While Go provides goroutines, channels and selects as first-class citizens to support concurrent programming, it is not trivial to combine these elements to address important concerns of goroutine orchestration, e.g. error handling, panic recovery, goroutine leak prevention, goroutine reuse, goroutine throttle and logging.

The package provides a mini-framework to address those cross-cutting concerns.

Quick start

go get -u h12.io/run

Here is an example illustrating the usage of the goroutine pool and the group. The task is described in the “Google Search 2.0” page from this slide.

// the goroutine pool
pool := run.NewGoroutinePool(
	run.Max(8),                // the pool contains maximum 8 goroutines
	run.IdleTime(time.Minute), // a goroutine will stay in idle for maximum 1 minute before exiting
)

// the group
// the goroutine pool might have longer lifespan than the group
group := run.NewGroup(
	context.Background(), // a context that can cancel the whole group
	run.Pool(pool),       // the goroutine pool used by the group
	run.Recover(true),    // recover from panic and returns the PanicError
	run.Log(func(info *run.LogInfo) { // a log function for all starts/stops
		log.Print(info)
	}),
)

searches := []*GoogleSearch{
	{Search: Web, Query: "golang"},
	{Search: Image, Query: "golang"},
	{Search: Video, Query: "golang"},
}
for _, search := range searches {
	// start searching in parallel
	if err := group.Go(search); err != nil {
		log.Fatal(err)
	}
}

// wait for all searches stop
if err := group.Wait(); err != nil {
	log.Fatal(err)
}

for _, search := range searches {
	fmt.Println(search.Result)
}

See the full example here.

Design

The package is built around the concept of a runner.

type Runner interface {
	Run(context.Context) error
}

A correct implementation of a runner should satisify the following conditions:

  • blocks when the work is on going
  • returns when all work is done, an error occurred or context is cancelled

With goroutine pool and group in the package, the user does not need to use the go statement explicitly, but only needs to implement their objects satisfying the Runner interface.

A Group is useful when multiple concurrent sub-tasks needed to be combined as a single task (the task failed when one of them failed, every sub-task should be cancelled when the task is cancelled).

A Pool is useful when there are many short-lived gorotuines.

Group can be built upon pool, not vice versa.

How to Start Godoc on Mac

5 May 2018

Fish:

godoc -http=:6060 &; disown (pidof godoc)

Bash:

godoc -http=:6060 &; disown `pidof godoc`

And pidof can be installed with Homebrew.

Tmux Cheatsheet on Mac

20 April 2018

Create a new session with iTerm2 integration

tmux -CC new -s [session-name]

Attach to a session with iTerm2 integration

tmux -CC attach -t [session-name]

How to Diff two JSON Files

10 April 2018

Just sort the keys first!

Example:

cat a.json | jq --sort-keys . > aa.json
cat b.json | jq --sort-keys . > bb.json
vimdiff aa.json bb.json

Pagination Done Right

28 February 2018

Server side pagination is intrinsically not accurate, as long as the data is dynamic.

The data items could be inserted, deleted or changed on the server side while the user goes forward and backward among the pages.

However, there is an algorithm that can keep the pagination as stable as possible:

  1. encode the id and sorting fields of last value in a page as the continue-token
  2. return the continue-token along with each page
  3. the client must pass the continue-token to fetch the next page
  4. the next page starts with value > continue-token || (value == continue-token && value.id > continue-token.id)

Reference

buid: Bipartite Unique Identifier

15 November 2017

A BUID is a 128-bit unique ID composed of two 64-bit parts: shard and key.

It is not only a unique ID, but also contains the sharding information, so that the messages with the same BUID could be stored together within the same DB shard.

Also, when a message is stored in a shard, the shard part of the BUID can be trimmed off to save the space, and only the key part needs to be stored as the primary key.

Bigendian is chosen to make each part byte-wise lexicographic sortable.

The string representation uses basex 62 encoding.

How to Measure Response Time with Curl

20 June 2017
curl [args] --write-out "%{time_total}s" --output /dev/null --silent [URL]