├── .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 | --------------------------------------------------------------------------------