greddis

package module
v0.0.0-...-cd52361 Latest Latest
Warning

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

Go to latest
Published: Apr 24, 2021 License: MIT Imports: 18 Imported by: 0

README

Greddis

Build Status CodeQL Status codecov Total alerts FOSSA Status Go Report Card Go Reference

Note: Currently Greddis only implements Pub/Sub/Del/Set/Get and is more of a proof of concept than a fully implemented client as of now.

Greddis focus is high performance and letting the user of the library control its allocations. It is built using learnings from database/sql/driver implementations in the standard package. Many implementations of these interfaces provide consistent and high performance through good use of buffer pools, as well as the connection pool implementation in the standard library itself.

Furthermore, it is compatible with any implementation of Valuer/Scanner from database/sql as long as they have a []byte implementation (as all data returned from Redis is []byte).

Roadmap

  • Pub/sub commands
  • Sentinel support
  • Set commands
  • List commands

Helping out?

To run the unit tests

$ go get .
$ go generate
$ go test

And to run the integration tests

$ go test -tags=integration

To generate a coverage report

$ go test -coverprofile=coverage.out
$ go tool cover -html=coverage.out

To run the benchmarks

$ cd benchmarks/
$ go test -bench=.

How does it compare to Redigo/GoRedis?

Efficient use of connections?
Client
Greddis Yes
Redigo No
GoRedis Yes

Redigo returns the underlying connection to execute your commands against (given Go's concurrency model, this gives you an insignificant performance benefit), whilst Greddis and GoRedis relies on a higher-level abstraction called "client" which is closer to the database/sql/driver abstraction. This means that between each interaction with the redis client the connection is being put back onto the pool. In high concurrency situations this is preferrable, as holding on to the connection across calls means your connection pool will grow faster than if you were to return it immediately (again see database/sql pool implementation).

Implements Redis Client protocol?
Client
Greddis Yes
Redigo No
GoRedis Yes

According to the Redis Serialization Protocol (RESP Specification), client libraries should use the RESP protocol to make requests as well as parse it for responses. The other option is to use their "human readable" Telnet protocol, which Redigo implements. The problem is that this does not allow the Redis server to up-front allocate the entire memory section required to store the request before parsing it, and thus it needs to iteratively parse the return in chunks until it reaches the end.

Pools request and response buffers to amortize allocation cost?
Client
Greddis Yes
Redigo No
GoRedis No

Neither Redigo nor GoRedis pools its buffers for reuse and allocates them on the stack per request. This becomes rather heavy on performance, especially as response sizes grow. You can see in the Get benchmarks for the three clients that GoRedis allocates the result no less than three times on the stack and Redigo allocates once. (Reference, Get10000b benchmark)

Allows for zero-copy parsing of response?
Client
Greddis Yes
Redigo No (but kind of)
GoRedis No

The Result.Scan interface provided by the database/sql package is designed to allow you to do zero-alloc/zero-copy result parsing. GoRedis does not have a scan command at all. And Redigo, whilst having a Scan command, still does one copy per response before passing it to Scan. It also uses reflection on the type you send in to ensure it is of the same type as what's been parsed, rather than sending in the raw []byte slice for casting (what database/sql does). Greddis has opted to implement the Result.Scan interface more closely and only supports the Result.Scan interface for responses, allowing the user to control the casting and parsing depending on type sent in to Result.Scan.

Benchmarks

The benchmarks are run against a real redis server on localhost (network stack), so no mock and no direct socket connection.

If we can maintain a single connection, how fast can we go? Also note the SockSingleFunc benchmark is implemented using syscalls, so it blocks the entire go-routine thread rather than using epoll whilst waiting for the response, so it is not realistic to use.

BenchmarkNetSingleBufIO-8   	   76858	     14251 ns/op	      16 B/op	       2 allocs/op
BenchmarkSockSingleFunc-8   	  126729	      8299 ns/op	       0 B/op	       0 allocs/op
BenchmarkNetSingleFunc-8    	   80509	     14925 ns/op	       8 B/op	       1 allocs/op
BenchmarkNetSingle-8        	   82456	     14629 ns/op	       0 B/op	       0 allocs/op

The next question to answer is "Which connection pool implementation is most efficient?" We put channels v sync.Pool, vs Atomic Pool (keep track of available connections in an atomic.Int), vs Semaphore Pool (using a semaphore) and lastly Dropbox's net2.Pool package.

BenchmarkNetChanPool-8      	   72476	     15093 ns/op	       0 B/op	       0 allocs/op
BenchmarkNetSyncPool-8      	   74612	     15654 ns/op	      32 B/op	       1 allocs/op
BenchmarkNetAtomicPool-8    	   81070	     15285 ns/op	      32 B/op	       1 allocs/op
BenchmarkNetSemPool-8       	   79828	     15712 ns/op	      32 B/op	       1 allocs/op
BenchmarkNet2Pool-8         	   77632	     16344 ns/op	     312 B/op	       5 allocs/op

After having picked the most efficient (using a channel for the pool) this was picked for implementation in Greddis. It was also the only one with zero allocs, so yay! The benchmarks following is comparing Redigo, GoRedis and Greddis at different object sizes doing pubsub, set and get.

BenchmarkDrivers/GoRedisPubSub1000b-8         	   50485	     22692 ns/op	    1520 B/op	      20 allocs/op
BenchmarkDrivers/GoRedisPubSub10000b-8        	   38638	     31074 ns/op	   10736 B/op	      20 allocs/op
BenchmarkDrivers/GoRedisPubSub100000b-8       	   17682	     67689 ns/op	  106993 B/op	      20 allocs/op
BenchmarkDrivers/GoRedisPubSub10000000b-8     	     150	   7995486 ns/op	10003132 B/op	      21 allocs/op
BenchmarkDrivers/RedigoPubSub1000b-8          	   71019	     16286 ns/op	    1480 B/op	      18 allocs/op
BenchmarkDrivers/RedigoPubSub10000b-8         	   43623	     25570 ns/op	   10696 B/op	      18 allocs/op
BenchmarkDrivers/RedigoPubSub100000b-8        	    9266	    109194 ns/op	  106966 B/op	      18 allocs/op
BenchmarkDrivers/RedigoPubSub10000000b-8      	     100	  12289723 ns/op	10103156 B/op	      19 allocs/op
BenchmarkDrivers/GreddisPubSub1000b-8         	   54388	     19923 ns/op	       2 B/op	       0 allocs/op
BenchmarkDrivers/GreddisPubSub10000b-8        	   41598	     27145 ns/op	       4 B/op	       0 allocs/op
BenchmarkDrivers/GreddisPubSub100000b-8       	   20304	     58576 ns/op	      25 B/op	       0 allocs/op
BenchmarkDrivers/GreddisPubSub10000000b-8     	     166	   7100689 ns/op	  262974 B/op	       2 allocs/op
BenchmarkDrivers/GoRedisGet1000b-8            	   72638	     15873 ns/op	    3348 B/op	      15 allocs/op
BenchmarkDrivers/GoRedisGet10000b-8           	   52407	     22818 ns/op	   31222 B/op	      15 allocs/op
BenchmarkDrivers/GoRedisGet100000b-8          	   12256	    126058 ns/op	  426675 B/op	      18 allocs/op
BenchmarkDrivers/GoRedisGet10000000b-8        	     148	   7967071 ns/op	40011394 B/op	      21 allocs/op
BenchmarkDrivers/GoRedisSet1000b-8            	   64404	     17622 ns/op	     226 B/op	       7 allocs/op
BenchmarkDrivers/GoRedisSet10000b-8           	   50204	     24055 ns/op	     226 B/op	       7 allocs/op
BenchmarkDrivers/GoRedisSet100000b-8          	   26132	     47400 ns/op	     226 B/op	       7 allocs/op
BenchmarkDrivers/GoRedisSet10000000b-8        	     332	   4064425 ns/op	     282 B/op	       7 allocs/op
BenchmarkDrivers/RedigoGet1000b-8             	   69430	     18184 ns/op	    1220 B/op	       9 allocs/op
BenchmarkDrivers/RedigoGet10000b-8            	   49509	     24123 ns/op	   10443 B/op	       9 allocs/op
BenchmarkDrivers/RedigoGet100000b-8           	   21214	     59771 ns/op	  106758 B/op	       9 allocs/op
BenchmarkDrivers/RedigoGet10000000b-8         	     288	   4908264 ns/op	10003177 B/op	      11 allocs/op
BenchmarkDrivers/RedigoSet1000b-8             	   70467	     16785 ns/op	      99 B/op	       5 allocs/op
BenchmarkDrivers/RedigoSet10000b-8            	   51565	     22755 ns/op	      99 B/op	       5 allocs/op
BenchmarkDrivers/RedigoSet100000b-8           	   12538	     98983 ns/op	     100 B/op	       5 allocs/op
BenchmarkDrivers/RedigoSet10000000b-8         	     138	   8812137 ns/op	     216 B/op	       5 allocs/op
BenchmarkDrivers/GreddisGet1000b-8            	   68478	     17082 ns/op	       1 B/op	       0 allocs/op
BenchmarkDrivers/GreddisGet10000b-8           	   65054	     19505 ns/op	       1 B/op	       0 allocs/op
BenchmarkDrivers/GreddisGet100000b-8          	   30398	     39965 ns/op	      14 B/op	       0 allocs/op
BenchmarkDrivers/GreddisGet10000000b-8        	     276	   4067938 ns/op	  157934 B/op	       0 allocs/op
BenchmarkDrivers/GreddisSet1000b-8            	   69969	     16474 ns/op	       1 B/op	       0 allocs/op
BenchmarkDrivers/GreddisSet10000b-8           	   47844	     23079 ns/op	       1 B/op	       0 allocs/op
BenchmarkDrivers/GreddisSet100000b-8          	   25050	     47438 ns/op	       3 B/op	       0 allocs/op
BenchmarkDrivers/GreddisSet10000000b-8        	     340	   4707750 ns/op	     129 B/op	       0 allocs/op

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrConnDial = errors.New("Received error whilst establishing connection")

ErrConnDial error during dial

View Source
var ErrConnRead = errors.New("Received error whilst reading from connection")

ErrConnRead received error when reading from connection

View Source
var ErrConnWrite = errors.New("Received error whilst writing to connection")

ErrConnWrite error when writing to connection

View Source
var ErrMalformedString = errors.New("Expected CRLF terminated string, but did not receive one")

ErrMalformedString received expected length, but it isn't terminated properly

View Source
var ErrMixedTopicTypes = errors.New("All the topics need to be either of type string or of RedisPattern, but not of both")

ErrMixedTopicTypes is given when you pass in arguments of both RedisPattern and String

View Source
var ErrNoMoreRows = errors.New("No more rows")

ErrNoMoreRows is returned when an ArrayResult does not contain any more entries when Next() is called

View Source
var ErrOptsDialAndURL = errors.New("Both Dial and URL is set, can only set one")

ErrOptsDialAndURL cannot combine both dial and URL for pool options

View Source
var ErrRetryable = errors.New("Temporary error, please retry!")

ErrRetryable is an error that can be retried

View Source
var ErrWrongPrefix = errors.New("Wrong prefix on string")

ErrWrongPrefix returns when we expected a certain type prefix, but received another

Functions

func ErrWrongToken

func ErrWrongToken(expToken byte, token byte) error

ErrWrongToken is used internally in the Redis Reader when it checks whether the token expected and this is not the case

func ErrWrongType

func ErrWrongType(v interface{}, expected string) error

ErrWrongType is returned when the function receives an unsupported type

func ScanArray

func ScanArray(r *bufio.Reader) (int, error)

func ScanBulkString

func ScanBulkString(r *bufio.Reader) (int, error)

func ScanInteger

func ScanInteger(r *bufio.Reader) (int, error)

func ScanSimpleString

func ScanSimpleString(r *bufio.Reader) (int, error)

Types

type ArrayReader

type ArrayReader struct {
	// contains filtered or unexported fields
}

func NewArrayReader

func NewArrayReader(r *Reader) *ArrayReader

func (*ArrayReader) Err

func (a *ArrayReader) Err() error

func (*ArrayReader) Expect

func (r *ArrayReader) Expect(vars ...string) error

Expect does an Any byte comparison with the values passed in against the next value in the array

func (*ArrayReader) Init

func (a *ArrayReader) Init(defaultScanFunc ScanFunc) error

func (*ArrayReader) Len

func (a *ArrayReader) Len() int

Len returns the length of the ArrayReader

func (*ArrayReader) Next

func (r *ArrayReader) Next() *ArrayReader

Next prepares the next row to be used by `Scan()`, it returns either a "no more rows" error or a connection/read error will be wrapped.

func (*ArrayReader) NextIs

func (r *ArrayReader) NextIs(scanFunc ScanFunc) *ArrayReader

func (*ArrayReader) Scan

func (r *ArrayReader) Scan(dst interface{}) error

Scan operates the same as `Scan` on a single result

func (*ArrayReader) SwitchOnNext

func (r *ArrayReader) SwitchOnNext() string

SwitchOnNext returns a string value of the next value in the ArrayReader which is a pointer to the underlying byte slice - as the name implies, it is mostly implemented for switch cases where there's a guarantee that the next Scan/SwitchOnNext call will happen after the last use of this value. If you want to not only switch on the value or do a one-off comparison, please use Scan() instead.

type ArrayWriter

type ArrayWriter struct {
	// contains filtered or unexported fields
}

func NewArrayWriter

func NewArrayWriter(bufw *bufio.Writer) *ArrayWriter

func (*ArrayWriter) Add

func (w *ArrayWriter) Add(items ...interface{}) error

func (*ArrayWriter) AddString

func (w *ArrayWriter) AddString(items ...string) *ArrayWriter

func (*ArrayWriter) Flush

func (w *ArrayWriter) Flush() error

func (*ArrayWriter) Init

func (w *ArrayWriter) Init(length int) *ArrayWriter

func (*ArrayWriter) Len

func (w *ArrayWriter) Len() int

func (*ArrayWriter) Reset

func (w *ArrayWriter) Reset(wr io.Writer)

type Client

type Client interface {
	// Get executes a get command on a redis server and returns a Result type, which you can use Scan
	// on to get the result put into a variable
	Get(ctx context.Context, key string) (*Result, error)
	// Set sets a Value in redis, it accepts a TTL which can be put to 0 to disable TTL
	Set(ctx context.Context, key string, value driver.Value, ttl int) error
	// Del removes a key from the redis server
	Del(ctx context.Context, key string) error
	// Ping pings the server, mostly an internal command to ensure the subscription connection is still working
	Ping(ctx context.Context) error
	// Publish publishes a message to the selected topic, it returns an int of the number of clients
	// that received the message
	Publish(ctx context.Context, topic string, message driver.Value) (recvCount int, err error)
}

Client is the interface to interact with Redis. It uses connections with a single buffer attached, much like the MySQL driver implementation. This allows it to reduce stack allocations.

type Message

type Message struct {
	Ctx context.Context

	Result *Result
	// contains filtered or unexported fields
}

I can't really have more than one message per connection "active" at a time because the reader, the result etc are all tied to the connection (and we should really not be advancing the reader in another thread) - so the idea here with the pool and stuff is pretty stupid. I should add the message to the subscription manager and listen for it on the channel at the top of every loop of Listen()

func (*Message) Init

func (m *Message) Init()

func (*Message) Pattern

func (m *Message) Pattern() string

func (*Message) Topic

func (m *Message) Topic() string

type MessageChan

type MessageChan <-chan *Message

type MessageChanMap

type MessageChanMap map[string]MessageChan

type PoolOptions

type PoolOptions struct {
	// MaxSize is the maximum size of the connection pool. If reached, it will
	// block the next client request until a connection is free
	MaxSize int
	// MaxIdle How many connections that can remain idle in the pool, will
	// otherwise be reaped by the trimming thread
	// Default: 5
	MaxIdle int
	// How long before reads time out
	// Default: 500 ms
	ReadTimeout time.Duration
	// URL is the protocol, address and database selection concatenated into
	// a URL format. Inspired by DSN.
	URL string
	// Dial is a function that returns an established net.Conn
	Dial dialFunc
	// InitialBufSize is the initial buffer size to associate with the
	// connection, this is also the minimum buffer size allowed when creating
	// new connections, but if the trimming thread is enabled and the
	// percentile target returns a higher value, this will be used for
	// any subsequent connections
	// Default: 4096 bytes
	InitialBufSize int
	// TrimOptions are fine-tuning options for the trimming go routine, this
	// should usually not be needed
	TrimOptions *TrimOptions
	// contains filtered or unexported fields
}

PoolOptions is specified to tune the connection pool for the client

type PubSubOpts

type PubSubOpts struct {
	PingInterval time.Duration
	ReadTimeout  time.Duration
	InitBufSize  int
}

type Reader

type Reader struct {
	// contains filtered or unexported fields
}

func NewReader

func NewReader(r *bufio.Reader) *Reader

func (*Reader) Bytes

func (r *Reader) Bytes() []byte

func (*Reader) Int

func (r *Reader) Int() int

func (*Reader) Len

func (r *Reader) Len() int

func (*Reader) Next

func (r *Reader) Next(scan ScanFunc) (err error)

func (*Reader) String

func (r *Reader) String() string

func (*Reader) WriteTo

func (r *Reader) WriteTo(w io.Writer) (int64, error)

type RedisPattern

type RedisPattern string

RedisPattern is a string that contains what is considered a pattern according to the spec here: https://redis.io/commands/KEYS

type Result

type Result struct {
	// contains filtered or unexported fields
}

Result is what is returned from the Redis client if a single response is expected

func NewResult

func NewResult(r *Reader) *Result

func (*Result) Scan

func (r *Result) Scan(dst interface{}) (err error)

Scan on result allows us to read with zero-copy into Scanner and io.Writer implementations as well as into string, int, other []byte slices (but with copy). However since Redis always returns a []byte slice, if you were to implement a scanner you would need to have a value switch that takes []byte slice and does something productive with it. This implementation can be simpler and forego the source switch since it is custom made for Redis.

type ScanFunc

type ScanFunc func(*bufio.Reader) (int, error)

type Scanner

type Scanner interface {
	Scan(dst interface{}) error
}

Scanner is an interface that allows you to implement custom logic with direct control over the byte buffer being used by the connection. Do not modify it and do not return it. As soon as your Scan function exits, the connection will start using the buffer again.

type StrInt

type StrInt int

type SubClient

type SubClient interface {
	Subscriber
	Client
}

func NewClient

func NewClient(ctx context.Context, opts *PoolOptions) (SubClient, error)

NewClient returns a client with the options specified

type Subscriber

type Subscriber interface {
	// Subscribe returns a map of channels corresponding to the string value of the topics being subscribed to
	Subscribe(ctx context.Context, topics ...interface{}) (msgChanMap MessageChanMap, err error)
	// Unsubscribe closes the subscriptions on the channels given
	Unsubscribe(ctx context.Context, topics ...interface{}) (err error)
}

type TrimOptions

type TrimOptions struct {
	Interval           time.Duration // default is 500 ms
	BufQuantileTargets []float64     // the targets to track in 0-1 percentiles, if none given, the default is []float{0.8, 1}
	BufQuantileTarget  float64       // The specific target for buf size. If invalid or omitted, it will pick the first (a[0]) percentile from BufQuantileTargets
	AllowedMargin      float64       // this is 0-1 representing a fraction of how far from the lowest percentile a higher one can be so that it shifts to using a higher percentile. To disable it, set it below 0. It can also go higher than 1, but we recommend targeting a higher percentile instead. The default is 0.1
}

TrimOptions represents advanced options to fine-tune the trimming background thread for connections

Directories

Path Synopsis
mocks
mock_driver
Package mock_driver is a generated GoMock package.
Package mock_driver is a generated GoMock package.
mock_greddis
Package mock_greddis is a generated GoMock package.
Package mock_greddis is a generated GoMock package.
mock_io
Package mock_io is a generated GoMock package.
Package mock_io is a generated GoMock package.
mock_net
Package mock_net is a generated GoMock package.
Package mock_net is a generated GoMock package.

Jump to

Keyboard shortcuts

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