Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DisableCAS support #113

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
_*
*.out
*~
.idea/
71 changes: 56 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -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`
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/bradfitz/gomemcache
module github.com/lovelock/gomemcache/v3

go 1.18
15 changes: 12 additions & 3 deletions memcache/memcache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
}
Expand Down
95 changes: 95 additions & 0 deletions memcache/roundrobin_selector.go
Original file line number Diff line number Diff line change
@@ -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
}
23 changes: 23 additions & 0 deletions memcache/roundrobin_selector_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}