├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── elasticache
├── elasticache.go
└── elasticache_test.go
├── glide.lock
├── glide.yaml
└── vendor
└── github.com
├── bradfitz
└── gomemcache
│ ├── .gitignore
│ ├── LICENSE
│ ├── README.md
│ └── memcache
│ ├── memcache.go
│ ├── memcache_test.go
│ ├── selector.go
│ └── selector_test.go
└── integralist
└── go-findroot
├── LICENSE
├── README.md
└── find
├── find.go
└── find_test.go
/.gitignore:
--------------------------------------------------------------------------------
1 | go-elasticache.log
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Mark McDonnell
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: tests
2 |
3 | tests:
4 | APP_ENV=test go test -v $$(glide novendor)
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
go-elasticache
2 |
3 |
4 |
5 |
6 |
7 |
8 | Thin abstraction over the Memcache client package gomemcache
9 | allowing it to support AWS ElastiCache cluster nodes
10 |
11 |
12 | ## Explanation
13 |
14 | When using the memcache client [gomemcache](https://github.com/bradfitz/gomemcache) you typically call a constructor and pass it a list of memcache nodes like so:
15 |
16 | ```go
17 | mc := memcache.New("10.0.0.1:11211", "10.0.0.2:11211", "10.0.0.3:11212")
18 | ```
19 |
20 | But when using the [AWS ElastiCache](https://aws.amazon.com/elasticache/) service you need to query a particular internal endpoint via a socket connection and manually parse out the details of the available cluster.
21 |
22 | In Ruby it seems most Memcache clients are setup to handle this for you, but in Go that doesn't appear to be the case. Hence I've created this package as a thin abstraction layer on top of the gomemcache package.
23 |
24 | ## Example
25 |
26 | Below is an example of how to use this package.
27 |
28 | To run it locally you will need the following dependencies installed and running:
29 |
30 | - Memcache (e.g. `docker run -d -p 11211:11211 memcached`)
31 | - [fake_elasticache](https://github.com/stevenjack/fake_elasticache) (e.g. `gem install fake_elasticache && fake_elasticache`)
32 |
33 | ```go
34 | package main
35 |
36 | import (
37 | "fmt"
38 | "log"
39 |
40 | "github.com/integralist/go-elasticache/elasticache"
41 | )
42 |
43 | func main() {
44 | mc, err := elasticache.New()
45 | if err != nil {
46 | log.Fatalf("Error: %s", err.Error())
47 | }
48 |
49 | if err := mc.Set(&elasticache.Item{Key: "foo", Value: []byte("my value")}); err != nil {
50 | log.Println(err.Error())
51 | }
52 |
53 | it, err := mc.Get("foo")
54 | if err != nil {
55 | log.Println(err.Error())
56 | return
57 | }
58 |
59 | fmt.Printf("%+v", it)
60 | // &{Key:foo Value:[109 121 32 118 97 108 117 101] Flags:0 Expiration:0 casid:9}
61 | }
62 | ```
63 |
64 | > Note: when running in production make sure to set the environment variable `ELASTICACHE_ENDPOINT`
65 |
66 | ## Licence
67 |
68 | [The MIT License (MIT)](http://opensource.org/licenses/MIT)
69 |
70 | Copyright (c) 2016 [Mark McDonnell](http://twitter.com/integralist)
71 |
--------------------------------------------------------------------------------
/elasticache/elasticache.go:
--------------------------------------------------------------------------------
1 | package elasticache
2 |
3 | import (
4 | "bufio"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "log"
9 | "net"
10 | "os"
11 | "strconv"
12 | "strings"
13 |
14 | "github.com/bradfitz/gomemcache/memcache"
15 | "github.com/integralist/go-findroot/find"
16 | )
17 |
18 | // Node is a single ElastiCache node
19 | type Node struct {
20 | URL string
21 | Host string
22 | IP string
23 | Port int
24 | }
25 |
26 | // Item embeds the memcache client's type of the same name
27 | type Item memcache.Item
28 |
29 | // Client embeds the memcache client so we can hide those details away
30 | type Client struct {
31 | *memcache.Client
32 | }
33 |
34 | // Set abstracts the memcache client details away,
35 | // by copying over the values provided by the user into the Set method,
36 | // as coercing the custom Item type to the required memcache.Item type isn't possible.
37 | // Downside is if memcache client fields ever change, it'll introduce a break
38 | func (c *Client) Set(item *Item) error {
39 | return c.Client.Set(&memcache.Item{
40 | Key: item.Key,
41 | Value: item.Value,
42 | Expiration: item.Expiration,
43 | })
44 | }
45 |
46 | var logger *log.Logger
47 |
48 | func init() {
49 | logger = log.New(os.Stdout, "go-elasticache: ", log.Ldate|log.Ltime|log.Lshortfile)
50 |
51 | if env := os.Getenv("APP_ENV"); env == "test" {
52 | root, err := find.Repo()
53 | if err != nil {
54 | log.Printf("Repo Error: %s", err.Error())
55 | }
56 |
57 | path := fmt.Sprintf("%s/go-elasticache.log", root.Path)
58 |
59 | file, err := os.OpenFile(path, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
60 | if err != nil {
61 | log.Printf("Open File Error: %s", err.Error())
62 | }
63 |
64 | logger = log.New(file, "go-elasticache: ", log.Ldate|log.Ltime|log.Lshortfile)
65 | }
66 | }
67 |
68 | // New returns an instance of the memcache client
69 | func New() (*Client, error) {
70 | urls, err := clusterNodes()
71 | if err != nil {
72 | return &Client{Client: memcache.New()}, err
73 | }
74 |
75 | return &Client{Client: memcache.New(urls...)}, nil
76 | }
77 |
78 | func clusterNodes() ([]string, error) {
79 | endpoint, err := elasticache()
80 | if err != nil {
81 | return nil, err
82 | }
83 |
84 | conn, err := net.Dial("tcp", endpoint)
85 | if err != nil {
86 | logger.Printf("Socket Dial (%s): %s", endpoint, err.Error())
87 | return nil, err
88 | }
89 | defer conn.Close()
90 |
91 | command := "config get cluster\r\n"
92 | fmt.Fprintf(conn, command)
93 |
94 | response, err := parseNodes(conn)
95 | if err != nil {
96 | return nil, err
97 | }
98 |
99 | urls, err := parseURLs(response)
100 | if err != nil {
101 | return nil, err
102 | }
103 |
104 | return urls, nil
105 | }
106 |
107 | func elasticache() (string, error) {
108 | var endpoint string
109 |
110 | endpoint = os.Getenv("ELASTICACHE_ENDPOINT")
111 | if len(endpoint) == 0 {
112 | logger.Println("ElastiCache endpoint not set")
113 | return "", errors.New("ElastiCache endpoint not set")
114 | }
115 |
116 | return endpoint, nil
117 | }
118 |
119 | func parseNodes(conn io.Reader) (string, error) {
120 | var response string
121 |
122 | count := 0
123 | location := 3 // AWS docs suggest that nodes will always be listed on line 3
124 |
125 | scanner := bufio.NewScanner(conn)
126 | for scanner.Scan() {
127 | count++
128 | if count == location {
129 | response = scanner.Text()
130 | }
131 | if scanner.Text() == "END" {
132 | break
133 | }
134 | }
135 |
136 | if err := scanner.Err(); err != nil {
137 | logger.Println("Scanner: ", err.Error())
138 | return "", err
139 | }
140 |
141 | logger.Println("ElastiCache nodes found: ", response)
142 | return response, nil
143 | }
144 |
145 | func parseURLs(response string) ([]string, error) {
146 | var urls []string
147 | var nodes []Node
148 |
149 | items := strings.Split(response, " ")
150 |
151 | for _, v := range items {
152 | fields := strings.Split(v, "|") // ["host", "ip", "port"]
153 |
154 | port, err := strconv.Atoi(fields[2])
155 | if err != nil {
156 | logger.Println("Integer conversion: ", err.Error())
157 | return nil, err
158 | }
159 |
160 | node := Node{fmt.Sprintf("%s:%d", fields[1], port), fields[0], fields[1], port}
161 | nodes = append(nodes, node)
162 | urls = append(urls, node.URL)
163 |
164 | logger.Printf("Host: %s, IP: %s, Port: %d, URL: %s", node.Host, node.IP, node.Port, node.URL)
165 | }
166 |
167 | return urls, nil
168 | }
169 |
--------------------------------------------------------------------------------
/elasticache/elasticache_test.go:
--------------------------------------------------------------------------------
1 | package elasticache
2 |
3 | import (
4 | "bytes"
5 | "os"
6 | "testing"
7 | )
8 |
9 | func TestElastiCacheEndpoint(t *testing.T) {
10 | expectation := "foo"
11 | os.Setenv("ELASTICACHE_ENDPOINT", expectation)
12 | response, _ := elasticache()
13 |
14 | if response != expectation {
15 | t.Errorf("The response '%s' didn't match the expectation '%s'", response, expectation)
16 | }
17 | }
18 |
19 | func TestParseNodes(t *testing.T) {
20 | expectation := "localhost|127.0.0.1|11211"
21 | cluster := `CONFIG cluster 0 25
22 | 1
23 | localhost|127.0.0.1|11211
24 |
25 | END`
26 |
27 | r := bytes.NewReader([]byte(cluster))
28 |
29 | response, _ := parseNodes(r)
30 |
31 | if response != expectation {
32 | t.Errorf("The response '%s' didn't match the expectation '%s'", response, expectation)
33 | }
34 | }
35 |
36 | func TestParseURLs(t *testing.T) {
37 | expectationLength := 3
38 |
39 | response, _ := parseURLs("host|foo|1 host|bar|2 host|baz|3")
40 |
41 | if len(response) != expectationLength {
42 | t.Errorf("The response length '%d' didn't match the expectation '%d'", len(response), expectationLength)
43 | }
44 |
45 | var suite = []struct {
46 | response string
47 | expectation string
48 | }{
49 | {response[0], "foo:1"},
50 | {response[1], "bar:2"},
51 | {response[2], "baz:3"},
52 | }
53 |
54 | for _, v := range suite {
55 | if v.response != v.expectation {
56 | t.Errorf("The response '%s' didn't match the expectation '%s'", v.response, v.expectation)
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/glide.lock:
--------------------------------------------------------------------------------
1 | hash: 61f0d96c966c7a1f175801e09ae467b4d9d12a4ae0b1a090853395b2e0b62fa3
2 | updated: 2018-01-22T15:47:11.740223268-08:00
3 | imports:
4 | - name: github.com/bradfitz/gomemcache
5 | version: 1952afaa557dc08e8e0d89eafab110fb501c1a2b
6 | subpackages:
7 | - memcache
8 | - name: github.com/integralist/go-findroot
9 | version: ac90681525dc30c2163cc606675922b7fdb9c041
10 | subpackages:
11 | - find
12 | testImports: []
13 |
--------------------------------------------------------------------------------
/glide.yaml:
--------------------------------------------------------------------------------
1 | package: github.com/integralist/go-elasticache
2 | import:
3 | - package: github.com/bradfitz/gomemcache
4 | - package: github.com/integralist/go-findroot
5 |
--------------------------------------------------------------------------------
/vendor/github.com/bradfitz/gomemcache/.gitignore:
--------------------------------------------------------------------------------
1 | _*
2 | *.out
3 | *~
4 |
--------------------------------------------------------------------------------
/vendor/github.com/bradfitz/gomemcache/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/vendor/github.com/bradfitz/gomemcache/README.md:
--------------------------------------------------------------------------------
1 | ## About
2 |
3 | This is a memcache client library for the Go programming language
4 | (http://golang.org/).
5 |
6 | ## Installing
7 |
8 | ### Using *go get*
9 |
10 | $ go get github.com/bradfitz/gomemcache/memcache
11 |
12 | After this command *gomemcache* is ready to use. Its source will be in:
13 |
14 | $GOPATH/src/github.com/bradfitz/gomemcache/memcache
15 |
16 | ## Example
17 |
18 | import (
19 | "github.com/bradfitz/gomemcache/memcache"
20 | )
21 |
22 | func main() {
23 | mc := memcache.New("10.0.0.1:11211", "10.0.0.2:11211", "10.0.0.3:11212")
24 | mc.Set(&memcache.Item{Key: "foo", Value: []byte("my value")})
25 |
26 | it, err := mc.Get("foo")
27 | ...
28 | }
29 |
30 | ## Full docs, see:
31 |
32 | See https://godoc.org/github.com/bradfitz/gomemcache/memcache
33 |
34 | Or run:
35 |
36 | $ godoc github.com/bradfitz/gomemcache/memcache
37 |
38 |
--------------------------------------------------------------------------------
/vendor/github.com/bradfitz/gomemcache/memcache/memcache.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2011 Google Inc.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | // Package memcache provides a client for the memcached cache server.
18 | package memcache
19 |
20 | import (
21 | "bufio"
22 | "bytes"
23 | "errors"
24 | "fmt"
25 | "io"
26 | "io/ioutil"
27 | "net"
28 |
29 | "strconv"
30 | "strings"
31 | "sync"
32 | "time"
33 | )
34 |
35 | // Similar to:
36 | // http://code.google.com/appengine/docs/go/memcache/reference.html
37 |
38 | var (
39 | // ErrCacheMiss means that a Get failed because the item wasn't present.
40 | ErrCacheMiss = errors.New("memcache: cache miss")
41 |
42 | // ErrCASConflict means that a CompareAndSwap call failed due to the
43 | // cached value being modified between the Get and the CompareAndSwap.
44 | // If the cached value was simply evicted rather than replaced,
45 | // ErrNotStored will be returned instead.
46 | ErrCASConflict = errors.New("memcache: compare-and-swap conflict")
47 |
48 | // ErrNotStored means that a conditional write operation (i.e. Add or
49 | // CompareAndSwap) failed because the condition was not satisfied.
50 | ErrNotStored = errors.New("memcache: item not stored")
51 |
52 | // ErrServer means that a server error occurred.
53 | ErrServerError = errors.New("memcache: server error")
54 |
55 | // ErrNoStats means that no statistics were available.
56 | ErrNoStats = errors.New("memcache: no statistics available")
57 |
58 | // ErrMalformedKey is returned when an invalid key is used.
59 | // Keys must be at maximum 250 bytes long and not
60 | // contain whitespace or control characters.
61 | ErrMalformedKey = errors.New("malformed: key is too long or contains invalid characters")
62 |
63 | // ErrNoServers is returned when no servers are configured or available.
64 | ErrNoServers = errors.New("memcache: no servers configured or available")
65 | )
66 |
67 | const (
68 | // DefaultTimeout is the default socket read/write timeout.
69 | DefaultTimeout = 100 * time.Millisecond
70 |
71 | // DefaultMaxIdleConns is the default maximum number of idle connections
72 | // kept for any single address.
73 | DefaultMaxIdleConns = 2
74 | )
75 |
76 | const buffered = 8 // arbitrary buffered channel size, for readability
77 |
78 | // resumableError returns true if err is only a protocol-level cache error.
79 | // This is used to determine whether or not a server connection should
80 | // be re-used or not. If an error occurs, by default we don't reuse the
81 | // connection, unless it was just a cache error.
82 | func resumableError(err error) bool {
83 | switch err {
84 | case ErrCacheMiss, ErrCASConflict, ErrNotStored, ErrMalformedKey:
85 | return true
86 | }
87 | return false
88 | }
89 |
90 | func legalKey(key string) bool {
91 | if len(key) > 250 {
92 | return false
93 | }
94 | for i := 0; i < len(key); i++ {
95 | if key[i] <= ' ' || key[i] == 0x7f {
96 | return false
97 | }
98 | }
99 | return true
100 | }
101 |
102 | var (
103 | crlf = []byte("\r\n")
104 | space = []byte(" ")
105 | resultOK = []byte("OK\r\n")
106 | resultStored = []byte("STORED\r\n")
107 | resultNotStored = []byte("NOT_STORED\r\n")
108 | resultExists = []byte("EXISTS\r\n")
109 | resultNotFound = []byte("NOT_FOUND\r\n")
110 | resultDeleted = []byte("DELETED\r\n")
111 | resultEnd = []byte("END\r\n")
112 | resultOk = []byte("OK\r\n")
113 | resultTouched = []byte("TOUCHED\r\n")
114 |
115 | resultClientErrorPrefix = []byte("CLIENT_ERROR ")
116 | )
117 |
118 | // New returns a memcache client using the provided server(s)
119 | // with equal weight. If a server is listed multiple times,
120 | // it gets a proportional amount of weight.
121 | func New(server ...string) *Client {
122 | ss := new(ServerList)
123 | ss.SetServers(server...)
124 | return NewFromSelector(ss)
125 | }
126 |
127 | // NewFromSelector returns a new Client using the provided ServerSelector.
128 | func NewFromSelector(ss ServerSelector) *Client {
129 | return &Client{selector: ss}
130 | }
131 |
132 | // Client is a memcache client.
133 | // It is safe for unlocked use by multiple concurrent goroutines.
134 | type Client struct {
135 | // Timeout specifies the socket read/write timeout.
136 | // If zero, DefaultTimeout is used.
137 | Timeout time.Duration
138 |
139 | // MaxIdleConns specifies the maximum number of idle connections that will
140 | // be maintained per address. If less than one, DefaultMaxIdleConns will be
141 | // used.
142 | //
143 | // Consider your expected traffic rates and latency carefully. This should
144 | // be set to a number higher than your peak parallel requests.
145 | MaxIdleConns int
146 |
147 | selector ServerSelector
148 |
149 | lk sync.Mutex
150 | freeconn map[string][]*conn
151 | }
152 |
153 | // Item is an item to be got or stored in a memcached server.
154 | type Item struct {
155 | // Key is the Item's key (250 bytes maximum).
156 | Key string
157 |
158 | // Value is the Item's value.
159 | Value []byte
160 |
161 | // Flags are server-opaque flags whose semantics are entirely
162 | // up to the app.
163 | Flags uint32
164 |
165 | // Expiration is the cache expiration time, in seconds: either a relative
166 | // time from now (up to 1 month), or an absolute Unix epoch time.
167 | // Zero means the Item has no expiration time.
168 | Expiration int32
169 |
170 | // Compare and swap ID.
171 | casid uint64
172 | }
173 |
174 | // conn is a connection to a server.
175 | type conn struct {
176 | nc net.Conn
177 | rw *bufio.ReadWriter
178 | addr net.Addr
179 | c *Client
180 | }
181 |
182 | // release returns this connection back to the client's free pool
183 | func (cn *conn) release() {
184 | cn.c.putFreeConn(cn.addr, cn)
185 | }
186 |
187 | func (cn *conn) extendDeadline() {
188 | cn.nc.SetDeadline(time.Now().Add(cn.c.netTimeout()))
189 | }
190 |
191 | // condRelease releases this connection if the error pointed to by err
192 | // is nil (not an error) or is only a protocol level error (e.g. a
193 | // cache miss). The purpose is to not recycle TCP connections that
194 | // are bad.
195 | func (cn *conn) condRelease(err *error) {
196 | if *err == nil || resumableError(*err) {
197 | cn.release()
198 | } else {
199 | cn.nc.Close()
200 | }
201 | }
202 |
203 | func (c *Client) putFreeConn(addr net.Addr, cn *conn) {
204 | c.lk.Lock()
205 | defer c.lk.Unlock()
206 | if c.freeconn == nil {
207 | c.freeconn = make(map[string][]*conn)
208 | }
209 | freelist := c.freeconn[addr.String()]
210 | if len(freelist) >= c.maxIdleConns() {
211 | cn.nc.Close()
212 | return
213 | }
214 | c.freeconn[addr.String()] = append(freelist, cn)
215 | }
216 |
217 | func (c *Client) getFreeConn(addr net.Addr) (cn *conn, ok bool) {
218 | c.lk.Lock()
219 | defer c.lk.Unlock()
220 | if c.freeconn == nil {
221 | return nil, false
222 | }
223 | freelist, ok := c.freeconn[addr.String()]
224 | if !ok || len(freelist) == 0 {
225 | return nil, false
226 | }
227 | cn = freelist[len(freelist)-1]
228 | c.freeconn[addr.String()] = freelist[:len(freelist)-1]
229 | return cn, true
230 | }
231 |
232 | func (c *Client) netTimeout() time.Duration {
233 | if c.Timeout != 0 {
234 | return c.Timeout
235 | }
236 | return DefaultTimeout
237 | }
238 |
239 | func (c *Client) maxIdleConns() int {
240 | if c.MaxIdleConns > 0 {
241 | return c.MaxIdleConns
242 | }
243 | return DefaultMaxIdleConns
244 | }
245 |
246 | // ConnectTimeoutError is the error type used when it takes
247 | // too long to connect to the desired host. This level of
248 | // detail can generally be ignored.
249 | type ConnectTimeoutError struct {
250 | Addr net.Addr
251 | }
252 |
253 | func (cte *ConnectTimeoutError) Error() string {
254 | return "memcache: connect timeout to " + cte.Addr.String()
255 | }
256 |
257 | func (c *Client) dial(addr net.Addr) (net.Conn, error) {
258 | type connError struct {
259 | cn net.Conn
260 | err error
261 | }
262 |
263 | nc, err := net.DialTimeout(addr.Network(), addr.String(), c.netTimeout())
264 | if err == nil {
265 | return nc, nil
266 | }
267 |
268 | if ne, ok := err.(net.Error); ok && ne.Timeout() {
269 | return nil, &ConnectTimeoutError{addr}
270 | }
271 |
272 | return nil, err
273 | }
274 |
275 | func (c *Client) getConn(addr net.Addr) (*conn, error) {
276 | cn, ok := c.getFreeConn(addr)
277 | if ok {
278 | cn.extendDeadline()
279 | return cn, nil
280 | }
281 | nc, err := c.dial(addr)
282 | if err != nil {
283 | return nil, err
284 | }
285 | cn = &conn{
286 | nc: nc,
287 | addr: addr,
288 | rw: bufio.NewReadWriter(bufio.NewReader(nc), bufio.NewWriter(nc)),
289 | c: c,
290 | }
291 | cn.extendDeadline()
292 | return cn, nil
293 | }
294 |
295 | func (c *Client) onItem(item *Item, fn func(*Client, *bufio.ReadWriter, *Item) error) error {
296 | addr, err := c.selector.PickServer(item.Key)
297 | if err != nil {
298 | return err
299 | }
300 | cn, err := c.getConn(addr)
301 | if err != nil {
302 | return err
303 | }
304 | defer cn.condRelease(&err)
305 | if err = fn(c, cn.rw, item); err != nil {
306 | return err
307 | }
308 | return nil
309 | }
310 |
311 | func (c *Client) FlushAll() error {
312 | return c.selector.Each(c.flushAllFromAddr)
313 | }
314 |
315 | // Get gets the item for the given key. ErrCacheMiss is returned for a
316 | // memcache cache miss. The key must be at most 250 bytes in length.
317 | func (c *Client) Get(key string) (item *Item, err error) {
318 | err = c.withKeyAddr(key, func(addr net.Addr) error {
319 | return c.getFromAddr(addr, []string{key}, func(it *Item) { item = it })
320 | })
321 | if err == nil && item == nil {
322 | err = ErrCacheMiss
323 | }
324 | return
325 | }
326 |
327 | // Touch updates the expiry for the given key. The seconds parameter is either
328 | // a Unix timestamp or, if seconds is less than 1 month, the number of seconds
329 | // into the future at which time the item will expire. ErrCacheMiss is returned if the
330 | // key is not in the cache. The key must be at most 250 bytes in length.
331 | func (c *Client) Touch(key string, seconds int32) (err error) {
332 | return c.withKeyAddr(key, func(addr net.Addr) error {
333 | return c.touchFromAddr(addr, []string{key}, seconds)
334 | })
335 | }
336 |
337 | func (c *Client) withKeyAddr(key string, fn func(net.Addr) error) (err error) {
338 | if !legalKey(key) {
339 | return ErrMalformedKey
340 | }
341 | addr, err := c.selector.PickServer(key)
342 | if err != nil {
343 | return err
344 | }
345 | return fn(addr)
346 | }
347 |
348 | func (c *Client) withAddrRw(addr net.Addr, fn func(*bufio.ReadWriter) error) (err error) {
349 | cn, err := c.getConn(addr)
350 | if err != nil {
351 | return err
352 | }
353 | defer cn.condRelease(&err)
354 | return fn(cn.rw)
355 | }
356 |
357 | func (c *Client) withKeyRw(key string, fn func(*bufio.ReadWriter) error) error {
358 | return c.withKeyAddr(key, func(addr net.Addr) error {
359 | return c.withAddrRw(addr, fn)
360 | })
361 | }
362 |
363 | func (c *Client) getFromAddr(addr net.Addr, keys []string, cb func(*Item)) error {
364 | return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error {
365 | if _, err := fmt.Fprintf(rw, "gets %s\r\n", strings.Join(keys, " ")); err != nil {
366 | return err
367 | }
368 | if err := rw.Flush(); err != nil {
369 | return err
370 | }
371 | if err := parseGetResponse(rw.Reader, cb); err != nil {
372 | return err
373 | }
374 | return nil
375 | })
376 | }
377 |
378 | // flushAllFromAddr send the flush_all command to the given addr
379 | func (c *Client) flushAllFromAddr(addr net.Addr) error {
380 | return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error {
381 | if _, err := fmt.Fprintf(rw, "flush_all\r\n"); err != nil {
382 | return err
383 | }
384 | if err := rw.Flush(); err != nil {
385 | return err
386 | }
387 | line, err := rw.ReadSlice('\n')
388 | if err != nil {
389 | return err
390 | }
391 | switch {
392 | case bytes.Equal(line, resultOk):
393 | break
394 | default:
395 | return fmt.Errorf("memcache: unexpected response line from flush_all: %q", string(line))
396 | }
397 | return nil
398 | })
399 | }
400 |
401 | func (c *Client) touchFromAddr(addr net.Addr, keys []string, expiration int32) error {
402 | return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error {
403 | for _, key := range keys {
404 | if _, err := fmt.Fprintf(rw, "touch %s %d\r\n", key, expiration); err != nil {
405 | return err
406 | }
407 | if err := rw.Flush(); err != nil {
408 | return err
409 | }
410 | line, err := rw.ReadSlice('\n')
411 | if err != nil {
412 | return err
413 | }
414 | switch {
415 | case bytes.Equal(line, resultTouched):
416 | break
417 | case bytes.Equal(line, resultNotFound):
418 | return ErrCacheMiss
419 | default:
420 | return fmt.Errorf("memcache: unexpected response line from touch: %q", string(line))
421 | }
422 | }
423 | return nil
424 | })
425 | }
426 |
427 | // GetMulti is a batch version of Get. The returned map from keys to
428 | // items may have fewer elements than the input slice, due to memcache
429 | // cache misses. Each key must be at most 250 bytes in length.
430 | // If no error is returned, the returned map will also be non-nil.
431 | func (c *Client) GetMulti(keys []string) (map[string]*Item, error) {
432 | var lk sync.Mutex
433 | m := make(map[string]*Item)
434 | addItemToMap := func(it *Item) {
435 | lk.Lock()
436 | defer lk.Unlock()
437 | m[it.Key] = it
438 | }
439 |
440 | keyMap := make(map[net.Addr][]string)
441 | for _, key := range keys {
442 | if !legalKey(key) {
443 | return nil, ErrMalformedKey
444 | }
445 | addr, err := c.selector.PickServer(key)
446 | if err != nil {
447 | return nil, err
448 | }
449 | keyMap[addr] = append(keyMap[addr], key)
450 | }
451 |
452 | ch := make(chan error, buffered)
453 | for addr, keys := range keyMap {
454 | go func(addr net.Addr, keys []string) {
455 | ch <- c.getFromAddr(addr, keys, addItemToMap)
456 | }(addr, keys)
457 | }
458 |
459 | var err error
460 | for _ = range keyMap {
461 | if ge := <-ch; ge != nil {
462 | err = ge
463 | }
464 | }
465 | return m, err
466 | }
467 |
468 | // parseGetResponse reads a GET response from r and calls cb for each
469 | // read and allocated Item
470 | func parseGetResponse(r *bufio.Reader, cb func(*Item)) error {
471 | for {
472 | line, err := r.ReadSlice('\n')
473 | if err != nil {
474 | return err
475 | }
476 | if bytes.Equal(line, resultEnd) {
477 | return nil
478 | }
479 | it := new(Item)
480 | size, err := scanGetResponseLine(line, it)
481 | if err != nil {
482 | return err
483 | }
484 | it.Value, err = ioutil.ReadAll(io.LimitReader(r, int64(size)+2))
485 | if err != nil {
486 | return err
487 | }
488 | if !bytes.HasSuffix(it.Value, crlf) {
489 | return fmt.Errorf("memcache: corrupt get result read")
490 | }
491 | it.Value = it.Value[:size]
492 | cb(it)
493 | }
494 | }
495 |
496 | // scanGetResponseLine populates it and returns the declared size of the item.
497 | // It does not read the bytes of the item.
498 | func scanGetResponseLine(line []byte, it *Item) (size int, err error) {
499 | pattern := "VALUE %s %d %d %d\r\n"
500 | dest := []interface{}{&it.Key, &it.Flags, &size, &it.casid}
501 | if bytes.Count(line, space) == 3 {
502 | pattern = "VALUE %s %d %d\r\n"
503 | dest = dest[:3]
504 | }
505 | n, err := fmt.Sscanf(string(line), pattern, dest...)
506 | if err != nil || n != len(dest) {
507 | return -1, fmt.Errorf("memcache: unexpected line in get response: %q", line)
508 | }
509 | return size, nil
510 | }
511 |
512 | // Set writes the given item, unconditionally.
513 | func (c *Client) Set(item *Item) error {
514 | return c.onItem(item, (*Client).set)
515 | }
516 |
517 | func (c *Client) set(rw *bufio.ReadWriter, item *Item) error {
518 | return c.populateOne(rw, "set", item)
519 | }
520 |
521 | // Add writes the given item, if no value already exists for its
522 | // key. ErrNotStored is returned if that condition is not met.
523 | func (c *Client) Add(item *Item) error {
524 | return c.onItem(item, (*Client).add)
525 | }
526 |
527 | func (c *Client) add(rw *bufio.ReadWriter, item *Item) error {
528 | return c.populateOne(rw, "add", item)
529 | }
530 |
531 | // Replace writes the given item, but only if the server *does*
532 | // already hold data for this key
533 | func (c *Client) Replace(item *Item) error {
534 | return c.onItem(item, (*Client).replace)
535 | }
536 |
537 | func (c *Client) replace(rw *bufio.ReadWriter, item *Item) error {
538 | return c.populateOne(rw, "replace", item)
539 | }
540 |
541 | // CompareAndSwap writes the given item that was previously returned
542 | // by Get, if the value was neither modified or evicted between the
543 | // Get and the CompareAndSwap calls. The item's Key should not change
544 | // between calls but all other item fields may differ. ErrCASConflict
545 | // is returned if the value was modified in between the
546 | // calls. ErrNotStored is returned if the value was evicted in between
547 | // the calls.
548 | func (c *Client) CompareAndSwap(item *Item) error {
549 | return c.onItem(item, (*Client).cas)
550 | }
551 |
552 | func (c *Client) cas(rw *bufio.ReadWriter, item *Item) error {
553 | return c.populateOne(rw, "cas", item)
554 | }
555 |
556 | func (c *Client) populateOne(rw *bufio.ReadWriter, verb string, item *Item) error {
557 | if !legalKey(item.Key) {
558 | return ErrMalformedKey
559 | }
560 | var err error
561 | if verb == "cas" {
562 | _, err = fmt.Fprintf(rw, "%s %s %d %d %d %d\r\n",
563 | verb, item.Key, item.Flags, item.Expiration, len(item.Value), item.casid)
564 | } else {
565 | _, err = fmt.Fprintf(rw, "%s %s %d %d %d\r\n",
566 | verb, item.Key, item.Flags, item.Expiration, len(item.Value))
567 | }
568 | if err != nil {
569 | return err
570 | }
571 | if _, err = rw.Write(item.Value); err != nil {
572 | return err
573 | }
574 | if _, err := rw.Write(crlf); err != nil {
575 | return err
576 | }
577 | if err := rw.Flush(); err != nil {
578 | return err
579 | }
580 | line, err := rw.ReadSlice('\n')
581 | if err != nil {
582 | return err
583 | }
584 | switch {
585 | case bytes.Equal(line, resultStored):
586 | return nil
587 | case bytes.Equal(line, resultNotStored):
588 | return ErrNotStored
589 | case bytes.Equal(line, resultExists):
590 | return ErrCASConflict
591 | case bytes.Equal(line, resultNotFound):
592 | return ErrCacheMiss
593 | }
594 | return fmt.Errorf("memcache: unexpected response line from %q: %q", verb, string(line))
595 | }
596 |
597 | func writeReadLine(rw *bufio.ReadWriter, format string, args ...interface{}) ([]byte, error) {
598 | _, err := fmt.Fprintf(rw, format, args...)
599 | if err != nil {
600 | return nil, err
601 | }
602 | if err := rw.Flush(); err != nil {
603 | return nil, err
604 | }
605 | line, err := rw.ReadSlice('\n')
606 | return line, err
607 | }
608 |
609 | func writeExpectf(rw *bufio.ReadWriter, expect []byte, format string, args ...interface{}) error {
610 | line, err := writeReadLine(rw, format, args...)
611 | if err != nil {
612 | return err
613 | }
614 | switch {
615 | case bytes.Equal(line, resultOK):
616 | return nil
617 | case bytes.Equal(line, expect):
618 | return nil
619 | case bytes.Equal(line, resultNotStored):
620 | return ErrNotStored
621 | case bytes.Equal(line, resultExists):
622 | return ErrCASConflict
623 | case bytes.Equal(line, resultNotFound):
624 | return ErrCacheMiss
625 | }
626 | return fmt.Errorf("memcache: unexpected response line: %q", string(line))
627 | }
628 |
629 | // Delete deletes the item with the provided key. The error ErrCacheMiss is
630 | // returned if the item didn't already exist in the cache.
631 | func (c *Client) Delete(key string) error {
632 | return c.withKeyRw(key, func(rw *bufio.ReadWriter) error {
633 | return writeExpectf(rw, resultDeleted, "delete %s\r\n", key)
634 | })
635 | }
636 |
637 | // DeleteAll deletes all items in the cache.
638 | func (c *Client) DeleteAll() error {
639 | return c.withKeyRw("", func(rw *bufio.ReadWriter) error {
640 | return writeExpectf(rw, resultDeleted, "flush_all\r\n")
641 | })
642 | }
643 |
644 | // Increment atomically increments key by delta. The return value is
645 | // the new value after being incremented or an error. If the value
646 | // didn't exist in memcached the error is ErrCacheMiss. The value in
647 | // memcached must be an decimal number, or an error will be returned.
648 | // On 64-bit overflow, the new value wraps around.
649 | func (c *Client) Increment(key string, delta uint64) (newValue uint64, err error) {
650 | return c.incrDecr("incr", key, delta)
651 | }
652 |
653 | // Decrement atomically decrements key by delta. The return value is
654 | // the new value after being decremented or an error. If the value
655 | // didn't exist in memcached the error is ErrCacheMiss. The value in
656 | // memcached must be an decimal number, or an error will be returned.
657 | // On underflow, the new value is capped at zero and does not wrap
658 | // around.
659 | func (c *Client) Decrement(key string, delta uint64) (newValue uint64, err error) {
660 | return c.incrDecr("decr", key, delta)
661 | }
662 |
663 | func (c *Client) incrDecr(verb, key string, delta uint64) (uint64, error) {
664 | var val uint64
665 | err := c.withKeyRw(key, func(rw *bufio.ReadWriter) error {
666 | line, err := writeReadLine(rw, "%s %s %d\r\n", verb, key, delta)
667 | if err != nil {
668 | return err
669 | }
670 | switch {
671 | case bytes.Equal(line, resultNotFound):
672 | return ErrCacheMiss
673 | case bytes.HasPrefix(line, resultClientErrorPrefix):
674 | errMsg := line[len(resultClientErrorPrefix) : len(line)-2]
675 | return errors.New("memcache: client error: " + string(errMsg))
676 | }
677 | val, err = strconv.ParseUint(string(line[:len(line)-2]), 10, 64)
678 | if err != nil {
679 | return err
680 | }
681 | return nil
682 | })
683 | return val, err
684 | }
685 |
--------------------------------------------------------------------------------
/vendor/github.com/bradfitz/gomemcache/memcache/memcache_test.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2011 Google Inc.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | // Package memcache provides a client for the memcached cache server.
18 | package memcache
19 |
20 | import (
21 | "bufio"
22 | "fmt"
23 | "io"
24 | "io/ioutil"
25 | "net"
26 | "os"
27 | "os/exec"
28 | "strings"
29 | "testing"
30 | "time"
31 | )
32 |
33 | const testServer = "localhost:11211"
34 |
35 | func setup(t *testing.T) bool {
36 | c, err := net.Dial("tcp", testServer)
37 | if err != nil {
38 | t.Skipf("skipping test; no server running at %s", testServer)
39 | }
40 | c.Write([]byte("flush_all\r\n"))
41 | c.Close()
42 | return true
43 | }
44 |
45 | func TestLocalhost(t *testing.T) {
46 | if !setup(t) {
47 | return
48 | }
49 | testWithClient(t, New(testServer))
50 | }
51 |
52 | // Run the memcached binary as a child process and connect to its unix socket.
53 | func TestUnixSocket(t *testing.T) {
54 | sock := fmt.Sprintf("/tmp/test-gomemcache-%d.sock", os.Getpid())
55 | cmd := exec.Command("memcached", "-s", sock)
56 | if err := cmd.Start(); err != nil {
57 | t.Skipf("skipping test; couldn't find memcached")
58 | return
59 | }
60 | defer cmd.Wait()
61 | defer cmd.Process.Kill()
62 |
63 | // Wait a bit for the socket to appear.
64 | for i := 0; i < 10; i++ {
65 | if _, err := os.Stat(sock); err == nil {
66 | break
67 | }
68 | time.Sleep(time.Duration(25*i) * time.Millisecond)
69 | }
70 |
71 | testWithClient(t, New(sock))
72 | }
73 |
74 | func mustSetF(t *testing.T, c *Client) func(*Item) {
75 | return func(it *Item) {
76 | if err := c.Set(it); err != nil {
77 | t.Fatalf("failed to Set %#v: %v", *it, err)
78 | }
79 | }
80 | }
81 |
82 | func testWithClient(t *testing.T, c *Client) {
83 | checkErr := func(err error, format string, args ...interface{}) {
84 | if err != nil {
85 | t.Fatalf(format, args...)
86 | }
87 | }
88 | mustSet := mustSetF(t, c)
89 |
90 | // Set
91 | foo := &Item{Key: "foo", Value: []byte("fooval"), Flags: 123}
92 | err := c.Set(foo)
93 | checkErr(err, "first set(foo): %v", err)
94 | err = c.Set(foo)
95 | checkErr(err, "second set(foo): %v", err)
96 |
97 | // Get
98 | it, err := c.Get("foo")
99 | checkErr(err, "get(foo): %v", err)
100 | if it.Key != "foo" {
101 | t.Errorf("get(foo) Key = %q, want foo", it.Key)
102 | }
103 | if string(it.Value) != "fooval" {
104 | t.Errorf("get(foo) Value = %q, want fooval", string(it.Value))
105 | }
106 | if it.Flags != 123 {
107 | t.Errorf("get(foo) Flags = %v, want 123", it.Flags)
108 | }
109 |
110 | // Get and set a unicode key
111 | quxKey := "Hello_世界"
112 | qux := &Item{Key: quxKey, Value: []byte("hello world")}
113 | err = c.Set(qux)
114 | checkErr(err, "first set(Hello_世界): %v", err)
115 | it, err = c.Get(quxKey)
116 | checkErr(err, "get(Hello_世界): %v", err)
117 | if it.Key != quxKey {
118 | t.Errorf("get(Hello_世界) Key = %q, want Hello_世界", it.Key)
119 | }
120 | if string(it.Value) != "hello world" {
121 | t.Errorf("get(Hello_世界) Value = %q, want hello world", string(it.Value))
122 | }
123 |
124 | // Set malformed keys
125 | malFormed := &Item{Key: "foo bar", Value: []byte("foobarval")}
126 | err = c.Set(malFormed)
127 | if err != ErrMalformedKey {
128 | t.Errorf("set(foo bar) should return ErrMalformedKey instead of %v", err)
129 | }
130 | malFormed = &Item{Key: "foo" + string(0x7f), Value: []byte("foobarval")}
131 | err = c.Set(malFormed)
132 | if err != ErrMalformedKey {
133 | t.Errorf("set(foo<0x7f>) should return ErrMalformedKey instead of %v", err)
134 | }
135 |
136 | // Add
137 | bar := &Item{Key: "bar", Value: []byte("barval")}
138 | err = c.Add(bar)
139 | checkErr(err, "first add(foo): %v", err)
140 | if err := c.Add(bar); err != ErrNotStored {
141 | t.Fatalf("second add(foo) want ErrNotStored, got %v", err)
142 | }
143 |
144 | // Replace
145 | baz := &Item{Key: "baz", Value: []byte("bazvalue")}
146 | if err := c.Replace(baz); err != ErrNotStored {
147 | t.Fatalf("expected replace(baz) to return ErrNotStored, got %v", err)
148 | }
149 | err = c.Replace(bar)
150 | checkErr(err, "replaced(foo): %v", err)
151 |
152 | // GetMulti
153 | m, err := c.GetMulti([]string{"foo", "bar"})
154 | checkErr(err, "GetMulti: %v", err)
155 | if g, e := len(m), 2; g != e {
156 | t.Errorf("GetMulti: got len(map) = %d, want = %d", g, e)
157 | }
158 | if _, ok := m["foo"]; !ok {
159 | t.Fatalf("GetMulti: didn't get key 'foo'")
160 | }
161 | if _, ok := m["bar"]; !ok {
162 | t.Fatalf("GetMulti: didn't get key 'bar'")
163 | }
164 | if g, e := string(m["foo"].Value), "fooval"; g != e {
165 | t.Errorf("GetMulti: foo: got %q, want %q", g, e)
166 | }
167 | if g, e := string(m["bar"].Value), "barval"; g != e {
168 | t.Errorf("GetMulti: bar: got %q, want %q", g, e)
169 | }
170 |
171 | // Delete
172 | err = c.Delete("foo")
173 | checkErr(err, "Delete: %v", err)
174 | it, err = c.Get("foo")
175 | if err != ErrCacheMiss {
176 | t.Errorf("post-Delete want ErrCacheMiss, got %v", err)
177 | }
178 |
179 | // Incr/Decr
180 | mustSet(&Item{Key: "num", Value: []byte("42")})
181 | n, err := c.Increment("num", 8)
182 | checkErr(err, "Increment num + 8: %v", err)
183 | if n != 50 {
184 | t.Fatalf("Increment num + 8: want=50, got=%d", n)
185 | }
186 | n, err = c.Decrement("num", 49)
187 | checkErr(err, "Decrement: %v", err)
188 | if n != 1 {
189 | t.Fatalf("Decrement 49: want=1, got=%d", n)
190 | }
191 | err = c.Delete("num")
192 | checkErr(err, "delete num: %v", err)
193 | n, err = c.Increment("num", 1)
194 | if err != ErrCacheMiss {
195 | t.Fatalf("increment post-delete: want ErrCacheMiss, got %v", err)
196 | }
197 | mustSet(&Item{Key: "num", Value: []byte("not-numeric")})
198 | n, err = c.Increment("num", 1)
199 | if err == nil || !strings.Contains(err.Error(), "client error") {
200 | t.Fatalf("increment non-number: want client error, got %v", err)
201 | }
202 | testTouchWithClient(t, c)
203 |
204 | // Test Delete All
205 | err = c.DeleteAll()
206 | checkErr(err, "DeleteAll: %v", err)
207 | it, err = c.Get("bar")
208 | if err != ErrCacheMiss {
209 | t.Errorf("post-DeleteAll want ErrCacheMiss, got %v", err)
210 | }
211 |
212 | }
213 |
214 | func testTouchWithClient(t *testing.T, c *Client) {
215 | if testing.Short() {
216 | t.Log("Skipping testing memcache Touch with testing in Short mode")
217 | return
218 | }
219 |
220 | mustSet := mustSetF(t, c)
221 |
222 | const secondsToExpiry = int32(2)
223 |
224 | // We will set foo and bar to expire in 2 seconds, then we'll keep touching
225 | // foo every second
226 | // After 3 seconds, we expect foo to be available, and bar to be expired
227 | foo := &Item{Key: "foo", Value: []byte("fooval"), Expiration: secondsToExpiry}
228 | bar := &Item{Key: "bar", Value: []byte("barval"), Expiration: secondsToExpiry}
229 |
230 | setTime := time.Now()
231 | mustSet(foo)
232 | mustSet(bar)
233 |
234 | for s := 0; s < 3; s++ {
235 | time.Sleep(time.Duration(1 * time.Second))
236 | err := c.Touch(foo.Key, secondsToExpiry)
237 | if nil != err {
238 | t.Errorf("error touching foo: %v", err.Error())
239 | }
240 | }
241 |
242 | _, err := c.Get("foo")
243 | if err != nil {
244 | if err == ErrCacheMiss {
245 | t.Fatalf("touching failed to keep item foo alive")
246 | } else {
247 | t.Fatalf("unexpected error retrieving foo after touching: %v", err.Error())
248 | }
249 | }
250 |
251 | _, err = c.Get("bar")
252 | if nil == err {
253 | t.Fatalf("item bar did not expire within %v seconds", time.Now().Sub(setTime).Seconds())
254 | } else {
255 | if err != ErrCacheMiss {
256 | t.Fatalf("unexpected error retrieving bar: %v", err.Error())
257 | }
258 | }
259 | }
260 |
261 | func BenchmarkOnItem(b *testing.B) {
262 | fakeServer, err := net.Listen("tcp", "localhost:0")
263 | if err != nil {
264 | b.Fatal("Could not open fake server: ", err)
265 | }
266 | defer fakeServer.Close()
267 | go func() {
268 | for {
269 | if c, err := fakeServer.Accept(); err == nil {
270 | go func() { io.Copy(ioutil.Discard, c) }()
271 | } else {
272 | return
273 | }
274 | }
275 | }()
276 |
277 | addr := fakeServer.Addr()
278 | c := New(addr.String())
279 | if _, err := c.getConn(addr); err != nil {
280 | b.Fatal("failed to initialize connection to fake server")
281 | }
282 |
283 | item := Item{Key: "foo"}
284 | dummyFn := func(_ *Client, _ *bufio.ReadWriter, _ *Item) error { return nil }
285 | b.ResetTimer()
286 | for i := 0; i < b.N; i++ {
287 | c.onItem(&item, dummyFn)
288 | }
289 | }
290 |
--------------------------------------------------------------------------------
/vendor/github.com/bradfitz/gomemcache/memcache/selector.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2011 Google Inc.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package memcache
18 |
19 | import (
20 | "hash/crc32"
21 | "net"
22 | "strings"
23 | "sync"
24 | )
25 |
26 | // ServerSelector is the interface that selects a memcache server
27 | // as a function of the item's key.
28 | //
29 | // All ServerSelector implementations must be safe for concurrent use
30 | // by multiple goroutines.
31 | type ServerSelector interface {
32 | // PickServer returns the server address that a given item
33 | // should be shared onto.
34 | PickServer(key string) (net.Addr, error)
35 | Each(func(net.Addr) error) error
36 | }
37 |
38 | // ServerList is a simple ServerSelector. Its zero value is usable.
39 | type ServerList struct {
40 | mu sync.RWMutex
41 | addrs []net.Addr
42 | }
43 |
44 | // staticAddr caches the Network() and String() values from any net.Addr.
45 | type staticAddr struct {
46 | ntw, str string
47 | }
48 |
49 | func newStaticAddr(a net.Addr) net.Addr {
50 | return &staticAddr{
51 | ntw: a.Network(),
52 | str: a.String(),
53 | }
54 | }
55 |
56 | func (s *staticAddr) Network() string { return s.ntw }
57 | func (s *staticAddr) String() string { return s.str }
58 |
59 | // SetServers changes a ServerList's set of servers at runtime and is
60 | // safe for concurrent use by multiple goroutines.
61 | //
62 | // Each server is given equal weight. A server is given more weight
63 | // if it's listed multiple times.
64 | //
65 | // SetServers returns an error if any of the server names fail to
66 | // resolve. No attempt is made to connect to the server. If any error
67 | // is returned, no changes are made to the ServerList.
68 | func (ss *ServerList) SetServers(servers ...string) error {
69 | naddr := make([]net.Addr, len(servers))
70 | for i, server := range servers {
71 | if strings.Contains(server, "/") {
72 | addr, err := net.ResolveUnixAddr("unix", server)
73 | if err != nil {
74 | return err
75 | }
76 | naddr[i] = newStaticAddr(addr)
77 | } else {
78 | tcpaddr, err := net.ResolveTCPAddr("tcp", server)
79 | if err != nil {
80 | return err
81 | }
82 | naddr[i] = newStaticAddr(tcpaddr)
83 | }
84 | }
85 |
86 | ss.mu.Lock()
87 | defer ss.mu.Unlock()
88 | ss.addrs = naddr
89 | return nil
90 | }
91 |
92 | // Each iterates over each server calling the given function
93 | func (ss *ServerList) Each(f func(net.Addr) error) error {
94 | ss.mu.RLock()
95 | defer ss.mu.RUnlock()
96 | for _, a := range ss.addrs {
97 | if err := f(a); nil != err {
98 | return err
99 | }
100 | }
101 | return nil
102 | }
103 |
104 | // keyBufPool returns []byte buffers for use by PickServer's call to
105 | // crc32.ChecksumIEEE to avoid allocations. (but doesn't avoid the
106 | // copies, which at least are bounded in size and small)
107 | var keyBufPool = sync.Pool{
108 | New: func() interface{} {
109 | b := make([]byte, 256)
110 | return &b
111 | },
112 | }
113 |
114 | func (ss *ServerList) PickServer(key string) (net.Addr, error) {
115 | ss.mu.RLock()
116 | defer ss.mu.RUnlock()
117 | if len(ss.addrs) == 0 {
118 | return nil, ErrNoServers
119 | }
120 | if len(ss.addrs) == 1 {
121 | return ss.addrs[0], nil
122 | }
123 | bufp := keyBufPool.Get().(*[]byte)
124 | n := copy(*bufp, key)
125 | cs := crc32.ChecksumIEEE((*bufp)[:n])
126 | keyBufPool.Put(bufp)
127 |
128 | return ss.addrs[cs%uint32(len(ss.addrs))], nil
129 | }
130 |
--------------------------------------------------------------------------------
/vendor/github.com/bradfitz/gomemcache/memcache/selector_test.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2014 Google Inc.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package memcache
18 |
19 | import "testing"
20 |
21 | func BenchmarkPickServer(b *testing.B) {
22 | // at least two to avoid 0 and 1 special cases:
23 | benchPickServer(b, "127.0.0.1:1234", "127.0.0.1:1235")
24 | }
25 |
26 | func BenchmarkPickServer_Single(b *testing.B) {
27 | benchPickServer(b, "127.0.0.1:1234")
28 | }
29 |
30 | func benchPickServer(b *testing.B, servers ...string) {
31 | b.ReportAllocs()
32 | var ss ServerList
33 | ss.SetServers(servers...)
34 | for i := 0; i < b.N; i++ {
35 | if _, err := ss.PickServer("some key"); err != nil {
36 | b.Fatal(err)
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/vendor/github.com/integralist/go-findroot/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Mark McDonnell
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/vendor/github.com/integralist/go-findroot/README.md:
--------------------------------------------------------------------------------
1 | go-findroot
2 |
3 |
4 |
5 |
6 |
7 |
8 | Locate the root directory of a project using Git via the command line
9 |
10 |
11 | ## Example
12 |
13 | ```go
14 | package main
15 |
16 | import (
17 | "fmt"
18 | "log"
19 |
20 | "github.com/integralist/go-findroot/find"
21 | )
22 |
23 | func main() {
24 | root, err := find.Repo()
25 | if err != nil {
26 | log.Fatalf("Error: %s", err.Error())
27 | }
28 |
29 | fmt.Printf("%+v", root)
30 | // {Name:go-findroot Path:/Users/M/Projects/golang/src/github.com/integralist/go-findroot}
31 | }
32 | ```
33 |
34 | ## Tests
35 |
36 | ```go
37 | go test -v ./...
38 | ```
39 |
40 | ## Licence
41 |
42 | [The MIT License (MIT)](http://opensource.org/licenses/MIT)
43 |
44 | Copyright (c) 2016 [Mark McDonnell](http://twitter.com/integralist)
45 |
--------------------------------------------------------------------------------
/vendor/github.com/integralist/go-findroot/find/find.go:
--------------------------------------------------------------------------------
1 | package find
2 |
3 | import (
4 | "os/exec"
5 | "strings"
6 | )
7 |
8 | // Stat is exported out of golang convention, rather than necessity
9 | type Stat struct {
10 | Name string
11 | Path string
12 | }
13 |
14 | // Repo uses git via the console to locate the top level directory
15 | func Repo() (Stat, error) {
16 | path, err := rootPath()
17 | if err != nil {
18 | return Stat{
19 | "Unknown",
20 | "./",
21 | }, err
22 | }
23 |
24 | gitRepo, err := exec.Command("basename", path).Output()
25 | if err != nil {
26 | return Stat{}, err
27 | }
28 |
29 | return Stat{
30 | strings.TrimSpace(string(gitRepo)),
31 | path,
32 | }, nil
33 | }
34 |
35 | func rootPath() (string, error) {
36 | path, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
37 | if err != nil {
38 | return "", err
39 | }
40 | return strings.TrimSpace(string(path)), nil
41 | }
42 |
--------------------------------------------------------------------------------
/vendor/github.com/integralist/go-findroot/find/find_test.go:
--------------------------------------------------------------------------------
1 | package find
2 |
3 | import (
4 | "log"
5 | "testing"
6 | )
7 |
8 | func TestRootIsFound(t *testing.T) {
9 | response, err := Repo()
10 | if err != nil {
11 | log.Fatalf("Error: %s", err.Error())
12 | }
13 |
14 | expectation := "go-findroot"
15 |
16 | if response.Name != expectation {
17 | t.Errorf("The response '%s' didn't match the expectation '%s'", response.Name, expectation)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------