From 027edff43a962d49f41921da787fcf0c92441d0c Mon Sep 17 00:00:00 2001 From: EyJunge1 Date: Tue, 11 Aug 2026 07:11:08 +0200 Subject: [PATCH 1/5] chore: migrate root module to stdlib errors Replace pkg/errors and go-multierror with fmt.Errorf, errors.Is, and errors.Join, tidy the root module to Go 1.26.5, and add error-chain tests. Co-authored-by: Cursor --- README.md | 2 +- client.go | 36 ++++---- client_cluster.go | 15 ++-- client_commands.go | 84 +++++++++--------- client_test.go | 12 +-- conn.go | 6 +- errors.go | 2 +- errors_test.go | 27 ++++++ go.mod | 202 ++----------------------------------------- go.work | 2 +- protocol.go | 34 ++++---- protocol_bin.go | 15 ++-- protocol_bin_test.go | 2 +- protocol_builder.go | 14 +-- protocol_meta.go | 20 ++--- protocol_test.go | 2 +- 16 files changed, 155 insertions(+), 320 deletions(-) create mode 100644 errors_test.go diff --git a/README.md b/README.md index c24f0d5..ba30e9c 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ Now, we have implemented some commands, and we will implement more commands in t #### Prerequisites -- Go 1.26 or higher +- Go 1.26.5 or higher - Python (for pre-commit hooks) or just `brew install pre-commit` on MacOS - Docker (for running memcached in tests) diff --git a/client.go b/client.go index 9790f24..438aa3e 100644 --- a/client.go +++ b/client.go @@ -3,12 +3,12 @@ package memcached import ( "bytes" "context" + "errors" + "fmt" "io" "sync" "time" - multierror "github.com/hashicorp/go-multierror" - "github.com/pkg/errors" "go.opentelemetry.io/otel/trace" "github.com/yeqown/memcached/telemetry" @@ -70,11 +70,11 @@ func newClientWithContext(_ context.Context, addr string, opts ...ClientOption) addrs, err := options.resolver.Resolve(addr) if err != nil { - return nil, errors.Wrap(err, "resolve failed") + return nil, fmt.Errorf("resolve failed: %w", err) } if len(addrs) == 0 { - return nil, errors.Wrap(ErrInvalidAddress, "empty address") + return nil, fmt.Errorf("empty address: %w", ErrInvalidAddress) } picker := options.pickBuilder.Build(addrs) @@ -100,7 +100,7 @@ func (c *client) Close() error { for _, pool := range c.connPools { if err := pool.close(); err != nil { - return errors.Wrap(err, "Close") + return fmt.Errorf("Close: %w", err) } } @@ -129,7 +129,7 @@ func (c *client) getConn(ctx context.Context, addr *Addr) (memcachedConn, error) cn, err = newConnContext(ctx2, addr, c.options.dialTimeout) if err != nil { - return nil, errors.Wrap(err, "newConnContext failed") + return nil, fmt.Errorf("newConnContext failed: %w", err) } // SASL auth if enabled @@ -196,12 +196,12 @@ func (c *client) broadcastRequest(ctx context.Context, call callFunc) error { wg.Wait() close(errCh) - var multiErr error + var errs []error for err := range errCh { - multiErr = multierror.Append(multiErr, err) + errs = append(errs, err) } - return multiErr + return errors.Join(errs...) } func (c *client) dispatchRequest(ctx context.Context, req *request, resp *response) error { @@ -213,7 +213,7 @@ func (c *client) dispatchRequest(ctx context.Context, req *request, resp *respon addr, err := c.picker.Pick(c.addrs, req.cmd, req.key) if err != nil { - return errors.Wrap(err, "pick node failed") + return fmt.Errorf("pick node failed: %w", err) } // START: Telemetry @@ -232,7 +232,7 @@ func (c *client) dispatchRequest(ctx context.Context, req *request, resp *respon if c.metrics != nil { c.metrics.RecordDuration(context.Background(), string(req.cmd), addr.Address, time.Since(start), err) } - return errors.Wrap(err, "alloc connection failed") + return fmt.Errorf("alloc connection failed: %w", err) } defer func() { _ = cn.release() }() @@ -245,7 +245,7 @@ func (c *client) dispatchRequest(ctx context.Context, req *request, resp *respon if c.metrics != nil { c.metrics.RecordDuration(context.Background(), string(req.cmd), addr.Address, time.Since(start), err) } - return errors.Wrap(err, "send failed") + return fmt.Errorf("send failed: %w", err) } recvErr := resp.recv(ctx, cn, c.options.readTimeout) @@ -275,13 +275,13 @@ func authSASL(conn memcachedConn, username, password string) error { // 1. first, list mechanisms the server supports req, resp := saslListMechanisms() if err := req.send(conn); err != nil { - return errors.Wrap(err, "authSASL send") + return fmt.Errorf("authSASL send: %w", err) } if err := resp.read(conn); err != nil { - return errors.Wrap(err, "authSASL recv") + return fmt.Errorf("authSASL recv: %w", err) } if err := resp.expect(_binaryStatusOK); err != nil { - return errors.Wrap(err, "authSASL") + return fmt.Errorf("authSASL: %w", err) } if !bytes.Contains(resp.value, []byte("PLAIN")) { @@ -291,13 +291,13 @@ func authSASL(conn memcachedConn, username, password string) error { // 2. choose one mechanism and send the authentication request req, resp = saslAuthRequestPlain(username, password) if err := req.send(conn); err != nil { - return errors.Wrap(err, "authSASL send") + return fmt.Errorf("authSASL send: %w", err) } if err := resp.read(conn); err != nil { - return errors.Wrap(err, "authSASL recv") + return fmt.Errorf("authSASL recv: %w", err) } if err := resp.expect(_binaryStatusOK); err != nil { - return errors.Wrap(err, "authSASL") + return fmt.Errorf("authSASL: %w", err) } return nil diff --git a/client_cluster.go b/client_cluster.go index c16f4a6..11845f8 100644 --- a/client_cluster.go +++ b/client_cluster.go @@ -1,12 +1,11 @@ package memcached import ( + "fmt" "hash/crc32" "net" "strings" - "github.com/pkg/errors" - "github.com/yeqown/memcached/hash" ) @@ -52,7 +51,7 @@ type defaultResolver struct{} func (r defaultResolver) Resolve(addr string) ([]*Addr, error) { if addr == "" { - return nil, errors.Wrap(ErrInvalidAddress, "empty address") + return nil, fmt.Errorf("empty address: %w", ErrInvalidAddress) } addrs := strings.Split(addr, ",") @@ -73,7 +72,7 @@ func (r defaultResolver) Resolve(addr string) ([]*Addr, error) { } if len(result) == 0 { - return nil, errors.Wrap(ErrInvalidAddress, "no available address") + return nil, fmt.Errorf("no available address: %w", ErrInvalidAddress) } return result, nil @@ -83,7 +82,7 @@ func (r defaultResolver) Resolve(addr string) ([]*Addr, error) { func (r defaultResolver) resolveAddr(address string) (network, addr string, err error) { address = strings.TrimSpace(address) if address == "" { - return "", "", errors.Wrap(ErrInvalidAddress, "empty address") + return "", "", fmt.Errorf("empty address: %w", ErrInvalidAddress) } network = "tcp" @@ -108,7 +107,7 @@ func (r defaultResolver) resolveAddr(address string) (network, addr string, err } if err != nil { - return "", "", errors.Wrap(err, "invalid address: "+address) + return "", "", fmt.Errorf("invalid address: %s: %w", address, err) } return network, addr, nil @@ -121,7 +120,7 @@ type crc32HashPicker struct{} func (p *crc32HashPicker) Pick(addrs []*Addr, _, key []byte) (*Addr, error) { n := len(addrs) if n == 0 { - return nil, errors.Wrap(ErrInvalidAddress, "no available address") + return nil, fmt.Errorf("no available address: %w", ErrInvalidAddress) } if n == 1 { return addrs[0], nil @@ -150,7 +149,7 @@ type murmur3HashPicker struct { func (p *murmur3HashPicker) Pick(addrs []*Addr, _, key []byte) (*Addr, error) { n := len(addrs) if n == 0 { - return nil, errors.Wrap(ErrInvalidAddress, "no available address") + return nil, fmt.Errorf("no available address: %w", ErrInvalidAddress) } if n == 1 { return addrs[0], nil diff --git a/client_commands.go b/client_commands.go index 017ca29..1544a36 100644 --- a/client_commands.go +++ b/client_commands.go @@ -3,9 +3,9 @@ package memcached import ( "bytes" "context" + "errors" + "fmt" "time" - - "github.com/pkg/errors" ) type basicTextProtocolCommander interface { @@ -144,16 +144,16 @@ func (c *client) storageCommand(ctx context.Context, command, key string, value req, resp, err := buildStorageCommand(command, key, value, flag, expiry, c.options.noReply, c.options.codec) if err != nil { - return errors.Wrap(err, "build storage command failed") + return fmt.Errorf("build storage command failed: %w", err) } defer releaseReqAndResp(req, resp) if err = c.dispatchRequest(ctx, req, resp); err != nil { - return errors.Wrap(err, "request failed") + return fmt.Errorf("request failed: %w", err) } if err = resp.expect(_StoredCRLFBytes); err != nil { - return errors.Wrap(ErrMalformedResponse, err.Error()) + return fmt.Errorf("%s: %w", err.Error(), ErrMalformedResponse) } return nil @@ -191,11 +191,11 @@ func (c *client) Cas(ctx context.Context, key string, value []byte, flag uint32, defer releaseReqAndResp(req, resp) if err = c.dispatchRequest(ctx, req, resp); err != nil { - return errors.Wrap(err, "request failed") + return fmt.Errorf("request failed: %w", err) } if err = resp.expect(_StoredCRLFBytes); err != nil { - return errors.Wrap(ErrMalformedResponse, err.Error()) + return fmt.Errorf("%s: %w", err.Error(), ErrMalformedResponse) } return nil @@ -214,15 +214,15 @@ func (c *client) Get(ctx context.Context, key string) (*Item, error) { defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") + return nil, fmt.Errorf("request failed: %w", err) } items, err := parseValueItems(resp.rawLines, false, false, c.options.codec) if err != nil { - return nil, errors.Wrap(err, "parse values failed") + return nil, fmt.Errorf("parse values failed: %w", err) } if len(items) == 0 { - return nil, errors.Wrap(ErrNotFound, "no items found") + return nil, fmt.Errorf("no items found: %w", ErrNotFound) } return items[0], nil @@ -237,15 +237,15 @@ func (c *client) Gets(ctx context.Context, keys ...string) ([]*Item, error) { defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") + return nil, fmt.Errorf("request failed: %w", err) } items, err := parseValueItems(resp.rawLines, false, true, c.options.codec) if err != nil { - return nil, errors.Wrap(ErrMalformedResponse, "parse values failed") + return nil, fmt.Errorf("parse values failed: %w", ErrMalformedResponse) } if len(items) == 0 { - return nil, errors.Wrap(ErrNotFound, "no items found") + return nil, fmt.Errorf("no items found: %w", ErrNotFound) } return items, nil @@ -260,16 +260,16 @@ func (c *client) GetAndTouch(ctx context.Context, expiry time.Duration, key stri defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") + return nil, fmt.Errorf("request failed: %w", err) } items, err := parseValueItems(resp.rawLines, false, false, c.options.codec) if err != nil { - return nil, errors.Wrap(ErrMalformedResponse, "parse values failed") + return nil, fmt.Errorf("parse values failed: %w", ErrMalformedResponse) } if len(items) == 0 { - return nil, errors.Wrap(ErrNotFound, "no items found") + return nil, fmt.Errorf("no items found: %w", ErrNotFound) } return items[0], nil @@ -284,17 +284,17 @@ func (c *client) GetAndTouches(ctx context.Context, expiry time.Duration, keys . defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") + return nil, fmt.Errorf("request failed: %w", err) } // parse response items, err := parseValueItems(resp.rawLines, false, true, c.options.codec) if err != nil { - return nil, errors.Wrap(ErrMalformedResponse, "parse values failed") + return nil, fmt.Errorf("parse values failed: %w", ErrMalformedResponse) } if len(items) == 0 { - return nil, errors.Wrap(ErrNotFound, "no items found") + return nil, fmt.Errorf("no items found: %w", ErrNotFound) } return items, nil @@ -313,12 +313,12 @@ func (c *client) Delete(ctx context.Context, key string) error { defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return errors.Wrap(err, "request failed") + return fmt.Errorf("request failed: %w", err) } // expect DELETED\r\n if err := resp.expect(_DeletedCRLFBytes); err != nil { - return errors.Wrap(ErrMalformedResponse, err.Error()) + return fmt.Errorf("%s: %w", err.Error(), ErrMalformedResponse) } return nil @@ -331,13 +331,13 @@ func (c *client) Incr(ctx context.Context, key string, delta uint64) (uint64, er req, resp := buildArithmeticCommand("incr", key, delta, c.options.noReply) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return 0, errors.Wrap(err, "request failed") + return 0, fmt.Errorf("request failed: %w", err) } // parse response value, err := parseArithmetic(resp.rawLines[0]) if err != nil { - return 0, errors.Wrap(ErrMalformedResponse, err.Error()) + return 0, fmt.Errorf("%s: %w", err.Error(), ErrMalformedResponse) } return value, nil @@ -350,13 +350,13 @@ func (c *client) Decr(ctx context.Context, key string, delta uint64) (uint64, er req, resp := buildArithmeticCommand("decr", key, delta, c.options.noReply) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return 0, errors.Wrap(err, "request failed") + return 0, fmt.Errorf("request failed: %w", err) } // parse response value, err := parseArithmetic(resp.rawLines[0]) if err != nil { - return 0, errors.Wrap(ErrMalformedResponse, err.Error()) + return 0, fmt.Errorf("%s: %w", err.Error(), ErrMalformedResponse) } return value, nil @@ -371,12 +371,12 @@ func (c *client) Touch(ctx context.Context, key string, expiry time.Duration) er defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return errors.Wrap(err, "request failed") + return fmt.Errorf("request failed: %w", err) } // expect TOUCHED\r\n if err := resp.expect(_TouchedCRLFBytes); err != nil { - return errors.Wrap(ErrMalformedResponse, err.Error()) + return fmt.Errorf("%s: %w", err.Error(), ErrMalformedResponse) } return nil @@ -387,14 +387,14 @@ func (c *client) Version(ctx context.Context) (string, error) { defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return "", errors.Wrap(err, "request") + return "", fmt.Errorf("request: %w", err) } // parse version number from response // VERSION 1.6.14 line := resp.rawLines[0] if !bytes.HasPrefix(line, _VersionBytes) { - return "", errors.Wrap(ErrMalformedResponse, string(line)) + return "", fmt.Errorf("%s: %w", string(line), ErrMalformedResponse) } return string(trimCRLF(line[8:])), nil @@ -408,22 +408,22 @@ func (c *client) FlushAll(ctx context.Context) error { c.autoSwitchToUDP(ctx, req, resp) if err := req.send(ctx, cn, c.options.writeTimeout); err != nil { - return errors.Wrap(err, "send failed") + return fmt.Errorf("send failed: %w", err) } if err := resp.recv(ctx, cn, c.options.readTimeout); err != nil { - return errors.Wrap(err, "recv failed") + return fmt.Errorf("recv failed: %w", err) } // expect OK\r\n if err := resp.expect(_OKCRLFBytes); err != nil { - return errors.Wrap(ErrMalformedResponse, err.Error()) + return fmt.Errorf("%s: %w", err.Error(), ErrMalformedResponse) } return nil } if err := c.broadcastRequest(ctx, call); err != nil { - return errors.Wrap(err, "request failed") + return fmt.Errorf("request failed: %w", err) } return nil @@ -451,7 +451,7 @@ func (c *client) MetaSet(ctx context.Context, key, value []byte, msOptions ...Me } defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") + return nil, fmt.Errorf("request failed: %w", err) } item := &MetaItem{ @@ -486,7 +486,7 @@ func (c *client) MetaGet(ctx context.Context, key []byte, mgOptions ...MetaGetOp defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") + return nil, fmt.Errorf("request failed: %w", err) } item := &MetaItem{ @@ -513,7 +513,7 @@ func (c *client) MetaDelete(ctx context.Context, key []byte, options ...MetaDele defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") + return nil, fmt.Errorf("request failed: %w", err) } item := &MetaItem{ @@ -540,7 +540,7 @@ func (c *client) MetaArithmetic(ctx context.Context, key []byte, delta uint64, o defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") + return nil, fmt.Errorf("request failed: %w", err) } item := &MetaItem{ @@ -579,7 +579,7 @@ func (c *client) MetaDebug(ctx context.Context, key []byte, options ...MetaDebug defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") + return nil, fmt.Errorf("request failed: %w", err) } item := &MetaItemDebug{ @@ -598,14 +598,14 @@ func (c *client) MetaNoOp(ctx context.Context) error { defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return errors.Wrap(err, "request failed") + return fmt.Errorf("request failed: %w", err) } if err := resp.expect(_MetaMNCRLFBytes); err != nil { if errors.Is(err, ErrMalformedResponse) { return err } - return errors.Wrap(ErrMalformedResponse, err.Error()) + return fmt.Errorf("%s: %w", err.Error(), ErrMalformedResponse) } return nil @@ -621,7 +621,7 @@ func (c *client) Stats(ctx context.Context) (*Statistic, error) { defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") + return nil, fmt.Errorf("request failed: %w", err) } return parseStats(resp.rawLines) @@ -632,7 +632,7 @@ func (c *client) Raw(ctx context.Context, cmd string) ([]string, error) { defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") + return nil, fmt.Errorf("request failed: %w", err) } lines := make([]string, 0, len(resp.rawLines)) diff --git a/client_test.go b/client_test.go index 3f767cb..17108c6 100644 --- a/client_test.go +++ b/client_test.go @@ -2,11 +2,11 @@ package memcached import ( "context" + "errors" "sync" "testing" "time" - pkgerrors "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -137,7 +137,7 @@ func (su *clientTestSuite) Test_concurrent() { for counter <= 200 { item, err := su.client.Get(ctx, key) - if pkgerrors.Is(err, ErrNotFound) { + if errors.Is(err, ErrNotFound) { goto next } su.NoError(err) @@ -160,7 +160,7 @@ func (su *clientTestSuite) Test_concurrent() { for counter <= 100 { err := su.client.Touch(ctx, key, 3) - if !pkgerrors.Is(err, ErrNotFound) { + if !errors.Is(err, ErrNotFound) { su.NoError(err) } @@ -247,11 +247,11 @@ func TestCompressionDisablesAppendPrepend(t *testing.T) { err := client.Append(context.Background(), "key", []byte("value"), 0, 0) require.Error(t, err) - assert.True(t, pkgerrors.Is(err, ErrNotSupported)) + assert.True(t, errors.Is(err, ErrNotSupported)) err = client.Prepend(context.Background(), "key", []byte("value"), 0, 0) require.Error(t, err) - assert.True(t, pkgerrors.Is(err, ErrNotSupported)) + assert.True(t, errors.Is(err, ErrNotSupported)) } func TestCompressionDisablesMetaAppendPrepend(t *testing.T) { @@ -269,7 +269,7 @@ func TestCompressionDisablesMetaAppendPrepend(t *testing.T) { ) assert.Nil(t, item) require.Error(t, err) - assert.True(t, pkgerrors.Is(err, ErrNotSupported)) + assert.True(t, errors.Is(err, ErrNotSupported)) }) } } diff --git a/conn.go b/conn.go index f5fc7d9..7b5c8e6 100644 --- a/conn.go +++ b/conn.go @@ -3,14 +3,14 @@ package memcached import ( "bufio" "context" + "errors" + "fmt" "io" "net" "strconv" "sync" "sync/atomic" "time" - - "github.com/pkg/errors" ) type nowFuncType func() time.Time @@ -121,7 +121,7 @@ type conn struct { func newConnContext(ctx context.Context, addr *Addr, dialTimeout time.Duration) (*conn, error) { rawConn, err := addr.dial(ctx, dialTimeout) if err != nil { - return nil, errors.Wrap(err, "dialContext") + return nil, fmt.Errorf("dialContext: %w", err) } cn := &conn{ diff --git a/errors.go b/errors.go index 8dce902..4e59271 100644 --- a/errors.go +++ b/errors.go @@ -1,7 +1,7 @@ package memcached import ( - "github.com/pkg/errors" + "errors" ) var ( diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 0000000..ea140c0 --- /dev/null +++ b/errors_test.go @@ -0,0 +1,27 @@ +package memcached + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWrappedErrorChainPreservesSentinel(t *testing.T) { + err := fmt.Errorf("request failed: %w", fmt.Errorf("send failed: %w", ErrNotFound)) + assert.True(t, errors.Is(err, ErrNotFound)) + assert.Contains(t, err.Error(), "request failed") + assert.Contains(t, err.Error(), "send failed") +} + +func TestErrorsJoinPreservesJoinedSentinels(t *testing.T) { + joined := errors.Join( + fmt.Errorf("node-a: %w", ErrNotFound), + fmt.Errorf("node-b: %w", ErrServerError), + ) + require.Error(t, joined) + assert.True(t, errors.Is(joined, ErrNotFound)) + assert.True(t, errors.Is(joined, ErrServerError)) +} diff --git a/go.mod b/go.mod index d097580..e48cfb7 100644 --- a/go.mod +++ b/go.mod @@ -1,210 +1,24 @@ module github.com/yeqown/memcached -go 1.26.0 +go 1.26.5 require ( - github.com/hashicorp/go-multierror v1.1.1 + github.com/golang/snappy v1.0.0 + github.com/klauspost/compress v1.19.2 + github.com/pierrec/lz4/v4 v4.1.28 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.11.1 - go.opentelemetry.io/otel v1.40.0 - go.opentelemetry.io/otel/metric v1.40.0 - go.opentelemetry.io/otel/trace v1.40.0 + go.opentelemetry.io/otel v1.45.0 + go.opentelemetry.io/otel/metric v1.45.0 + go.opentelemetry.io/otel/trace v1.45.0 ) require ( - 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect - 4d63.com/gochecknoglobals v0.2.2 // indirect - github.com/4meepo/tagalign v1.4.2 // indirect - github.com/Abirdcfly/dupword v0.1.3 // indirect - github.com/Antonboom/errname v1.0.0 // indirect - github.com/Antonboom/nilnil v1.0.1 // indirect - github.com/Antonboom/testifylint v1.5.2 // indirect - github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect - github.com/Crocmagnon/fatcontext v0.7.1 // indirect - github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect - github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 // indirect - github.com/Masterminds/semver/v3 v3.3.0 // indirect - github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect - github.com/alecthomas/go-check-sumtype v0.3.1 // indirect - github.com/alexkohler/nakedret/v2 v2.0.5 // indirect - github.com/alexkohler/prealloc v1.0.0 // indirect - github.com/alingse/asasalint v0.0.11 // indirect - github.com/alingse/nilnesserr v0.1.2 // indirect - github.com/ashanbrown/forbidigo v1.6.0 // indirect - github.com/ashanbrown/makezero v1.2.0 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/bkielbasa/cyclop v1.2.3 // indirect - github.com/blizzy78/varnamelen v0.8.0 // indirect - github.com/bombsimon/wsl/v4 v4.5.0 // indirect - github.com/breml/bidichk v0.3.2 // indirect - github.com/breml/errchkjson v0.4.0 // indirect - github.com/butuzov/ireturn v0.3.1 // indirect - github.com/butuzov/mirror v1.3.0 // indirect - github.com/catenacyber/perfsprint v0.8.2 // indirect - github.com/ccojocar/zxcvbn-go v1.0.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/charithe/durationcheck v0.0.10 // indirect - github.com/chavacava/garif v0.1.0 // indirect - github.com/ckaznocha/intrange v0.3.0 // indirect - github.com/curioswitch/go-reassign v0.3.0 // indirect - github.com/daixiang0/gci v0.13.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/denis-tingaikin/go-header v0.5.0 // indirect - github.com/ettle/strcase v0.2.0 // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/fatih/structtag v1.2.0 // indirect - github.com/firefart/nonamedreturns v1.0.5 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/fzipp/gocyclo v0.6.0 // indirect - github.com/ghostiam/protogetter v0.3.9 // indirect - github.com/go-critic/go-critic v0.12.0 // indirect - github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-toolsmith/astcast v1.1.0 // indirect - github.com/go-toolsmith/astcopy v1.1.0 // indirect - github.com/go-toolsmith/astequal v1.2.0 // indirect - github.com/go-toolsmith/astfmt v1.1.0 // indirect - github.com/go-toolsmith/astp v1.1.0 // indirect - github.com/go-toolsmith/strparse v1.1.0 // indirect - github.com/go-toolsmith/typep v1.1.0 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect - github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect - github.com/gobwas/glob v0.2.3 // indirect - github.com/gofrs/flock v0.12.1 // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/golang/snappy v1.0.0 // indirect - github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect - github.com/golangci/go-printf-func-name v0.1.0 // indirect - github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect - github.com/golangci/golangci-lint v1.64.8 // indirect - github.com/golangci/misspell v0.6.0 // indirect - github.com/golangci/plugin-module-register v0.1.1 // indirect - github.com/golangci/revgrep v0.8.0 // indirect - github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/gordonklaus/ineffassign v0.1.0 // indirect - github.com/gostaticanalysis/analysisutil v0.7.1 // indirect - github.com/gostaticanalysis/comment v1.5.0 // indirect - github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect - github.com/gostaticanalysis/nilerr v0.1.1 // indirect - github.com/hashicorp/errwrap v1.0.0 // indirect - github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect - github.com/hashicorp/go-version v1.7.0 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/hexops/gotextdiff v1.0.3 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jgautheron/goconst v1.7.1 // indirect - github.com/jingyugao/rowserrcheck v1.1.1 // indirect - github.com/jjti/go-spancheck v0.6.4 // indirect - github.com/julz/importas v0.2.0 // indirect - github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect - github.com/kisielk/errcheck v1.9.0 // indirect - github.com/kkHAIKE/contextcheck v1.1.6 // indirect - github.com/klauspost/compress v1.18.6 // indirect - github.com/kulti/thelper v0.6.3 // indirect - github.com/kunwardeep/paralleltest v1.0.10 // indirect - github.com/lasiar/canonicalheader v1.1.2 // indirect - github.com/ldez/exptostd v0.4.2 // indirect - github.com/ldez/gomoddirectives v0.6.1 // indirect - github.com/ldez/grignotin v0.9.0 // indirect - github.com/ldez/tagliatelle v0.7.1 // indirect - github.com/ldez/usetesting v0.4.2 // indirect - github.com/leonklingele/grouper v1.1.2 // indirect - github.com/macabu/inamedparam v0.1.3 // indirect - github.com/magiconair/properties v1.8.6 // indirect - github.com/maratori/testableexamples v1.0.0 // indirect - github.com/maratori/testpackage v1.1.1 // indirect - github.com/matoous/godox v1.1.0 // indirect - github.com/matryer/is v1.4.1 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect - github.com/mgechev/revive v1.7.0 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/moricho/tparallel v0.3.2 // indirect - github.com/nakabonne/nestif v0.3.1 // indirect - github.com/nishanths/exhaustive v0.12.0 // indirect - github.com/nishanths/predeclared v0.2.2 // indirect - github.com/nunnatsa/ginkgolinter v0.19.1 // indirect - github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect - github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/polyfloyd/go-errorlint v1.7.1 // indirect - github.com/prometheus/client_golang v1.12.1 // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.32.1 // indirect - github.com/prometheus/procfs v0.7.3 // indirect - github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect - github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect - github.com/quasilyte/gogrep v0.5.0 // indirect - github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect - github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect - github.com/raeperd/recvcheck v0.2.0 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/ryancurrah/gomodguard v1.3.5 // indirect - github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect - github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect - github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect - github.com/sashamelentyev/interfacebloat v1.1.0 // indirect - github.com/sashamelentyev/usestdlibvars v1.28.0 // indirect - github.com/securego/gosec/v2 v2.22.2 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/sivchari/containedctx v1.0.3 // indirect - github.com/sivchari/tenv v1.12.1 // indirect - github.com/sonatard/noctx v0.1.0 // indirect - github.com/sourcegraph/go-diff v0.7.0 // indirect - github.com/spf13/afero v1.12.0 // indirect - github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/cobra v1.9.1 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.6 // indirect - github.com/spf13/viper v1.12.0 // indirect - github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect - github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect - github.com/stretchr/objx v0.5.2 // indirect - github.com/subosito/gotenv v1.4.1 // indirect - github.com/tdakkota/asciicheck v0.4.1 // indirect - github.com/tetafro/godot v1.5.0 // indirect - github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 // indirect - github.com/timonwong/loggercheck v0.10.1 // indirect - github.com/tomarrell/wrapcheck/v2 v2.10.0 // indirect - github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect - github.com/ultraware/funlen v0.2.0 // indirect - github.com/ultraware/whitespace v0.2.0 // indirect - github.com/uudashr/gocognit v1.2.0 // indirect - github.com/uudashr/iface v1.3.1 // indirect - github.com/xen0n/gosmopolitan v1.2.2 // indirect - github.com/yagipy/maintidx v1.0.0 // indirect - github.com/yeya24/promlinter v0.3.0 // indirect - github.com/ykadowak/zerologlint v0.1.5 // indirect - gitlab.com/bosi/decorder v0.4.2 // indirect - go-simpler.org/musttag v0.13.0 // indirect - go-simpler.org/sloglint v0.9.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.uber.org/atomic v1.7.0 // indirect - go.uber.org/automaxprocs v1.6.0 // indirect - go.uber.org/goleak v1.3.0 // indirect - go.uber.org/multierr v1.6.0 // indirect - go.uber.org/zap v1.24.0 // indirect - golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/tools v0.31.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - honnef.co/go/tools v0.6.1 // indirect - mvdan.cc/gofumpt v0.7.0 // indirect - mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect ) - -tool github.com/golangci/golangci-lint/cmd/golangci-lint diff --git a/go.work b/go.work index a4b482a..cc5a23f 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,4 @@ -go 1.26.0 +go 1.26.5 use ( . diff --git a/protocol.go b/protocol.go index 7c80a1f..ba7954e 100644 --- a/protocol.go +++ b/protocol.go @@ -3,11 +3,10 @@ package memcached import ( "bytes" "encoding/json" + "fmt" "log" "strconv" "time" - - "github.com/pkg/errors" ) // Codec transforms value and flags at the protocol boundary. @@ -27,7 +26,7 @@ type Codec interface { func checkCodecSupportsOperation(codec Codec, operation string) error { if err := codec.SupportsOperation(operation); err != nil { - return errors.Wrap(ErrNotSupported, err.Error()) + return fmt.Errorf("%s: %w", err.Error(), ErrNotSupported) } return nil } @@ -147,9 +146,8 @@ func buildStorageCommand( noReply bool, codec Codec, ) (*request, *response, error) { - if err := checkCodecSupportsOperation(codec, command); err != nil { - return nil, nil, errors.Wrap(err, "codec does not support operation") + return nil, nil, fmt.Errorf("codec does not support operation: %w", err) } evalue, eflags, err := codec.Encode([]byte(key), value, flags) @@ -238,7 +236,7 @@ func buildCasCommand( key string, value []byte, flag uint32, expTime time.Duration, casUnique uint64, noReply bool, codec Codec, ) (*request, *response, error) { if err := checkCodecSupportsOperation(codec, "cas"); err != nil { - return nil, nil, errors.Wrap(err, "codec does not support operation") + return nil, nil, fmt.Errorf("codec does not support operation: %w", err) } evalue, eflag, err := codec.Encode([]byte(key), value, flag) @@ -328,11 +326,11 @@ func parseValueItems(lines [][]byte, withoutEndLine, withCAS bool, codec Codec) n := len(lines) if withoutEndLine && n%2 != 0 { // n must be even - return nil, errors.Wrap(ErrMalformedResponse, "want times of 2 lines, got "+strconv.Itoa(n)) + return nil, fmt.Errorf("want times of 2 lines, got %d: %w", n, ErrMalformedResponse) } if !withoutEndLine && n%2 == 0 { // n must be odd - return nil, errors.Wrap(ErrMalformedResponse, "want odd lines, got "+strconv.Itoa(n)) + return nil, fmt.Errorf("want odd lines, got %d: %w", n, ErrMalformedResponse) } var ( @@ -365,11 +363,11 @@ func parseValueItems(lines [][]byte, withoutEndLine, withCAS bool, codec Codec) // Read the data block if i+1 >= n { - return nil, errors.Wrap(ErrMalformedResponse, "missing data block") + return nil, fmt.Errorf("missing data block: %w", ErrMalformedResponse) } item.Value = trimCRLF(lines[i+1]) if len(item.Value) != int(dataLen) { - return nil, errors.Wrap(ErrMalformedResponse, "data block length mismatch") + return nil, fmt.Errorf("data block length mismatch: %w", ErrMalformedResponse) } decodedValue, decodedFlags, err := codec.Decode([]byte(item.Key), item.Value, uint32(item.Flags)) @@ -404,7 +402,7 @@ func parseValueLine(line []byte, item *Item, withCas bool) (dataLen uint64, err for i := start; i < n; i++ { if nField > 5 || (!withCas && nField > 4) { - return 0, errors.Wrap(ErrMalformedResponse, "invalid VALUE line") + return 0, fmt.Errorf("invalid VALUE line: %w", ErrMalformedResponse) } if line[i] != ' ' && i != n-1 { @@ -419,7 +417,7 @@ func parseValueLine(line []byte, item *Item, withCas bool) (dataLen uint64, err case flagsIndex: flags, err := parseUintFromBytes(line[fieldStart:i]) if err != nil { - return 0, errors.Wrap(ErrMalformedResponse, "invalid flags") + return 0, fmt.Errorf("invalid flags: %w", ErrMalformedResponse) } item.Flags = uint32(flags) case dataLenIndex: @@ -429,7 +427,7 @@ func parseValueLine(line []byte, item *Item, withCas bool) (dataLen uint64, err } dataLen, err = parseUintFromBytes(line[fieldStart:si]) if err != nil { - return 0, errors.Wrap(ErrMalformedResponse, "invalid data length") + return 0, fmt.Errorf("invalid data length: %w", ErrMalformedResponse) } case casIndex: si := i @@ -438,7 +436,7 @@ func parseValueLine(line []byte, item *Item, withCas bool) (dataLen uint64, err } item.CAS, err = parseUintFromBytes(line[fieldStart:si]) if err != nil { - return 0, errors.Wrap(ErrMalformedResponse, "invalid CAS") + return 0, fmt.Errorf("invalid CAS: %w", ErrMalformedResponse) } } @@ -465,7 +463,7 @@ func parseUintFromBytes(bs []byte) (uint64, error) { r := uint64(0) for _, b := range bs { if b < '0' || b > '9' { - return 0, errors.Wrap(ErrMalformedResponse, "invalid uint number") + return 0, fmt.Errorf("invalid uint number: %w", ErrMalformedResponse) } r = r*10 + uint64(b-'0') @@ -617,7 +615,7 @@ type Statistic struct { func parseStats(lines [][]byte) (*Statistic, error) { if len(lines) <= 0 { - return nil, errors.Wrap(ErrMalformedResponse, "empty response") + return nil, fmt.Errorf("empty response: %w", ErrMalformedResponse) } transitionMap := make(map[string]any, len(lines)) @@ -662,12 +660,12 @@ func parseStats(lines [][]byte) (*Statistic, error) { raw, err := json.Marshal(transitionMap) if err != nil { - return nil, errors.Wrap(err, "parseStats marshal transition map failed") + return nil, fmt.Errorf("parseStats marshal transition map failed: %w", err) } stat := &Statistic{} if err = json.Unmarshal(raw, stat); err != nil { - return nil, errors.Wrap(err, "parseStats unmarshal failed") + return nil, fmt.Errorf("parseStats unmarshal failed: %w", err) } return stat, nil diff --git a/protocol_bin.go b/protocol_bin.go index 6083633..824cd39 100644 --- a/protocol_bin.go +++ b/protocol_bin.go @@ -2,9 +2,8 @@ package memcached import ( "encoding/binary" + "fmt" "io" - - "github.com/pkg/errors" ) // https://docs.memcached.org/protocols/binary/ @@ -199,25 +198,25 @@ func (br *binaryResponse) expect(status uint16) error { case _binaryStatusNotSupported: return ErrNotSupported case _binaryStatusInternalError: - return errors.Wrap(ErrServerError, "internal error") + return fmt.Errorf("internal error: %w", ErrServerError) case _binaryStatusInvalidArgs: return ErrInvalidArgument case _binaryStatusOutOfMemory: - return errors.Wrap(ErrServerError, "out of memory") + return fmt.Errorf("out of memory: %w", ErrServerError) } // return: status: 0x1234 format - return errors.Wrapf(ErrServerError, "status: %x", br.status) + return fmt.Errorf("status: %x: %w", br.status, ErrServerError) } func (br *binaryResponse) read(rr io.Reader) error { buf := make([]byte, 24) if _, err := io.ReadFull(rr, buf); err != nil { - return errors.Wrap(err, "read header") + return fmt.Errorf("read header: %w", err) } if magic := buf[0]; magic != _binaryMagicRes { - return errors.Wrapf(ErrInvalidBinaryProtocol, "invalid magic: %d", magic) + return fmt.Errorf("invalid magic: %d: %w", magic, ErrInvalidBinaryProtocol) } br.opcode = buf[1] @@ -236,7 +235,7 @@ func (br *binaryResponse) read(rr io.Reader) error { // read the whole body and split them into extras, key, value body := make([]byte, br.totalBodyLength) if _, err := io.ReadFull(rr, body); err != nil { - return errors.Wrap(err, "read body") + return fmt.Errorf("read body: %w", err) } s := uint32(0) diff --git a/protocol_bin_test.go b/protocol_bin_test.go index 69ccccc..aabb253 100644 --- a/protocol_bin_test.go +++ b/protocol_bin_test.go @@ -2,10 +2,10 @@ package memcached import ( "bytes" + "errors" "io" "testing" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) diff --git a/protocol_builder.go b/protocol_builder.go index 4d7da57..949387e 100644 --- a/protocol_builder.go +++ b/protocol_builder.go @@ -4,13 +4,13 @@ import ( "bytes" "context" "encoding/base64" + "errors" + "fmt" "math" "strconv" "sync" "time" "unsafe" - - "github.com/pkg/errors" ) const ( @@ -53,10 +53,10 @@ func forecastCommonFaultLine(line []byte) error { return ErrNonexistentCommand case bytes.HasPrefix(line, []byte("CLIENT_ERROR")): message := string(line[12 : len(line)-2]) - return errors.Wrap(ErrClientError, message) + return fmt.Errorf("%s: %w", message, ErrClientError) case bytes.HasPrefix(line, []byte("SERVER_ERROR")): message := string(line[12 : len(line)-2]) - return errors.Wrap(ErrServerError, message) + return fmt.Errorf("%s: %w", message, ErrServerError) case bytes.Equal(line, []byte("NOT_FOUND\r\n")): return ErrNotFound case bytes.Equal(line, []byte("EXISTS\r\n")): @@ -468,7 +468,7 @@ func (resp *response) read1(rr memcachedConn) error { for read < int(resp.limitedLines) { line, err := rr.readLine('\n') if err != nil { - return errors.Wrap(err, "dispatchRequest read") + return fmt.Errorf("dispatchRequest read: %w", err) } if read == 0 { @@ -495,7 +495,7 @@ func (resp *response) read2(rr memcachedConn) error { // FIXME(@yeqown): read line would cost too much capacity. line, err := rr.readLine('\n') if err != nil { - return errors.Wrap(err, "dispatchRequest read") + return fmt.Errorf("dispatchRequest read: %w", err) } if read == 0 && resp.udpEnabled { @@ -528,7 +528,7 @@ func (resp *response) expect(line []byte) error { return nil } if n := len(resp.rawLines); n != 1 { - return errors.Wrapf(ErrMalformedResponse, "expect only 1 line, but got %d", n) + return fmt.Errorf("expect only 1 line, but got %d: %w", n, ErrMalformedResponse) } if bytes.Equal(resp.rawLines[0], line) { diff --git a/protocol_meta.go b/protocol_meta.go index 660814e..85b53a8 100644 --- a/protocol_meta.go +++ b/protocol_meta.go @@ -4,8 +4,6 @@ import ( "bytes" "fmt" "strconv" - - "github.com/pkg/errors" ) type metaSetFlags struct { @@ -118,13 +116,13 @@ func buildMetaSetCommand(key, value []byte, flags *metaSetFlags, codec Codec) (* operation = string(MetaSetModeSet) } if err := checkCodecSupportsOperation(codec, operation); err != nil { - return nil, nil, errors.Wrap(err, "codec does not support operation") + return nil, nil, fmt.Errorf("codec does not support operation: %w", err) } // codec hook evalue, eflags, err := codec.Encode(key, value, flags.F) if err != nil { - return nil, nil, errors.Wrap(err, "encode value and flags") + return nil, nil, fmt.Errorf("encode value and flags: %w", err) } flags.F = eflags @@ -358,13 +356,13 @@ func parseMetaItem(lines [][]byte, item *MetaItem, noReply bool, codec Codec) er // figure out the if len(lines) == 0 { - return errors.Wrap(ErrMalformedResponse, "missing response") + return fmt.Errorf("missing response: %w", ErrMalformedResponse) } // Normal CD handling parts := bytes.Split(trimCRLF(lines[0]), _SpaceBytes) if len(parts) < 1 { - return errors.Wrap(ErrMalformedResponse, "invalid response") + return fmt.Errorf("invalid response: %w", ErrMalformedResponse) } const ( CDIndex = 0 @@ -393,12 +391,12 @@ func parseMetaItem(lines [][]byte, item *MetaItem, noReply bool, codec Codec) er parseFlags(parts, 2, item) if len(lines) < 2 { - return errors.Wrap(ErrMalformedResponse, "missing value") + return fmt.Errorf("missing value: %w", ErrMalformedResponse) } var err error if item.Value, item.Flags, err = codec.Decode(item.Key, trimCRLF(lines[1]), item.Flags); err != nil { - return errors.Wrap(err, "codec decode") + return fmt.Errorf("codec decode: %w", err) } return nil @@ -711,12 +709,12 @@ func buildMetaDebugCommand(key []byte, flags *metaDebugFlags) (*request, *respon // failed: EN\r\n func parseMetaItemDebug(lines [][]byte, item *MetaItemDebug) error { if len(lines) != 1 { - return errors.Wrap(ErrMalformedResponse, "invalid response") + return fmt.Errorf("invalid response: %w", ErrMalformedResponse) } parts := bytes.Split(trimCRLF(lines[0]), _SpaceBytes) if len(parts) < 1 { - return errors.Wrap(ErrMalformedResponse, "invalid response") + return fmt.Errorf("invalid response: %w", ErrMalformedResponse) } const ( @@ -731,7 +729,7 @@ func parseMetaItemDebug(lines [][]byte, item *MetaItemDebug) error { case "ME": // success default: - return errors.Wrap(ErrMalformedResponse, "unexpected cd<="+string(cd)+">") + return fmt.Errorf("unexpected cd<=%s>: %w", string(cd), ErrMalformedResponse) } // parse key diff --git a/protocol_test.go b/protocol_test.go index 18781d4..e04d788 100644 --- a/protocol_test.go +++ b/protocol_test.go @@ -2,11 +2,11 @@ package memcached import ( "bytes" + "errors" "strconv" "testing" "time" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" memcodec "github.com/yeqown/memcached/codec" ) From 5e4e505cac74bf15b0fdc758f92472fbb666e45f Mon Sep 17 00:00:00 2001 From: EyJunge1 Date: Mon, 17 Aug 2026 12:21:40 +0200 Subject: [PATCH 2/5] chore: pin module language version to go 1.26 Use the language version instead of a patch pin so consumers are not forced onto a specific toolchain patch. --- go.mod | 2 +- go.work | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index e48cfb7..0136e75 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/yeqown/memcached -go 1.26.5 +go 1.26 require ( github.com/golang/snappy v1.0.0 diff --git a/go.work b/go.work index cc5a23f..51730c4 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,4 @@ -go 1.26.5 +go 1.26 use ( . From 8132a17cbef7c8f734bd6cdd2ea61a0f9cf0e079 Mon Sep 17 00:00:00 2001 From: EyJunge1 Date: Mon, 17 Aug 2026 12:42:21 +0200 Subject: [PATCH 3/5] chore: drop leftover pkg/errors require from go.mod Switch conn_test to stdlib errors.Is and remove the unused direct dependency. --- conn_test.go | 2 +- go.mod | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/conn_test.go b/conn_test.go index 8db1732..b89593a 100644 --- a/conn_test.go +++ b/conn_test.go @@ -2,11 +2,11 @@ package memcached import ( "context" + "errors" "sync" "testing" "time" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) diff --git a/go.mod b/go.mod index 0136e75..840192a 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( github.com/golang/snappy v1.0.0 github.com/klauspost/compress v1.19.2 github.com/pierrec/lz4/v4 v4.1.28 - github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.11.1 go.opentelemetry.io/otel v1.45.0 go.opentelemetry.io/otel/metric v1.45.0 From 45debf9dc57b5b631f8ae2c09a6f991d394f5bdd Mon Sep 17 00:00:00 2001 From: EyJunge1 Date: Wed, 19 Aug 2026 21:37:44 +0200 Subject: [PATCH 4/5] chore: align workspace and README Go versions Keep go.work at 1.26.0 so submodule language requirements are satisfied, and document Go 1.26+ in the README to match the root module. --- README.md | 2 +- go.work | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ba30e9c..c24f0d5 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ Now, we have implemented some commands, and we will implement more commands in t #### Prerequisites -- Go 1.26.5 or higher +- Go 1.26 or higher - Python (for pre-commit hooks) or just `brew install pre-commit` on MacOS - Docker (for running memcached in tests) diff --git a/go.work b/go.work index 51730c4..a4b482a 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,4 @@ -go 1.26 +go 1.26.0 use ( . From 257411653d48fcf1eee59bb9c0c82bd6f72d8c63 Mon Sep 17 00:00:00 2001 From: EyJunge1 Date: Mon, 17 Aug 2026 12:42:44 +0200 Subject: [PATCH 5/5] fix: harden connection pool shutdown and cleanup Keep the idle channel open across close and wake waiters via closedCh; drain connections created during shutdown. --- conn.go | 178 +++++++++++++++++++++++++++++++-------------------- conn_test.go | 157 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 258 insertions(+), 77 deletions(-) diff --git a/conn.go b/conn.go index 7b5c8e6..15e8cbc 100644 --- a/conn.go +++ b/conn.go @@ -244,6 +244,8 @@ func (c *conn) release() error { return c.pool.put(c) } +var errPoolClosed = errors.New("connection pool is closed") + // The connPool holds a pool of connections to one memcached server instance // and provides a way to get a connection from the pool. // @@ -268,7 +270,10 @@ type connPool struct { numOpen atomic.Int32 // Indicate if the pool is closed, if true, no new connections will be created // and all existing connections will be closed. - closed bool + closed bool + // closedCh is closed exactly once when the pool closes, so waiting get() + // callers can abort without receiving from a closed idle channel. + closedCh chan struct{} cleanerCh chan struct{} maxIdleClosed int64 // the number of connections closed due to maxIdle @@ -292,6 +297,7 @@ func newConnPool( createConn: createConn, numOpen: atomic.Int32{}, closed: false, + closedCh: make(chan struct{}), cleanerCh: nil, // created when needed in startCleaner maxIdleClosed: 0, @@ -310,11 +316,26 @@ func (p *connPool) close() error { } p.closed = true - // close all existing connections, so that the get() couldn't - // create a new connection. - close(p.conns) + close(p.closedCh) + + // Drain idle connections without closing the idle channel itself. + // Closing the channel would race with lock-free get() receives. + for { + select { + case cn := <-p.conns: + _ = cn.Close() + p.numOpen.Add(-1) + default: + goto drained + } + } +drained: + if p.cleanerCh != nil { - p.cleanerCh <- struct{}{} + select { + case p.cleanerCh <- struct{}{}: + default: + } } p.mu.Unlock() @@ -322,39 +343,48 @@ func (p *connPool) close() error { } func (p *connPool) get(ctx context.Context) (memcachedConn, error) { - if p.closed { - return nil, errors.New("connection pool is closed") - } - - // try to get a connection from the pool first if there is any + // Try to get an idle connection first if there is any, // otherwise create a new connection. select { case cn := <-p.conns: return cn, nil default: - p.mu.Lock() - // no available connection, check if we can create a new one. - if int(p.numOpen.Load()) >= p.maxConns { - p.mu.Unlock() - // the pool is full, wait for a connection to be returned - select { - case cn := <-p.conns: - return cn, nil - case <-ctx.Done(): - return nil, ctx.Err() - } - } - p.mu.Unlock() + } - cn, err := p.createConn(ctx) - if err != nil { - return nil, err + p.mu.Lock() + if p.closed { + p.mu.Unlock() + return nil, errPoolClosed + } + // No available connection, check if we can create a new one. + if int(p.numOpen.Load()) >= p.maxConns { + p.mu.Unlock() + // The pool is full, wait for a connection to be returned or for close. + select { + case cn := <-p.conns: + return cn, nil + case <-p.closedCh: + return nil, errPoolClosed + case <-ctx.Done(): + return nil, ctx.Err() } - cn.setConnPool(p) - p.numOpen.Add(1) + } + p.mu.Unlock() - return cn, nil + cn, err := p.createConn(ctx) + if err != nil { + return nil, err } + cn.setConnPool(p) + + p.mu.Lock() + if p.closed { + p.mu.Unlock() + return nil, errors.Join(errPoolClosed, cn.Close()) + } + p.numOpen.Add(1) + p.mu.Unlock() + return cn, nil } func (p *connPool) put(cn memcachedConn) error { @@ -408,7 +438,7 @@ func (p *connPool) connectionsCleaner(d time.Duration) { for { select { case <-t.C: - case <-p.cleanerCh: // db was closed + case <-p.cleanerCh: // pool was closed } p.mu.Lock() @@ -423,7 +453,7 @@ func (p *connPool) connectionsCleaner(d time.Duration) { return } - // make a copy of the connections those need to be closed. + // Snapshot idle connections that need to be closed. d, closing := p.connectionCleanerRunLocked(d) p.mu.Unlock() @@ -461,55 +491,65 @@ func (p *connPool) shortestIdleTimeLocked() time.Duration { // // 1. if the connection is expired (exceeds maxLifeTime since created). // 2. if the connection idle time exceeds the idle connection limit(maxIdleTime). +// +// The idle channel is kept intact: currently available connections are drained +// with non-blocking receives, filtered in one pass, and survivors are put back. func (p *connPool) connectionCleanerRunLocked(d time.Duration) (time.Duration, []memcachedConn) { var idleClosing int64 closing := make([]memcachedConn, 0, p.maxIdle/2) - newConns := make(chan memcachedConn, p.maxConns) - - if p.maxIdleTime > 0 { - idleSince := nowFunc().Add(-p.maxIdleTime) - close(p.conns) - // TODO(@yeqown): could we optimize the loop here? - // we re-allocate the newConns channel every time we clean up the connections. - for c := range p.conns { - if d2, ok := c.idle(idleSince); !ok { - if d2 < d { - // Ensure idle connections are cleaned up as soon - // as possible. - d = d2 - } - newConns <- c // put back - continue - } - closing = append(closing, c) - idleClosing++ + kept := make([]memcachedConn, 0, len(p.conns)) + + var idleSince, expiredSince time.Time + checkIdle := p.maxIdleTime > 0 + checkLife := p.maxLifeTime > 0 + if checkIdle { + idleSince = nowFunc().Add(-p.maxIdleTime) + } + if checkLife { + expiredSince = nowFunc().Add(-p.maxLifeTime) + } + + for { + var c memcachedConn + select { + case c = <-p.conns: + default: + goto drained } - p.conns = newConns - p.maxIdleTimeClosed += idleClosing - } - - newConns = make(chan memcachedConn, p.maxConns) - if p.maxLifeTime > 0 { - expiredSince := nowFunc().Add(-p.maxLifeTime) - close(p.conns) - for c := range p.conns { - if d2, ok := c.expired(expiredSince); !ok { - if d2 < d { - // Prevents connections staying in the pool when they - // have expired. - d = d2 - } - newConns <- c // put back - continue + shouldClose := false + if checkIdle { + if d2, ok := c.idle(idleSince); ok { + shouldClose = true + idleClosing++ + } else if d2 < d { + // Ensure idle connections are cleaned up as soon as possible. + d = d2 + } + } + if !shouldClose && checkLife { + if d2, ok := c.expired(expiredSince); ok { + shouldClose = true + } else if d2 < d { + // Prevents connections staying in the pool when they have expired. + d = d2 } + } + + if shouldClose { closing = append(closing, c) + } else { + kept = append(kept, c) } + } - p.conns = newConns - p.maxLifeTimeClosed += int64(len(closing)) - idleClosing +drained: + for _, c := range kept { + p.conns <- c } + p.maxIdleTimeClosed += idleClosing + p.maxLifeTimeClosed += int64(len(closing)) - idleClosing return d, closing } diff --git a/conn_test.go b/conn_test.go index b89593a..823b83f 100644 --- a/conn_test.go +++ b/conn_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "sync" + "sync/atomic" "testing" "time" @@ -21,6 +22,16 @@ type mockConn struct { pool *connPool } +type closeTrackingConn struct { + *mockConn + closed atomic.Bool +} + +func (m *closeTrackingConn) Close() error { + m.closed.Store(true) + return nil +} + func newMockConn() *mockConn { return &mockConn{ createdAt: time.Now(), @@ -131,14 +142,14 @@ func Test_connPool_new(t *testing.T) { t.Run(tt.name, func(t *testing.T) { pool := newConnPool(tt.maxIdle, tt.maxConn, tt.maxLifeTime, tt.maxIdleTimeout, createConn) - // 验证基本属性 assert.NotNil(t, pool) assert.Equal(t, tt.maxIdle, pool.maxIdle) assert.Equal(t, tt.maxConn, pool.maxConns) assert.Equal(t, tt.maxIdleTimeout, pool.maxIdleTime) - assert.Equal(t, tt.maxConn, cap(pool.conns)) + assert.Equal(t, 0, pool.stats().IdleConns) assert.Equal(t, 0, int(pool.numOpen.Load())) assert.False(t, pool.closed) + assert.NotNil(t, pool.closedCh) assert.Nil(t, pool.cleanerCh) assert.Equal(t, int64(0), pool.maxIdleClosed) assert.Equal(t, int64(0), pool.maxIdleTimeClosed) @@ -172,9 +183,9 @@ func Test_connPool_get_put(t *testing.T) { wg.Wait() - // idle connections reached maxConn + // idle connections reached maxIdle assert.Equal(t, 5, int(pool.numOpen.Load())) - assert.Equal(t, 5, len(pool.conns)) + assert.Equal(t, 5, pool.stats().IdleConns) } // Test_connPool_get_timeout_case1 mocking the case that the createConn function @@ -271,7 +282,7 @@ func Test_connPool_get_oversize(t *testing.T) { wg.Wait() assert.Equal(t, 5, int(pool.numOpen.Load())) - assert.Equal(t, 5, len(pool.conns)) + assert.Equal(t, 5, pool.stats().IdleConns) ctx, cancel = context.WithTimeout(context.Background(), 10*time.Millisecond) defer cancel() @@ -317,7 +328,7 @@ func Test_connPool_cleanup_maxIdle(t *testing.T) { // connections to 5. time.Sleep(3 * time.Second) assert.Equal(t, 0, int(pool.numOpen.Load())) - assert.Equal(t, 0, len(pool.conns)) + assert.Equal(t, 0, pool.stats().IdleConns) assert.False(t, pool.closed) stat = pool.stats() @@ -368,16 +379,146 @@ func Test_connPool_cleanup_maxLife(t *testing.T) { // connections to 5. time.Sleep(3 * time.Second) assert.Equal(t, 0, int(pool.numOpen.Load())) - assert.Equal(t, 0, len(pool.conns)) + assert.Equal(t, 0, pool.stats().IdleConns) assert.False(t, pool.closed) stat = pool.stats() t.Logf("After cleanup: %+v", stat) assert.Equal(t, 0, stat.TotalConns) - assert.Equal(t, 0, stat.IdleConns) // since all connections are cleaned up(idle=1s) + assert.Equal(t, 0, stat.IdleConns) // since all connections are cleaned up(life=2s) assert.Equal(t, 10, stat.MaxConns) assert.Equal(t, 5, stat.MaxIdle) assert.Equal(t, int64(5), stat.maxIdleClosed) assert.Equal(t, int64(5), stat.maxLifeTimeClosed) assert.Equal(t, int64(0), stat.maxIdleTimeClosed) } + +func Test_connectionCleanerRunLocked_both_limits(t *testing.T) { + pool := newConnPool(10, 10, time.Hour, time.Hour, createConn) + fixedNow := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) + origNow := nowFunc + nowFunc = func() time.Time { return fixedNow } + t.Cleanup(func() { nowFunc = origNow }) + + idleExpired := newMockConn() + idleExpired.createdAt = fixedNow.Add(-30 * time.Minute) + idleExpired.returnedAt = fixedNow.Add(-2 * time.Hour) + + lifeExpired := newMockConn() + lifeExpired.createdAt = fixedNow.Add(-2 * time.Hour) + lifeExpired.returnedAt = fixedNow.Add(-time.Minute) + + fresh := newMockConn() + fresh.createdAt = fixedNow.Add(-time.Minute) + fresh.returnedAt = fixedNow.Add(-time.Minute) + + pool.conns <- idleExpired + pool.conns <- lifeExpired + pool.conns <- fresh + pool.numOpen.Store(3) + connsBefore := pool.conns + + pool.maxIdleTime = time.Hour + pool.maxLifeTime = time.Hour + + d, closing := pool.connectionCleanerRunLocked(time.Hour) + assert.True(t, connsBefore == pool.conns, "cleaner must keep the idle channel") + assert.Len(t, closing, 2) + assert.Equal(t, int64(1), pool.maxIdleTimeClosed) + assert.Equal(t, int64(1), pool.maxLifeTimeClosed) + assert.Equal(t, 1, len(pool.conns)) + assert.Greater(t, d, time.Duration(0)) + + kept := <-pool.conns + assert.Same(t, fresh, kept) +} + +func Test_connPool_cleanup_concurrent_get(t *testing.T) { + pool := newConnPool(8, 8, time.Hour, 100*time.Millisecond, createConn) + ctx := context.Background() + + for i := 0; i < 8; i++ { + cn, err := pool.get(ctx) + assert.NoError(t, err) + assert.NoError(t, pool.put(cn)) + } + + done := make(chan struct{}) + go func() { + defer close(done) + deadline := time.Now().Add(2 * time.Second) + for time.Now().Before(deadline) { + cn, err := pool.get(ctx) + if err != nil { + assert.Fail(t, "get failed during cleanup", err.Error()) + return + } + assert.NotNil(t, cn) + _ = pool.put(cn) + } + }() + + <-done + assert.NoError(t, pool.close()) + assert.Equal(t, 0, int(pool.numOpen.Load())) +} + +func Test_connPool_close_wakes_waiters(t *testing.T) { + pool := newConnPool(1, 1, time.Hour, time.Hour, createConn) + ctx := context.Background() + + cn, err := pool.get(ctx) + assert.NoError(t, err) + assert.NotNil(t, cn) + + errCh := make(chan error, 1) + go func() { + _, getErr := pool.get(ctx) + errCh <- getErr + }() + + time.Sleep(50 * time.Millisecond) + assert.NoError(t, pool.close()) + + select { + case getErr := <-errCh: + assert.ErrorIs(t, getErr, errPoolClosed) + case <-time.After(time.Second): + t.Fatal("waiting get was not woken by close") + } + + assert.NoError(t, pool.put(cn)) + assert.Equal(t, 0, int(pool.numOpen.Load())) + assert.Equal(t, 0, pool.stats().IdleConns) +} + +func Test_connPool_get_closes_connection_created_during_close(t *testing.T) { + creationStarted := make(chan struct{}) + releaseCreation := make(chan struct{}) + createdConn := &closeTrackingConn{mockConn: newMockConn()} + pool := newConnPool(1, 1, time.Hour, time.Hour, func(context.Context) (memcachedConn, error) { + close(creationStarted) + <-releaseCreation + return createdConn, nil + }) + + type getResult struct { + conn memcachedConn + err error + } + resultCh := make(chan getResult, 1) + go func() { + conn, err := pool.get(context.Background()) + resultCh <- getResult{conn: conn, err: err} + }() + + <-creationStarted + assert.NoError(t, pool.close()) + close(releaseCreation) + + result := <-resultCh + assert.Nil(t, result.conn) + assert.ErrorIs(t, result.err, errPoolClosed) + assert.True(t, createdConn.closed.Load()) + assert.Equal(t, 0, int(pool.numOpen.Load())) +}