From 157b276638d21ceed5a396a279ae3530f55ec7ef Mon Sep 17 00:00:00 2001 From: Paulo Zenida Date: Wed, 16 Mar 2022 10:43:47 +0000 Subject: [PATCH 1/8] add support for methods using contexts --- .gitignore | 1 + memcache/conn.go | 37 ++++ memcache/errors.go | 46 ++++ memcache/helpers.go | 79 +++++++ memcache/item.go | 22 ++ memcache/memcache.go | 516 +++++++++++++++++++------------------------ memcache/selector.go | 11 +- 7 files changed, 415 insertions(+), 297 deletions(-) create mode 100644 memcache/conn.go create mode 100644 memcache/errors.go create mode 100644 memcache/helpers.go create mode 100644 memcache/item.go diff --git a/.gitignore b/.gitignore index 02c604d7..b4a2d1c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ _* *.out *~ +.idea diff --git a/memcache/conn.go b/memcache/conn.go new file mode 100644 index 00000000..1c6eb273 --- /dev/null +++ b/memcache/conn.go @@ -0,0 +1,37 @@ +package memcache + +import ( + "bufio" + "net" + "time" +) + +// conn is a connection to a server. +type conn struct { + nc net.Conn + rw *bufio.ReadWriter + addr net.Addr + c *Client +} + +// release returns this connection back to the client's free pool +func (cn *conn) release() { + cn.c.putFreeConn(cn.addr, cn) +} + +func (cn *conn) extendDeadline() { + cn.nc.SetDeadline(time.Now().Add(cn.c.netTimeout())) +} + +// condRelease releases this connection if the error pointed to by err +// is nil (not an error) or is only a protocol level error (e.g. a +// cache miss). The purpose is to not recycle TCP connections that +// are bad. +func (cn *conn) condRelease(err *error) { + if *err == nil || resumableError(*err) { + cn.release() + } else { + cn.nc.Close() + } +} + diff --git a/memcache/errors.go b/memcache/errors.go new file mode 100644 index 00000000..eb8e4932 --- /dev/null +++ b/memcache/errors.go @@ -0,0 +1,46 @@ +package memcache + +import ( + "errors" + "net" +) + +var ( + // ErrCacheMiss means that a Get failed because the item wasn't present. + ErrCacheMiss = errors.New("memcache: cache miss") + + // ErrCASConflict means that a CompareAndSwap call failed due to the + // cached value being modified between the Get and the CompareAndSwap. + // If the cached value was simply evicted rather than replaced, + // ErrNotStored will be returned instead. + ErrCASConflict = errors.New("memcache: compare-and-swap conflict") + + // ErrNotStored means that a conditional write operation (i.e. Add or + // CompareAndSwap) failed because the condition was not satisfied. + ErrNotStored = errors.New("memcache: item not stored") + + // ErrServerError means that a server error occurred. + ErrServerError = errors.New("memcache: server error") + + // ErrNoStats means that no statistics were available. + ErrNoStats = errors.New("memcache: no statistics available") + + // ErrMalformedKey is returned when an invalid key is used. + // Keys must be at maximum 250 bytes long and not + // contain whitespace or control characters. + ErrMalformedKey = errors.New("malformed: key is too long or contains invalid characters") + + // ErrNoServers is returned when no servers are configured or available. + ErrNoServers = errors.New("memcache: no servers configured or available") +) + +// ConnectTimeoutError is the error type used when it takes +// too long to connect to the desired host. This level of +// detail can generally be ignored. +type ConnectTimeoutError struct { + Addr net.Addr +} + +func (cte *ConnectTimeoutError) Error() string { + return "memcache: connect timeout to " + cte.Addr.String() +} \ No newline at end of file diff --git a/memcache/helpers.go b/memcache/helpers.go new file mode 100644 index 00000000..46402cde --- /dev/null +++ b/memcache/helpers.go @@ -0,0 +1,79 @@ +package memcache + +import ( + "bufio" + "bytes" + "fmt" +) + +// resumableError returns true if err is only a protocol-level cache error. +// This is used to determine whether or not a server connection should +// be re-used or not. If an error occurs, by default we don't reuse the +// connection, unless it was just a cache error. +func resumableError(err error) bool { + switch err { + case ErrCacheMiss, ErrCASConflict, ErrNotStored, ErrMalformedKey: + return true + } + return false +} + +func legalKey(key string) bool { + if len(key) > 250 { + return false + } + for i := 0; i < len(key); i++ { + if key[i] <= ' ' || key[i] == 0x7f { + return false + } + } + return true +} + +// scanGetResponseLine populates it and returns the declared size of the item. +// It does not read the bytes of the item. +func scanGetResponseLine(line []byte, it *Item) (size int, err error) { + pattern := "VALUE %s %d %d %d\r\n" + dest := []interface{}{&it.Key, &it.Flags, &size, &it.casid} + if bytes.Count(line, space) == 3 { + pattern = "VALUE %s %d %d\r\n" + dest = dest[:3] + } + n, err := fmt.Sscanf(string(line), pattern, dest...) + if err != nil || n != len(dest) { + return -1, fmt.Errorf("memcache: unexpected line in get response: %q", line) + } + return size, nil +} + +func writeReadLine(rw *bufio.ReadWriter, format string, args ...interface{}) ([]byte, error) { + _, err := fmt.Fprintf(rw, format, args...) + if err != nil { + return nil, err + } + if err := rw.Flush(); err != nil { + return nil, err + } + line, err := rw.ReadSlice('\n') + return line, err +} + +func writeExpectf(rw *bufio.ReadWriter, expect []byte, format string, args ...interface{}) error { + line, err := writeReadLine(rw, format, args...) + if err != nil { + return err + } + switch { + case bytes.Equal(line, resultOK): + return nil + case bytes.Equal(line, expect): + return nil + case bytes.Equal(line, resultNotStored): + return ErrNotStored + case bytes.Equal(line, resultExists): + return ErrCASConflict + case bytes.Equal(line, resultNotFound): + return ErrCacheMiss + } + return fmt.Errorf("memcache: unexpected response line: %q", string(line)) +} diff --git a/memcache/item.go b/memcache/item.go new file mode 100644 index 00000000..69ec3cae --- /dev/null +++ b/memcache/item.go @@ -0,0 +1,22 @@ +package memcache + +// Item is an item to be got or stored in a memcached server. +type Item struct { + // Key is the Item's key (250 bytes maximum). + Key string + + // Value is the Item's value. + Value []byte + + // Flags are server-opaque flags whose semantics are entirely + // up to the app. + Flags uint32 + + // Expiration is the cache expiration time, in seconds: either a relative + // time from now (up to 1 month), or an absolute Unix epoch time. + // Zero means the Item has no expiration time. + Expiration int32 + + // Compare and swap ID. + casid uint64 +} diff --git a/memcache/memcache.go b/memcache/memcache.go index 28eccf03..4f94420a 100644 --- a/memcache/memcache.go +++ b/memcache/memcache.go @@ -20,6 +20,7 @@ package memcache import ( "bufio" "bytes" + "context" "errors" "fmt" "io" @@ -34,35 +35,6 @@ import ( // Similar to: // https://godoc.org/google.golang.org/appengine/memcache -var ( - // ErrCacheMiss means that a Get failed because the item wasn't present. - ErrCacheMiss = errors.New("memcache: cache miss") - - // ErrCASConflict means that a CompareAndSwap call failed due to the - // cached value being modified between the Get and the CompareAndSwap. - // If the cached value was simply evicted rather than replaced, - // ErrNotStored will be returned instead. - ErrCASConflict = errors.New("memcache: compare-and-swap conflict") - - // ErrNotStored means that a conditional write operation (i.e. Add or - // CompareAndSwap) failed because the condition was not satisfied. - ErrNotStored = errors.New("memcache: item not stored") - - // ErrServer means that a server error occurred. - ErrServerError = errors.New("memcache: server error") - - // ErrNoStats means that no statistics were available. - ErrNoStats = errors.New("memcache: no statistics available") - - // ErrMalformedKey is returned when an invalid key is used. - // Keys must be at maximum 250 bytes long and not - // contain whitespace or control characters. - ErrMalformedKey = errors.New("malformed: key is too long or contains invalid characters") - - // ErrNoServers is returned when no servers are configured or available. - ErrNoServers = errors.New("memcache: no servers configured or available") -) - const ( // DefaultTimeout is the default socket read/write timeout. DefaultTimeout = 100 * time.Millisecond @@ -74,30 +46,6 @@ const ( const buffered = 8 // arbitrary buffered channel size, for readability -// resumableError returns true if err is only a protocol-level cache error. -// This is used to determine whether or not a server connection should -// be re-used or not. If an error occurs, by default we don't reuse the -// connection, unless it was just a cache error. -func resumableError(err error) bool { - switch err { - case ErrCacheMiss, ErrCASConflict, ErrNotStored, ErrMalformedKey: - return true - } - return false -} - -func legalKey(key string) bool { - if len(key) > 250 { - return false - } - for i := 0; i < len(key); i++ { - if key[i] <= ' ' || key[i] == 0x7f { - return false - } - } - return true -} - var ( crlf = []byte("\r\n") space = []byte(" ") @@ -150,54 +98,233 @@ type Client struct { freeconn map[string][]*conn } -// Item is an item to be got or stored in a memcached server. -type Item struct { - // Key is the Item's key (250 bytes maximum). - Key string - - // Value is the Item's value. - Value []byte +// FlushAll flushes all from all servers +func (c *Client) FlushAll() error { + return c.FlushAllWithContext(context.Background()) +} - // Flags are server-opaque flags whose semantics are entirely - // up to the app. - Flags uint32 +// FlushAllWithContext flushes all from all servers +func (c *Client) FlushAllWithContext(ctx context.Context) error { + return c.selector.Each(ctx, c.flushAllFromAddr) +} - // Expiration is the cache expiration time, in seconds: either a relative - // time from now (up to 1 month), or an absolute Unix epoch time. - // Zero means the Item has no expiration time. - Expiration int32 +// Get gets the item for the given key. ErrCacheMiss is returned for a +// memcache cache miss. The key must be at most 250 bytes in length. +func (c *Client) Get(key string) (item *Item, err error) { + return c.GetWithContext(context.Background(), key) +} - // Compare and swap ID. - casid uint64 +// GetWithContext gets the item for the given key. ErrCacheMiss is returned for a +// memcache cache miss. The key must be at most 250 bytes in length. +func (c *Client) GetWithContext(ctx context.Context, key string) (item *Item, err error) { + err = c.withKeyAddr(ctx, key, func(addr net.Addr) error { + return c.getFromAddr(addr, []string{key}, func(it *Item) { item = it }) + }) + if err == nil && item == nil { + err = ErrCacheMiss + } + return } -// conn is a connection to a server. -type conn struct { - nc net.Conn - rw *bufio.ReadWriter - addr net.Addr - c *Client +// Touch updates the expiry for the given key. The seconds parameter is either +// a Unix timestamp or, if seconds is less than 1 month, the number of seconds +// into the future at which time the item will expire. Zero means the item has +// no expiration time. ErrCacheMiss is returned if the key is not in the cache. +// The key must be at most 250 bytes in length. +func (c *Client) Touch(key string, seconds int32) (err error) { + return c.TouchWithContext(context.Background(), key, seconds) } -// release returns this connection back to the client's free pool -func (cn *conn) release() { - cn.c.putFreeConn(cn.addr, cn) +// TouchWithContext updates the expiry for the given key. The seconds parameter +// is either a Unix timestamp or, if seconds is less than 1 month, the number of +// seconds into the future at which time the item will expire. Zero means the item +// has no expiration time. ErrCacheMiss is returned if the key is not in the cache. +// The key must be at most 250 bytes in length. +func (c *Client) TouchWithContext(ctx context.Context, key string, seconds int32) (err error) { + return c.withKeyAddr(ctx, key, func(addr net.Addr) error { + return c.touchFromAddr(addr, []string{key}, seconds) + }) } -func (cn *conn) extendDeadline() { - cn.nc.SetDeadline(time.Now().Add(cn.c.netTimeout())) +// GetMulti is a batch version of Get. The returned map from keys to +// items may have fewer elements than the input slice, due to memcache +// cache misses. Each key must be at most 250 bytes in length. +// If no error is returned, the returned map will also be non-nil. +func (c *Client) GetMulti(keys []string) (map[string]*Item, error) { + return c.GetMultiWithContext(context.Background(), keys) } -// condRelease releases this connection if the error pointed to by err -// is nil (not an error) or is only a protocol level error (e.g. a -// cache miss). The purpose is to not recycle TCP connections that -// are bad. -func (cn *conn) condRelease(err *error) { - if *err == nil || resumableError(*err) { - cn.release() - } else { - cn.nc.Close() +// GetMultiWithContext is a batch version of Get. The returned map from keys to +// items may have fewer elements than the input slice, due to memcache +// cache misses. Each key must be at most 250 bytes in length. +// If no error is returned, the returned map will also be non-nil. +func (c *Client) GetMultiWithContext(ctx context.Context, keys []string) (map[string]*Item, error) { + var lk sync.Mutex + m := make(map[string]*Item) + addItemToMap := func(it *Item) { + lk.Lock() + defer lk.Unlock() + m[it.Key] = it + } + + keyMap := make(map[net.Addr][]string) + for _, key := range keys { + if !legalKey(key) { + return nil, ErrMalformedKey + } + addr, err := c.selector.PickServer(ctx, key) + if err != nil { + return nil, err + } + keyMap[addr] = append(keyMap[addr], key) } + + ch := make(chan error, buffered) + for addr, keys := range keyMap { + go func(addr net.Addr, keys []string) { + ch <- c.getFromAddr(addr, keys, addItemToMap) + }(addr, keys) + } + + var err error + for _ = range keyMap { + if ge := <-ch; ge != nil { + err = ge + } + } + return m, err +} + +// Set writes the given item, unconditionally. +func (c *Client) Set(item *Item) error { + return c.SetWithContext(context.Background(), item) +} + +// SetWithContext writes the given item, unconditionally. +func (c *Client) SetWithContext(ctx context.Context, item *Item) error { + return c.onItem(ctx, item, (*Client).set) +} + +// Add writes the given item, if no value already exists for its +// key. ErrNotStored is returned if that condition is not met. +func (c *Client) Add(item *Item) error { + return c.AddWithContext(context.Background(), item) +} + +// AddWithContext writes the given item, if no value already exists for its +// key. ErrNotStored is returned if that condition is not met. +func (c *Client) AddWithContext(ctx context.Context, item *Item) error { + return c.onItem(ctx, item, (*Client).add) +} + +// Replace writes the given item, but only if the server *does* +// already hold data for this key +func (c *Client) Replace(item *Item) error { + return c.ReplaceWithContext(context.Background(), item) +} + +// ReplaceWithContext writes the given item, but only if the server *does* +// already hold data for this key +func (c *Client) ReplaceWithContext(ctx context.Context, item *Item) error { + return c.onItem(ctx, item, (*Client).replace) +} + +// CompareAndSwap writes the given item that was previously returned +// by Get, if the value was neither modified or evicted between the +// Get and the CompareAndSwap calls. The item's Key should not change +// between calls but all other item fields may differ. ErrCASConflict +// is returned if the value was modified in between the +// calls. ErrNotStored is returned if the value was evicted in between +// the calls. +func (c *Client) CompareAndSwap(item *Item) error { + return c.CompareAndSwapWithContext(context.Background(), item) +} + +// CompareAndSwapWithContext writes the given item that was previously returned +// by Get, if the value was neither modified or evicted between the +// Get and the CompareAndSwap calls. The item's Key should not change +// between calls but all other item fields may differ. ErrCASConflict +// is returned if the value was modified in between the +// calls. ErrNotStored is returned if the value was evicted in between +// the calls. +func (c *Client) CompareAndSwapWithContext(ctx context.Context, item *Item) error { + return c.onItem(ctx, item, (*Client).cas) +} + +// Delete deletes the item with the provided key. The error ErrCacheMiss is +// returned if the item didn't already exist in the cache. +func (c *Client) Delete(key string) error { + return c.DeleteWithContext(context.Background(), key) +} + +// DeleteWithContext deletes the item with the provided key. The error ErrCacheMiss is +// returned if the item didn't already exist in the cache. +func (c *Client) DeleteWithContext(ctx context.Context, key string) error { + return c.withKeyRw(ctx, key, func(rw *bufio.ReadWriter) error { + return writeExpectf(rw, resultDeleted, "delete %s\r\n", key) + }) +} + +// DeleteAll deletes all items in the cache. +func (c *Client) DeleteAll() error { + return c.DeleteAllWithContext(context.Background()) +} + +// DeleteAllWithContext deletes all items in the cache. +func (c *Client) DeleteAllWithContext(ctx context.Context) error { + return c.withKeyRw(ctx, "", func(rw *bufio.ReadWriter) error { + return writeExpectf(rw, resultDeleted, "flush_all\r\n") + }) +} + +// Ping checks all instances if they are alive. Returns error if any +// of them is down. +func (c *Client) Ping() error { + return c.PingWithContext(context.Background()) +} + +// PingWithContext checks all instances if they are alive. Returns error if any +// of them is down. +func (c *Client) PingWithContext(ctx context.Context) error { + return c.selector.Each(ctx, c.ping) +} + +// Increment atomically increments key by delta. The return value is +// the new value after being incremented or an error. If the value +// didn't exist in memcached the error is ErrCacheMiss. The value in +// memcached must be an decimal number, or an error will be returned. +// On 64-bit overflow, the new value wraps around. +func (c *Client) Increment(key string, delta uint64) (newValue uint64, err error) { + return c.IncrementWithContext(context.Background(), key, delta) +} + +// IncrementWithContext atomically increments key by delta. The return value is +// the new value after being incremented or an error. If the value +// didn't exist in memcached the error is ErrCacheMiss. The value in +// memcached must be an decimal number, or an error will be returned. +// On 64-bit overflow, the new value wraps around. +func (c *Client) IncrementWithContext(ctx context.Context, key string, delta uint64) (newValue uint64, err error) { + return c.incrDecr(ctx, "incr", key, delta) +} + +// Decrement atomically decrements key by delta. The return value is +// the new value after being decremented or an error. If the value +// didn't exist in memcached the error is ErrCacheMiss. The value in +// memcached must be an decimal number, or an error will be returned. +// On underflow, the new value is capped at zero and does not wrap +// around. +func (c *Client) Decrement(key string, delta uint64) (newValue uint64, err error) { + return c.DecrementWithContext(context.Background(), key, delta) +} + +// DecrementWithContext atomically decrements key by delta. The return value is +// the new value after being decremented or an error. If the value +// didn't exist in memcached the error is ErrCacheMiss. The value in +// memcached must be an decimal number, or an error will be returned. +// On underflow, the new value is capped at zero and does not wrap +// around. +func (c *Client) DecrementWithContext(ctx context.Context, key string, delta uint64) (newValue uint64, err error) { + return c.incrDecr(ctx, "decr", key, delta) } func (c *Client) putFreeConn(addr net.Addr, cn *conn) { @@ -243,17 +370,6 @@ func (c *Client) maxIdleConns() int { return DefaultMaxIdleConns } -// ConnectTimeoutError is the error type used when it takes -// too long to connect to the desired host. This level of -// detail can generally be ignored. -type ConnectTimeoutError struct { - Addr net.Addr -} - -func (cte *ConnectTimeoutError) Error() string { - return "memcache: connect timeout to " + cte.Addr.String() -} - func (c *Client) dial(addr net.Addr) (net.Conn, error) { nc, err := net.DialTimeout(addr.Network(), addr.String(), c.netTimeout()) if err == nil { @@ -287,8 +403,8 @@ func (c *Client) getConn(addr net.Addr) (*conn, error) { return cn, nil } -func (c *Client) onItem(item *Item, fn func(*Client, *bufio.ReadWriter, *Item) error) error { - addr, err := c.selector.PickServer(item.Key) +func (c *Client) onItem(ctx context.Context, item *Item, fn func(*Client, *bufio.ReadWriter, *Item) error) error { + addr, err := c.selector.PickServer(ctx, item.Key) if err != nil { return err } @@ -303,38 +419,11 @@ func (c *Client) onItem(item *Item, fn func(*Client, *bufio.ReadWriter, *Item) e return nil } -func (c *Client) FlushAll() error { - return c.selector.Each(c.flushAllFromAddr) -} - -// Get gets the item for the given key. ErrCacheMiss is returned for a -// memcache cache miss. The key must be at most 250 bytes in length. -func (c *Client) Get(key string) (item *Item, err error) { - err = c.withKeyAddr(key, func(addr net.Addr) error { - return c.getFromAddr(addr, []string{key}, func(it *Item) { item = it }) - }) - if err == nil && item == nil { - err = ErrCacheMiss - } - return -} - -// Touch updates the expiry for the given key. The seconds parameter is either -// a Unix timestamp or, if seconds is less than 1 month, the number of seconds -// into the future at which time the item will expire. Zero means the item has -// no expiration time. ErrCacheMiss is returned if the key is not in the cache. -// The key must be at most 250 bytes in length. -func (c *Client) Touch(key string, seconds int32) (err error) { - return c.withKeyAddr(key, func(addr net.Addr) error { - return c.touchFromAddr(addr, []string{key}, seconds) - }) -} - -func (c *Client) withKeyAddr(key string, fn func(net.Addr) error) (err error) { +func (c *Client) withKeyAddr(ctx context.Context, key string, fn func(net.Addr) error) (err error) { if !legalKey(key) { return ErrMalformedKey } - addr, err := c.selector.PickServer(key) + addr, err := c.selector.PickServer(ctx, key) if err != nil { return err } @@ -350,8 +439,8 @@ func (c *Client) withAddrRw(addr net.Addr, fn func(*bufio.ReadWriter) error) (er return fn(cn.rw) } -func (c *Client) withKeyRw(key string, fn func(*bufio.ReadWriter) error) error { - return c.withKeyAddr(key, func(addr net.Addr) error { +func (c *Client) withKeyRw(ctx context.Context, key string, fn func(*bufio.ReadWriter) error) error { + return c.withKeyAddr(ctx, key, func(addr net.Addr) error { return c.withAddrRw(addr, fn) }) } @@ -444,47 +533,6 @@ func (c *Client) touchFromAddr(addr net.Addr, keys []string, expiration int32) e }) } -// GetMulti is a batch version of Get. The returned map from keys to -// items may have fewer elements than the input slice, due to memcache -// cache misses. Each key must be at most 250 bytes in length. -// If no error is returned, the returned map will also be non-nil. -func (c *Client) GetMulti(keys []string) (map[string]*Item, error) { - var lk sync.Mutex - m := make(map[string]*Item) - addItemToMap := func(it *Item) { - lk.Lock() - defer lk.Unlock() - m[it.Key] = it - } - - keyMap := make(map[net.Addr][]string) - for _, key := range keys { - if !legalKey(key) { - return nil, ErrMalformedKey - } - addr, err := c.selector.PickServer(key) - if err != nil { - return nil, err - } - keyMap[addr] = append(keyMap[addr], key) - } - - ch := make(chan error, buffered) - for addr, keys := range keyMap { - go func(addr net.Addr, keys []string) { - ch <- c.getFromAddr(addr, keys, addItemToMap) - }(addr, keys) - } - - var err error - for _ = range keyMap { - if ge := <-ch; ge != nil { - err = ge - } - } - return m, err -} - // parseGetResponse reads a GET response from r and calls cb for each // read and allocated Item func parseGetResponse(r *bufio.Reader, cb func(*Item)) error { @@ -516,62 +564,18 @@ func parseGetResponse(r *bufio.Reader, cb func(*Item)) error { } } -// scanGetResponseLine populates it and returns the declared size of the item. -// It does not read the bytes of the item. -func scanGetResponseLine(line []byte, it *Item) (size int, err error) { - pattern := "VALUE %s %d %d %d\r\n" - dest := []interface{}{&it.Key, &it.Flags, &size, &it.casid} - if bytes.Count(line, space) == 3 { - pattern = "VALUE %s %d %d\r\n" - dest = dest[:3] - } - n, err := fmt.Sscanf(string(line), pattern, dest...) - if err != nil || n != len(dest) { - return -1, fmt.Errorf("memcache: unexpected line in get response: %q", line) - } - return size, nil -} - -// Set writes the given item, unconditionally. -func (c *Client) Set(item *Item) error { - return c.onItem(item, (*Client).set) -} - func (c *Client) set(rw *bufio.ReadWriter, item *Item) error { return c.populateOne(rw, "set", item) } -// Add writes the given item, if no value already exists for its -// key. ErrNotStored is returned if that condition is not met. -func (c *Client) Add(item *Item) error { - return c.onItem(item, (*Client).add) -} - func (c *Client) add(rw *bufio.ReadWriter, item *Item) error { return c.populateOne(rw, "add", item) } -// Replace writes the given item, but only if the server *does* -// already hold data for this key -func (c *Client) Replace(item *Item) error { - return c.onItem(item, (*Client).replace) -} - func (c *Client) replace(rw *bufio.ReadWriter, item *Item) error { return c.populateOne(rw, "replace", item) } -// CompareAndSwap writes the given item that was previously returned -// by Get, if the value was neither modified or evicted between the -// Get and the CompareAndSwap calls. The item's Key should not change -// between calls but all other item fields may differ. ErrCASConflict -// is returned if the value was modified in between the -// calls. ErrNotStored is returned if the value was evicted in between -// the calls. -func (c *Client) CompareAndSwap(item *Item) error { - return c.onItem(item, (*Client).cas) -} - func (c *Client) cas(rw *bufio.ReadWriter, item *Item) error { return c.populateOne(rw, "cas", item) } @@ -617,81 +621,9 @@ func (c *Client) populateOne(rw *bufio.ReadWriter, verb string, item *Item) erro return fmt.Errorf("memcache: unexpected response line from %q: %q", verb, string(line)) } -func writeReadLine(rw *bufio.ReadWriter, format string, args ...interface{}) ([]byte, error) { - _, err := fmt.Fprintf(rw, format, args...) - if err != nil { - return nil, err - } - if err := rw.Flush(); err != nil { - return nil, err - } - line, err := rw.ReadSlice('\n') - return line, err -} - -func writeExpectf(rw *bufio.ReadWriter, expect []byte, format string, args ...interface{}) error { - line, err := writeReadLine(rw, format, args...) - if err != nil { - return err - } - switch { - case bytes.Equal(line, resultOK): - return nil - case bytes.Equal(line, expect): - return nil - case bytes.Equal(line, resultNotStored): - return ErrNotStored - case bytes.Equal(line, resultExists): - return ErrCASConflict - case bytes.Equal(line, resultNotFound): - return ErrCacheMiss - } - return fmt.Errorf("memcache: unexpected response line: %q", string(line)) -} - -// Delete deletes the item with the provided key. The error ErrCacheMiss is -// returned if the item didn't already exist in the cache. -func (c *Client) Delete(key string) error { - return c.withKeyRw(key, func(rw *bufio.ReadWriter) error { - return writeExpectf(rw, resultDeleted, "delete %s\r\n", key) - }) -} - -// DeleteAll deletes all items in the cache. -func (c *Client) DeleteAll() error { - return c.withKeyRw("", func(rw *bufio.ReadWriter) error { - return writeExpectf(rw, resultDeleted, "flush_all\r\n") - }) -} - -// Ping checks all instances if they are alive. Returns error if any -// of them is down. -func (c *Client) Ping() error { - return c.selector.Each(c.ping) -} - -// Increment atomically increments key by delta. The return value is -// the new value after being incremented or an error. If the value -// didn't exist in memcached the error is ErrCacheMiss. The value in -// memcached must be an decimal number, or an error will be returned. -// On 64-bit overflow, the new value wraps around. -func (c *Client) Increment(key string, delta uint64) (newValue uint64, err error) { - return c.incrDecr("incr", key, delta) -} - -// Decrement atomically decrements key by delta. The return value is -// the new value after being decremented or an error. If the value -// didn't exist in memcached the error is ErrCacheMiss. The value in -// memcached must be an decimal number, or an error will be returned. -// On underflow, the new value is capped at zero and does not wrap -// around. -func (c *Client) Decrement(key string, delta uint64) (newValue uint64, err error) { - return c.incrDecr("decr", key, delta) -} - -func (c *Client) incrDecr(verb, key string, delta uint64) (uint64, error) { +func (c *Client) incrDecr(ctx context.Context, verb, key string, delta uint64) (uint64, error) { var val uint64 - err := c.withKeyRw(key, func(rw *bufio.ReadWriter) error { + err := c.withKeyRw(ctx, key, func(rw *bufio.ReadWriter) error { line, err := writeReadLine(rw, "%s %s %d\r\n", verb, key, delta) if err != nil { return err diff --git a/memcache/selector.go b/memcache/selector.go index 89ad81e0..e1337268 100644 --- a/memcache/selector.go +++ b/memcache/selector.go @@ -17,6 +17,7 @@ limitations under the License. package memcache import ( + "context" "hash/crc32" "net" "strings" @@ -31,8 +32,8 @@ import ( type ServerSelector interface { // PickServer returns the server address that a given item // should be shared onto. - PickServer(key string) (net.Addr, error) - Each(func(net.Addr) error) error + PickServer(ctx context.Context, key string) (net.Addr, error) + Each(ctx context.Context, fn func(net.Addr) error) error } // ServerList is a simple ServerSelector. Its zero value is usable. @@ -90,11 +91,11 @@ func (ss *ServerList) SetServers(servers ...string) error { } // Each iterates over each server calling the given function -func (ss *ServerList) Each(f func(net.Addr) error) error { +func (ss *ServerList) Each(_ context.Context, fn func(net.Addr) error) error { ss.mu.RLock() defer ss.mu.RUnlock() for _, a := range ss.addrs { - if err := f(a); nil != err { + if err := fn(a); nil != err { return err } } @@ -111,7 +112,7 @@ var keyBufPool = sync.Pool{ }, } -func (ss *ServerList) PickServer(key string) (net.Addr, error) { +func (ss *ServerList) PickServer(_ context.Context, key string) (net.Addr, error) { ss.mu.RLock() defer ss.mu.RUnlock() if len(ss.addrs) == 0 { From 2a9ae78f6674b40b20f4c4057620ac6ae5825744 Mon Sep 17 00:00:00 2001 From: Paulo Zenida Date: Wed, 16 Mar 2022 10:50:24 +0000 Subject: [PATCH 2/8] update go.mod --- go.mod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 0d0eed2c..f7f4a483 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/bradfitz/gomemcache +module github.com/paulo.zenida/gomemcache -go 1.12 +go 1.17 From bef8c2afe5041c91326697d6f50e4a63e45d5a8f Mon Sep 17 00:00:00 2001 From: Paulo Zenida Date: Wed, 16 Mar 2022 10:53:58 +0000 Subject: [PATCH 3/8] update go.mod --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index f7f4a483..c74c12b6 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/paulo.zenida/gomemcache +module github.com/paulozenida/gomemcache go 1.17 From 88ab6197902148dcf3c2cebd645b502c2fce2f99 Mon Sep 17 00:00:00 2001 From: Paulo Zenida Date: Wed, 16 Mar 2022 17:35:39 +0000 Subject: [PATCH 4/8] update readme --- README.md | 36 +----------------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/README.md b/README.md index f987363c..01020dae 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,3 @@ ## About -This is a memcache client library for the Go programming language -(http://golang.org/). - -## Installing - -### Using *go get* - - $ go get github.com/bradfitz/gomemcache/memcache - -After this command *gomemcache* is ready to use. Its source will be in: - - $GOPATH/src/github.com/bradfitz/gomemcache/memcache - -## Example - - import ( - "github.com/bradfitz/gomemcache/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")}) - - it, err := mc.Get("foo") - ... - } - -## Full docs, see: - -See https://godoc.org/github.com/bradfitz/gomemcache/memcache - -Or run: - - $ godoc github.com/bradfitz/gomemcache/memcache - +This is a fork from github.com/bradfitz/gomemcache. From 761568a9e16a14df0a625095c714cc4abf9d967f Mon Sep 17 00:00:00 2001 From: Paulo Zenida Date: Thu, 17 Mar 2022 12:00:39 +0000 Subject: [PATCH 5/8] update readme and go.mod for upstream fork --- README.md | 36 +++++++++++++++++++++++++++++++++++- go.mod | 4 ++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 01020dae..f987363c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,37 @@ ## About -This is a fork from github.com/bradfitz/gomemcache. +This is a memcache client library for the Go programming language +(http://golang.org/). + +## Installing + +### Using *go get* + + $ go get github.com/bradfitz/gomemcache/memcache + +After this command *gomemcache* is ready to use. Its source will be in: + + $GOPATH/src/github.com/bradfitz/gomemcache/memcache + +## Example + + import ( + "github.com/bradfitz/gomemcache/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")}) + + it, err := mc.Get("foo") + ... + } + +## Full docs, see: + +See https://godoc.org/github.com/bradfitz/gomemcache/memcache + +Or run: + + $ godoc github.com/bradfitz/gomemcache/memcache + diff --git a/go.mod b/go.mod index c74c12b6..0d0eed2c 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/paulozenida/gomemcache +module github.com/bradfitz/gomemcache -go 1.17 +go 1.12 From c35fade5485eb7680a42f65d685cfd5303b7cc8e Mon Sep 17 00:00:00 2001 From: Paulo Zenida Date: Thu, 17 Mar 2022 12:05:39 +0000 Subject: [PATCH 6/8] fix tests --- memcache/memcache_test.go | 3 ++- memcache/selector_test.go | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/memcache/memcache_test.go b/memcache/memcache_test.go index b94a30ca..f0077f90 100644 --- a/memcache/memcache_test.go +++ b/memcache/memcache_test.go @@ -19,6 +19,7 @@ package memcache import ( "bufio" + "context" "fmt" "io" "io/ioutil" @@ -287,6 +288,6 @@ func BenchmarkOnItem(b *testing.B) { dummyFn := func(_ *Client, _ *bufio.ReadWriter, _ *Item) error { return nil } b.ResetTimer() for i := 0; i < b.N; i++ { - c.onItem(&item, dummyFn) + c.onItem(context.Background(), &item, dummyFn) } } diff --git a/memcache/selector_test.go b/memcache/selector_test.go index 65a2c4dd..4241d560 100644 --- a/memcache/selector_test.go +++ b/memcache/selector_test.go @@ -16,7 +16,10 @@ limitations under the License. package memcache -import "testing" +import ( + "context" + "testing" +) func BenchmarkPickServer(b *testing.B) { // at least two to avoid 0 and 1 special cases: @@ -32,7 +35,7 @@ func benchPickServer(b *testing.B, servers ...string) { var ss ServerList ss.SetServers(servers...) for i := 0; i < b.N; i++ { - if _, err := ss.PickServer("some key"); err != nil { + if _, err := ss.PickServer(context.Background(), "some key"); err != nil { b.Fatal(err) } } From 4269599f33e989e3c151debfda177c3aa6a7f4f5 Mon Sep 17 00:00:00 2001 From: Paulo Zenida Date: Thu, 17 Mar 2022 12:26:48 +0000 Subject: [PATCH 7/8] passing context to the connection method as well --- memcache/memcache.go | 39 +++++++++++++++++++++++---------------- memcache/selector.go | 6 +++--- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/memcache/memcache.go b/memcache/memcache.go index 4f94420a..8b6f8e3b 100644 --- a/memcache/memcache.go +++ b/memcache/memcache.go @@ -118,7 +118,7 @@ func (c *Client) Get(key string) (item *Item, err error) { // memcache cache miss. The key must be at most 250 bytes in length. func (c *Client) GetWithContext(ctx context.Context, key string) (item *Item, err error) { err = c.withKeyAddr(ctx, key, func(addr net.Addr) error { - return c.getFromAddr(addr, []string{key}, func(it *Item) { item = it }) + return c.getFromAddr(ctx, addr, []string{key}, func(it *Item) { item = it }) }) if err == nil && item == nil { err = ErrCacheMiss @@ -142,7 +142,7 @@ func (c *Client) Touch(key string, seconds int32) (err error) { // The key must be at most 250 bytes in length. func (c *Client) TouchWithContext(ctx context.Context, key string, seconds int32) (err error) { return c.withKeyAddr(ctx, key, func(addr net.Addr) error { - return c.touchFromAddr(addr, []string{key}, seconds) + return c.touchFromAddr(ctx, addr, []string{key}, seconds) }) } @@ -182,7 +182,7 @@ func (c *Client) GetMultiWithContext(ctx context.Context, keys []string) (map[st ch := make(chan error, buffered) for addr, keys := range keyMap { go func(addr net.Addr, keys []string) { - ch <- c.getFromAddr(addr, keys, addItemToMap) + ch <- c.getFromAddr(ctx, addr, keys, addItemToMap) }(addr, keys) } @@ -383,7 +383,14 @@ func (c *Client) dial(addr net.Addr) (net.Conn, error) { return nil, err } -func (c *Client) getConn(addr net.Addr) (*conn, error) { +func (c *Client) getConn(ctx context.Context, addr net.Addr) (*conn, error) { + // Check if the context is expired. + select { + default: + case <-ctx.Done(): + return nil, ctx.Err() + } + cn, ok := c.getFreeConn(addr) if ok { cn.extendDeadline() @@ -408,7 +415,7 @@ func (c *Client) onItem(ctx context.Context, item *Item, fn func(*Client, *bufio if err != nil { return err } - cn, err := c.getConn(addr) + cn, err := c.getConn(ctx, addr) if err != nil { return err } @@ -430,8 +437,8 @@ func (c *Client) withKeyAddr(ctx context.Context, key string, fn func(net.Addr) return fn(addr) } -func (c *Client) withAddrRw(addr net.Addr, fn func(*bufio.ReadWriter) error) (err error) { - cn, err := c.getConn(addr) +func (c *Client) withAddrRw(ctx context.Context, addr net.Addr, fn func(*bufio.ReadWriter) error) (err error) { + cn, err := c.getConn(ctx, addr) if err != nil { return err } @@ -441,12 +448,12 @@ func (c *Client) withAddrRw(addr net.Addr, fn func(*bufio.ReadWriter) error) (er func (c *Client) withKeyRw(ctx context.Context, key string, fn func(*bufio.ReadWriter) error) error { return c.withKeyAddr(ctx, key, func(addr net.Addr) error { - return c.withAddrRw(addr, fn) + return c.withAddrRw(ctx, addr, fn) }) } -func (c *Client) getFromAddr(addr net.Addr, keys []string, cb func(*Item)) error { - return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error { +func (c *Client) getFromAddr(ctx context.Context, addr net.Addr, keys []string, cb func(*Item)) error { + return c.withAddrRw(ctx, addr, func(rw *bufio.ReadWriter) error { if _, err := fmt.Fprintf(rw, "gets %s\r\n", strings.Join(keys, " ")); err != nil { return err } @@ -461,8 +468,8 @@ func (c *Client) getFromAddr(addr net.Addr, keys []string, cb func(*Item)) error } // flushAllFromAddr send the flush_all command to the given addr -func (c *Client) flushAllFromAddr(addr net.Addr) error { - return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error { +func (c *Client) flushAllFromAddr(ctx context.Context, addr net.Addr) error { + return c.withAddrRw(ctx, addr, func(rw *bufio.ReadWriter) error { if _, err := fmt.Fprintf(rw, "flush_all\r\n"); err != nil { return err } @@ -484,8 +491,8 @@ func (c *Client) flushAllFromAddr(addr net.Addr) error { } // ping sends the version command to the given addr -func (c *Client) ping(addr net.Addr) error { - return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error { +func (c *Client) ping(ctx context.Context, addr net.Addr) error { + return c.withAddrRw(ctx, addr, func(rw *bufio.ReadWriter) error { if _, err := fmt.Fprintf(rw, "version\r\n"); err != nil { return err } @@ -507,8 +514,8 @@ func (c *Client) ping(addr net.Addr) error { }) } -func (c *Client) touchFromAddr(addr net.Addr, keys []string, expiration int32) error { - return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error { +func (c *Client) touchFromAddr(ctx context.Context, addr net.Addr, keys []string, expiration int32) error { + return c.withAddrRw(ctx, addr, func(rw *bufio.ReadWriter) error { for _, key := range keys { if _, err := fmt.Fprintf(rw, "touch %s %d\r\n", key, expiration); err != nil { return err diff --git a/memcache/selector.go b/memcache/selector.go index e1337268..47ded8fa 100644 --- a/memcache/selector.go +++ b/memcache/selector.go @@ -33,7 +33,7 @@ type ServerSelector interface { // PickServer returns the server address that a given item // should be shared onto. PickServer(ctx context.Context, key string) (net.Addr, error) - Each(ctx context.Context, fn func(net.Addr) error) error + Each(ctx context.Context, fn func(context.Context, net.Addr) error) error } // ServerList is a simple ServerSelector. Its zero value is usable. @@ -91,11 +91,11 @@ func (ss *ServerList) SetServers(servers ...string) error { } // Each iterates over each server calling the given function -func (ss *ServerList) Each(_ context.Context, fn func(net.Addr) error) error { +func (ss *ServerList) Each(ctx context.Context, fn func(context.Context, net.Addr) error) error { ss.mu.RLock() defer ss.mu.RUnlock() for _, a := range ss.addrs { - if err := fn(a); nil != err { + if err := fn(ctx, a); nil != err { return err } } From 923d02dad59d0878ebddf2d067d40f883999bd54 Mon Sep 17 00:00:00 2001 From: Paulo Zenida Date: Thu, 17 Mar 2022 12:28:43 +0000 Subject: [PATCH 8/8] update unit test --- memcache/memcache_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/memcache/memcache_test.go b/memcache/memcache_test.go index f0077f90..47a0d36e 100644 --- a/memcache/memcache_test.go +++ b/memcache/memcache_test.go @@ -278,9 +278,10 @@ func BenchmarkOnItem(b *testing.B) { } }() + ctx := context.Background() addr := fakeServer.Addr() c := New(addr.String()) - if _, err := c.getConn(addr); err != nil { + if _, err := c.getConn(ctx, addr); err != nil { b.Fatal("failed to initialize connection to fake server") } @@ -288,6 +289,6 @@ func BenchmarkOnItem(b *testing.B) { dummyFn := func(_ *Client, _ *bufio.ReadWriter, _ *Item) error { return nil } b.ResetTimer() for i := 0; i < b.N; i++ { - c.onItem(context.Background(), &item, dummyFn) + c.onItem(ctx, &item, dummyFn) } }