diff --git a/.gitignore b/.gitignore index 02c604d7..4d0a2a9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ _* *.out *~ +.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 350c4d0a..6f3e205d 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,80 @@ ## About -This is a memcache client library for the Go programming language +This is a memcache client library derived from [gomemcache](https://github.com/bradfitz/gomemcache) for the Go programming language (http://golang.org/). -## Example +## Why this project? -Install with: +The version is bumped to v3 to indicate that it has something incompatible with the vanilla one. + +### `get` vs `gets` + +There are many derivate servers which implement **incomplete** memcache prototol, e.g. only `get` and `set` are implemented. + +The thing is, the original repository of [gomemcache](https://github.com/bradfitz/gomemcache) has something confusing when it comes to [`get` command](https://github.com/bradfitz/gomemcache/blob/fb4bf637b56d66a1925c1bb0780b27dd714ec380/memcache/memcache.go#L361). -```shell -$ go get github.com/bradfitz/gomemcache/memcache +```go +if _, err := fmt.Fprintf(rw, "gets %s\r\n", strings.Join(keys, " ")); err != nil { + return err +} ``` -Then use it like: +It means when you call `get`, the `gets` command is executed and [`casid` is always returned](https://github.com/bradfitz/gomemcache/blob/fb4bf637b56d66a1925c1bb0780b27dd714ec380/memcache/memcache.go#L523). +```go +dest := []interface{}{&it.Key, &it.Flags, &size, &it.casid} +``` + +I've talked to bradfitz and got a lot of important advises from him. Truly all the things I mentioned above are all because of the **incomplete implementation** and have nothing to do with the client. What I stand for is `get` means `get` and `gets` means `gets`. + +### `RoundRobinServerSelector` + +Vanilla memcache server use crc32 to pick which server to store a key, for servers those distribute keys equally to all nodes the crc32 is not what is wanted. Thanks for bradfitz's brilliant work I can implement a `RoundRobinServerSelector` painlessly. + +## Installing + +### Using _go get_ + +`$ go get -u github.com/lovelock/gomemcache/v3/memcache` + +After this command _gomemcache_ is ready to use. Its source will be in: + +`$GOPATH/src/github.com/lovelock/gomemcache/memcache` + +## Example + +Install with: ```go import ( - "github.com/bradfitz/gomemcache/memcache" + "github.com/lovelock/gomemcache/v3/memcache" ) - func main() { mc := memcache.New("10.0.0.1:11211", "10.0.0.2:11211", "10.0.0.3:11212") mc.Set(&memcache.Item{Key: "foo", Value: []byte("my value")}) +} +``` + +### For other derivatives - it, err := mc.Get("foo") - ... +```go +import ( + "github.com/lovelock/gomemcache/v3/memcache" +) + +func main() { + mc := memcache.NewRoundRobin("10.0.0.1:11211", "10.0.0.2:11211", "10.0.0.3:11212") + mc.DisableCAS = true // don't want get casid + mc.Set(&memcache.Item{Key: "foo", Value: []byte("my value")}) + + it, err := mc.Get("foo") + ... } ``` ## Full docs, see: -See https://pkg.go.dev/github.com/bradfitz/gomemcache/memcache +See https://godoc.org/github.com/lovelock/gomemcache/v3/memcache Or run: -```shell -$ godoc github.com/bradfitz/gomemcache/memcache -``` - +`$ godoc github.com/lovelock/gomemcache/v3/memcache` diff --git a/go.mod b/go.mod index 1bf1615a..0aac4539 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/bradfitz/gomemcache +module github.com/lovelock/gomemcache/v3 go 1.18 diff --git a/memcache/memcache.go b/memcache/memcache.go index b2ebacd2..ce677c10 100644 --- a/memcache/memcache.go +++ b/memcache/memcache.go @@ -156,6 +156,10 @@ type Client struct { lk sync.Mutex freeconn map[string][]*conn + + // When true, get is sent instead of gets when Client.Get() is called. + // Default false. + DisableCAS bool } // Item is an item to be got or stored in a memcached server. @@ -379,8 +383,13 @@ func (c *Client) withKeyRw(key string, fn func(*bufio.ReadWriter) error) error { } func (c *Client) getFromAddr(addr net.Addr, keys []string, cb func(*Item)) error { + cmd := "gets" + if c.DisableCAS { + cmd = "get" + } + return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error { - if _, err := fmt.Fprintf(rw, "gets %s\r\n", strings.Join(keys, " ")); err != nil { + if _, err := fmt.Fprintf(rw, "%s %s\r\n", cmd, strings.Join(keys, " ")); err != nil { return err } if err := rw.Flush(); err != nil { @@ -455,7 +464,7 @@ func (c *Client) touchFromAddr(addr net.Addr, keys []string, expiration int32) e } switch { case bytes.Equal(line, resultTouched): - break + continue case bytes.Equal(line, resultNotFound): return ErrCacheMiss default: @@ -499,7 +508,7 @@ func (c *Client) GetMulti(keys []string) (map[string]*Item, error) { } var err error - for _ = range keyMap { + for range keyMap { if ge := <-ch; ge != nil { err = ge } diff --git a/memcache/roundrobin_selector.go b/memcache/roundrobin_selector.go new file mode 100644 index 00000000..a32cc3d8 --- /dev/null +++ b/memcache/roundrobin_selector.go @@ -0,0 +1,95 @@ +package memcache + +import ( + "net" + "strings" + "sync" +) + +// NewRoundRobin returns a memcache client using the provided server(s) +// with equal weight. If a server is listed multiple times, +// it gets a proportional amount of weight. +func NewRoundRobin(server ...string) *Client { + ss := new(RoundRobinServerList) + err := ss.SetServers(server...) + if err != nil { + return nil + } + + return NewFromRoundRobinSelector(ss) +} + +// NewFromRoundRobinSelector returns a new Client using the provided RoundRobinServerSelector. +func NewFromRoundRobinSelector(ss *RoundRobinServerList) *Client { + return &Client{ + selector: ss, + DisableCAS: false, + } +} + +// RoundRobinServerList is a simple ServerSelector. Its zero value is usable. +type RoundRobinServerList struct { + mu sync.Mutex + addrs []net.Addr + next int +} + +// SetServers changes a RoundRobinServerList's set of servers at runtime and is +// safe for concurrent use by multiple goroutines. +// +// Each server is given equal weight. A server is given more weight +// if it's listed multiple times. +// +// SetServers returns an error if any of the server names fail to +// resolve. No attempt is made to connect to the server. If any error +// is returned, no changes are made to the RoundRobinServerList. +func (ss *RoundRobinServerList) SetServers(servers ...string) error { + naddr := make([]net.Addr, len(servers)) + for i, server := range servers { + if strings.Contains(server, "/") { + addr, err := net.ResolveUnixAddr("unix", server) + if err != nil { + return err + } + naddr[i] = newStaticAddr(addr) + } else { + tcpaddr, err := net.ResolveTCPAddr("tcp", server) + if err != nil { + return err + } + naddr[i] = newStaticAddr(tcpaddr) + } + } + + ss.mu.Lock() + defer ss.mu.Unlock() + ss.addrs = naddr + return nil +} + +// Each iterates over each server calling the given function +func (ss *RoundRobinServerList) Each(f func(net.Addr) error) error { + ss.mu.Lock() + defer ss.mu.Unlock() + for _, a := range ss.addrs { + if err := f(a); nil != err { + return err + } + } + return nil +} + +func (ss *RoundRobinServerList) PickServer(key string) (net.Addr, error) { + ss.mu.Lock() + defer ss.mu.Unlock() + if len(ss.addrs) == 0 { + return nil, ErrNoServers + } + if len(ss.addrs) == 1 { + return ss.addrs[0], nil + } + + ss.next = (ss.next + 1) % len(ss.addrs) + + return ss.addrs[ss.next], nil +} diff --git a/memcache/roundrobin_selector_test.go b/memcache/roundrobin_selector_test.go new file mode 100644 index 00000000..791b8b5e --- /dev/null +++ b/memcache/roundrobin_selector_test.go @@ -0,0 +1,23 @@ +package memcache + +import "testing" + +func BenchmarkPickRoundRobinServer(b *testing.B) { + // at least two to avoid 0 and 1 special cases: + benchPickRoundRobinServer(b, "127.0.0.1:1234", "127.0.0.1:1235") +} + +func BenchmarkPickRoundRobinServer_Single(b *testing.B) { + benchPickRoundRobinServer(b, "127.0.0.1:1234") +} + +func benchPickRoundRobinServer(b *testing.B, servers ...string) { + b.ReportAllocs() + var ss RoundRobinServerList + ss.SetServers(servers...) + for i := 0; i < b.N; i++ { + if _, err := ss.PickServer("some key"); err != nil { + b.Fatal(err) + } + } +}