├── License ├── Readme.md ├── common_test.go ├── conn.go ├── conn_test.go ├── doc.go ├── err.go ├── example_test.go ├── go.mod ├── name.go ├── parse.go ├── parse_test.go ├── time.go ├── time_test.go ├── tube.go ├── tube_test.go ├── tubeset.go └── tubeset_test.go /License: -------------------------------------------------------------------------------- 1 | Copyright 2012 Keith Rarick 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Beanstalk 2 | 3 | Go client for [beanstalkd](https://beanstalkd.github.io). 4 | 5 | ## Install 6 | 7 | $ go get github.com/beanstalkd/go-beanstalk 8 | 9 | ## Use 10 | 11 | Produce jobs: 12 | 13 | c, err := beanstalk.Dial("tcp", "127.0.0.1:11300") 14 | id, err := c.Put([]byte("hello"), 1, 0, 120*time.Second) 15 | 16 | Consume jobs: 17 | 18 | c, err := beanstalk.Dial("tcp", "127.0.0.1:11300") 19 | id, body, err := c.Reserve(5 * time.Second) 20 | -------------------------------------------------------------------------------- /common_test.go: -------------------------------------------------------------------------------- 1 | package beanstalk 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | type mockError struct { 10 | exp []byte 11 | got []byte 12 | } 13 | 14 | func (e mockError) Error() string { 15 | return fmt.Sprintf( 16 | "mock error: exp %#v, got %#v", 17 | string(e.exp), 18 | string(e.got), 19 | ) 20 | } 21 | 22 | type mockIO struct { 23 | recv *strings.Reader 24 | send *strings.Reader 25 | } 26 | 27 | func mock(recv, send string) io.ReadWriteCloser { 28 | return &mockIO{strings.NewReader(recv), strings.NewReader(send)} 29 | } 30 | 31 | func (m mockIO) Read(b []byte) (int, error) { 32 | return m.send.Read(b) 33 | } 34 | 35 | func (m mockIO) Write(got []byte) (n int, err error) { 36 | exp := make([]byte, len(got)) 37 | n, err = m.recv.Read(exp) 38 | if err != nil { 39 | return n, err 40 | } 41 | exp = exp[:n] 42 | for i := range exp { 43 | if exp[i] != got[i] { 44 | return i, mockError{exp, got} 45 | } 46 | } 47 | if n != len(got) { 48 | return n, mockError{exp, got} 49 | } 50 | return n, err 51 | } 52 | 53 | func (m mockIO) Close() error { 54 | if m.recv.Len() == 0 && m.send.Len() == 0 { 55 | return nil 56 | } 57 | if m.recv.Len() > 0 { 58 | b := make([]byte, m.recv.Len()) 59 | m.recv.Read(b) 60 | return mockError{b, nil} 61 | } 62 | b := make([]byte, m.send.Len()) 63 | m.send.Read(b) 64 | return mockError{b, nil} 65 | } 66 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | package beanstalk 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "net/textproto" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | // DefaultDialTimeout is the time to wait for a connection to the beanstalk server. 13 | const DefaultDialTimeout = 10 * time.Second 14 | 15 | // DefaultKeepAlivePeriod is the default period between TCP keepalive messages. 16 | const DefaultKeepAlivePeriod = 10 * time.Second 17 | 18 | // A Conn represents a connection to a beanstalkd server. It consists 19 | // of a default Tube and TubeSet as well as the underlying network 20 | // connection. The embedded types carry methods with them; see the 21 | // documentation of those types for details. 22 | type Conn struct { 23 | c *textproto.Conn 24 | used string 25 | watched map[string]bool 26 | Tube 27 | TubeSet 28 | } 29 | 30 | var ( 31 | space = []byte{' '} 32 | crnl = []byte{'\r', '\n'} 33 | yamlHead = []byte{'-', '-', '-', '\n'} 34 | nl = []byte{'\n'} 35 | colonSpace = []byte{':', ' '} 36 | minusSpace = []byte{'-', ' '} 37 | ) 38 | 39 | // NewConn returns a new Conn using conn for I/O. 40 | func NewConn(conn io.ReadWriteCloser) *Conn { 41 | c := new(Conn) 42 | c.c = textproto.NewConn(conn) 43 | c.Tube = *NewTube(c, "default") 44 | c.TubeSet = *NewTubeSet(c, "default") 45 | c.used = "default" 46 | c.watched = map[string]bool{"default": true} 47 | return c 48 | } 49 | 50 | // Dial connects addr on the given network using net.DialTimeout 51 | // with a default timeout of 10s and then returns a new Conn for the connection. 52 | func Dial(network, addr string) (*Conn, error) { 53 | return DialTimeout(network, addr, DefaultDialTimeout) 54 | } 55 | 56 | // DialTimeout connects addr on the given network using net.DialTimeout 57 | // with a supplied timeout and then returns a new Conn for the connection. 58 | func DialTimeout(network, addr string, timeout time.Duration) (*Conn, error) { 59 | dialer := &net.Dialer{ 60 | Timeout: timeout, 61 | KeepAlive: DefaultKeepAlivePeriod, 62 | } 63 | c, err := dialer.Dial(network, addr) 64 | if err != nil { 65 | return nil, err 66 | } 67 | return NewConn(c), nil 68 | } 69 | 70 | // Close closes the underlying network connection. 71 | func (c *Conn) Close() error { 72 | return c.c.Close() 73 | } 74 | 75 | func (c *Conn) cmd(t *Tube, ts *TubeSet, body []byte, op string, args ...interface{}) (req, error) { 76 | // negative dur checking 77 | for _, arg := range args { 78 | if d, _ := arg.(dur); d < 0 { 79 | return req{}, fmt.Errorf("duration must be non-negative, got %v", time.Duration(d)) 80 | } 81 | } 82 | 83 | r := req{c.c.Next(), op} 84 | c.c.StartRequest(r.id) 85 | defer c.c.EndRequest(r.id) 86 | err := c.adjustTubes(t, ts) 87 | if err != nil { 88 | return req{}, err 89 | } 90 | if body != nil { 91 | args = append(args, len(body)) 92 | } 93 | c.printLine(op, args...) 94 | if body != nil { 95 | c.c.W.Write(body) 96 | c.c.W.Write(crnl) 97 | } 98 | err = c.c.W.Flush() 99 | if err != nil { 100 | return req{}, ConnError{c, op, err} 101 | } 102 | return r, nil 103 | } 104 | 105 | func (c *Conn) adjustTubes(t *Tube, ts *TubeSet) error { 106 | if t != nil && t.Name != c.used { 107 | if err := checkName(t.Name); err != nil { 108 | return err 109 | } 110 | c.printLine("use", t.Name) 111 | c.used = t.Name 112 | } 113 | if ts != nil { 114 | for s := range ts.Name { 115 | if !c.watched[s] { 116 | if err := checkName(s); err != nil { 117 | return err 118 | } 119 | c.printLine("watch", s) 120 | } 121 | } 122 | for s := range c.watched { 123 | if !ts.Name[s] { 124 | c.printLine("ignore", s) 125 | } 126 | } 127 | c.watched = make(map[string]bool) 128 | for s := range ts.Name { 129 | c.watched[s] = true 130 | } 131 | } 132 | return nil 133 | } 134 | 135 | // does not flush 136 | func (c *Conn) printLine(cmd string, args ...interface{}) { 137 | io.WriteString(c.c.W, cmd) 138 | for _, a := range args { 139 | c.c.W.Write(space) 140 | fmt.Fprint(c.c.W, a) 141 | } 142 | c.c.W.Write(crnl) 143 | } 144 | 145 | func (c *Conn) readResp(r req, readBody bool, f string, a ...interface{}) (body []byte, err error) { 146 | c.c.StartResponse(r.id) 147 | defer c.c.EndResponse(r.id) 148 | line, err := c.c.ReadLine() 149 | for strings.HasPrefix(line, "WATCHING ") || strings.HasPrefix(line, "USING ") { 150 | line, err = c.c.ReadLine() 151 | } 152 | if err != nil { 153 | return nil, ConnError{c, r.op, err} 154 | } 155 | toScan := line 156 | if readBody { 157 | var size int 158 | toScan, size, err = parseSize(toScan) 159 | if err != nil { 160 | return nil, ConnError{c, r.op, err} 161 | } 162 | body = make([]byte, size+2) // include trailing CR NL 163 | _, err = io.ReadFull(c.c.R, body) 164 | if err != nil { 165 | return nil, ConnError{c, r.op, err} 166 | } 167 | body = body[:size] // exclude trailing CR NL 168 | } 169 | 170 | err = scan(toScan, f, a...) 171 | if err != nil { 172 | return nil, ConnError{c, r.op, err} 173 | } 174 | return body, nil 175 | } 176 | 177 | // Delete deletes the given job. 178 | func (c *Conn) Delete(id uint64) error { 179 | r, err := c.cmd(nil, nil, nil, "delete", id) 180 | if err != nil { 181 | return err 182 | } 183 | _, err = c.readResp(r, false, "DELETED") 184 | return err 185 | } 186 | 187 | // Release tells the server to perform the following actions: 188 | // set the priority of the given job to pri, remove it from the list of 189 | // jobs reserved by c, wait delay seconds, then place the job in the 190 | // ready queue, which makes it available for reservation by any client. 191 | func (c *Conn) Release(id uint64, pri uint32, delay time.Duration) error { 192 | r, err := c.cmd(nil, nil, nil, "release", id, pri, dur(delay)) 193 | if err != nil { 194 | return err 195 | } 196 | _, err = c.readResp(r, false, "RELEASED") 197 | return err 198 | } 199 | 200 | // Bury places the given job in a holding area in the job's tube and 201 | // sets its priority to pri. The job will not be scheduled again until it 202 | // has been kicked; see also the documentation of Kick. 203 | func (c *Conn) Bury(id uint64, pri uint32) error { 204 | r, err := c.cmd(nil, nil, nil, "bury", id, pri) 205 | if err != nil { 206 | return err 207 | } 208 | _, err = c.readResp(r, false, "BURIED") 209 | return err 210 | } 211 | 212 | // KickJob places the given job to the ready queue of the same tube where it currently belongs 213 | // when the given job id exists and is in a buried or delayed state. 214 | func (c *Conn) KickJob(id uint64) error { 215 | r, err := c.cmd(nil, nil, nil, "kick-job", id) 216 | if err != nil { 217 | return err 218 | } 219 | _, err = c.readResp(r, false, "KICKED") 220 | return err 221 | } 222 | 223 | // Touch resets the reservation timer for the given job. 224 | // It is an error if the job isn't currently reserved by c. 225 | // See the documentation of Reserve for more details. 226 | func (c *Conn) Touch(id uint64) error { 227 | r, err := c.cmd(nil, nil, nil, "touch", id) 228 | if err != nil { 229 | return err 230 | } 231 | _, err = c.readResp(r, false, "TOUCHED") 232 | return err 233 | } 234 | 235 | // Peek gets a copy of the specified job from the server. 236 | func (c *Conn) Peek(id uint64) (body []byte, err error) { 237 | r, err := c.cmd(nil, nil, nil, "peek", id) 238 | if err != nil { 239 | return nil, err 240 | } 241 | return c.readResp(r, true, "FOUND %d", &id) 242 | } 243 | 244 | // ReserveJob reserves the specified job by id from the server. 245 | func (c *Conn) ReserveJob(id uint64) (body []byte, err error) { 246 | r, err := c.cmd(nil, nil, nil, "reserve-job", id) 247 | if err != nil { 248 | return nil, err 249 | } 250 | return c.readResp(r, true, "RESERVED %d", &id) 251 | } 252 | 253 | // Stats retrieves global statistics from the server. 254 | func (c *Conn) Stats() (map[string]string, error) { 255 | r, err := c.cmd(nil, nil, nil, "stats") 256 | if err != nil { 257 | return nil, err 258 | } 259 | body, err := c.readResp(r, true, "OK") 260 | return parseDict(body), err 261 | } 262 | 263 | // StatsJob retrieves statistics about the given job. 264 | func (c *Conn) StatsJob(id uint64) (map[string]string, error) { 265 | r, err := c.cmd(nil, nil, nil, "stats-job", id) 266 | if err != nil { 267 | return nil, err 268 | } 269 | body, err := c.readResp(r, true, "OK") 270 | return parseDict(body), err 271 | } 272 | 273 | // ListTubes returns the names of the tubes that currently 274 | // exist on the server. 275 | func (c *Conn) ListTubes() ([]string, error) { 276 | r, err := c.cmd(nil, nil, nil, "list-tubes") 277 | if err != nil { 278 | return nil, err 279 | } 280 | body, err := c.readResp(r, true, "OK") 281 | return parseList(body), err 282 | } 283 | 284 | func scan(input, format string, a ...interface{}) error { 285 | _, err := fmt.Sscanf(input, format, a...) 286 | if err != nil { 287 | return findRespError(input) 288 | } 289 | return nil 290 | } 291 | 292 | type req struct { 293 | id uint 294 | op string 295 | } 296 | -------------------------------------------------------------------------------- /conn_test.go: -------------------------------------------------------------------------------- 1 | package beanstalk 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestNameTooLong(t *testing.T) { 9 | c := NewConn(mock("", "")) 10 | 11 | tube := NewTube(c, string(make([]byte, 201))) 12 | _, err := tube.Put([]byte("foo"), 0, 0, 0) 13 | if e, ok := err.(NameError); !ok || e.Err != ErrTooLong { 14 | t.Fatal(err) 15 | } 16 | if err = c.Close(); err != nil { 17 | t.Fatal(err) 18 | } 19 | } 20 | 21 | func TestNameEmpty(t *testing.T) { 22 | c := NewConn(mock("", "")) 23 | 24 | tube := NewTube(c, "") 25 | _, err := tube.Put([]byte("foo"), 0, 0, 0) 26 | if e, ok := err.(NameError); !ok || e.Err != ErrEmpty { 27 | t.Fatal(err) 28 | } 29 | if err = c.Close(); err != nil { 30 | t.Fatal(err) 31 | } 32 | } 33 | 34 | func TestNameBadChar(t *testing.T) { 35 | c := NewConn(mock("", "")) 36 | 37 | tube := NewTube(c, "*") 38 | _, err := tube.Put([]byte("foo"), 0, 0, 0) 39 | if e, ok := err.(NameError); !ok || e.Err != ErrBadChar { 40 | t.Fatal(err) 41 | } 42 | if err = c.Close(); err != nil { 43 | t.Fatal(err) 44 | } 45 | } 46 | 47 | func TestNegativeDuration(t *testing.T) { 48 | c := NewConn(mock("", "")) 49 | tube := NewTube(c, "foo") 50 | for _, d := range []time.Duration{-100 * time.Millisecond, -2 * time.Second} { 51 | if _, err := tube.Put([]byte("hello"), 0, d, d); err == nil { 52 | t.Fatalf("put job with negative duration %v expected error, got nil", d) 53 | } 54 | } 55 | } 56 | 57 | func TestDeleteMissing(t *testing.T) { 58 | c := NewConn(mock("delete 1\r\n", "NOT_FOUND\r\n")) 59 | 60 | err := c.Delete(1) 61 | if e, ok := err.(ConnError); !ok || e.Err != ErrNotFound { 62 | t.Fatal(err) 63 | } 64 | if err = c.Close(); err != nil { 65 | t.Fatal(err) 66 | } 67 | } 68 | 69 | func TestUse(t *testing.T) { 70 | c := NewConn(mock( 71 | "use foo\r\nput 0 0 0 5\r\nhello\r\n", 72 | "USING foo\r\nINSERTED 1\r\n", 73 | )) 74 | tube := NewTube(c, "foo") 75 | id, err := tube.Put([]byte("hello"), 0, 0, 0) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | if id != 1 { 80 | t.Fatal("expected 1, got", id) 81 | } 82 | if err = c.Close(); err != nil { 83 | t.Fatal(err) 84 | } 85 | } 86 | 87 | func TestWatchIgnore(t *testing.T) { 88 | c := NewConn(mock( 89 | "watch foo\r\nignore default\r\nreserve-with-timeout 1\r\n", 90 | "WATCHING 2\r\nWATCHING 1\r\nRESERVED 1 1\r\nx\r\n", 91 | )) 92 | ts := NewTubeSet(c, "foo") 93 | id, body, err := ts.Reserve(time.Second) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | if id != 1 { 98 | t.Fatal("expected 1, got", id) 99 | } 100 | if len(body) != 1 || body[0] != 'x' { 101 | t.Fatalf("bad body, expected %#v, got %#v", "x", string(body)) 102 | } 103 | if err = c.Close(); err != nil { 104 | t.Fatal(err) 105 | } 106 | } 107 | 108 | func TestBury(t *testing.T) { 109 | c := NewConn(mock("bury 1 3\r\n", "BURIED\r\n")) 110 | 111 | err := c.Bury(1, 3) 112 | if err != nil { 113 | t.Fatal(err) 114 | } 115 | if err = c.Close(); err != nil { 116 | t.Fatal(err) 117 | } 118 | } 119 | 120 | func TestTubeKickJob(t *testing.T) { 121 | c := NewConn(mock("kick-job 3\r\n", "KICKED\r\n")) 122 | 123 | err := c.KickJob(3) 124 | if err != nil { 125 | t.Fatal(err) 126 | } 127 | if err = c.Close(); err != nil { 128 | t.Fatal(err) 129 | } 130 | } 131 | 132 | func TestDelete(t *testing.T) { 133 | c := NewConn(mock("delete 1\r\n", "DELETED\r\n")) 134 | 135 | err := c.Delete(1) 136 | if err != nil { 137 | t.Fatal(err) 138 | } 139 | if err = c.Close(); err != nil { 140 | t.Fatal(err) 141 | } 142 | } 143 | 144 | func TestListTubes(t *testing.T) { 145 | c := NewConn(mock("list-tubes\r\n", "OK 14\r\n---\n- default\n\r\n")) 146 | 147 | l, err := c.ListTubes() 148 | if err != nil { 149 | t.Fatal(err) 150 | } 151 | if len(l) != 1 || l[0] != "default" { 152 | t.Fatalf("expected %#v, got %#v", []string{"default"}, l) 153 | } 154 | if err = c.Close(); err != nil { 155 | t.Fatal(err) 156 | } 157 | } 158 | 159 | func TestPeek(t *testing.T) { 160 | c := NewConn(mock("peek 1\r\n", "FOUND 1 1\r\nx\r\n")) 161 | 162 | body, err := c.Peek(1) 163 | if err != nil { 164 | t.Fatal(err) 165 | } 166 | if len(body) != 1 || body[0] != 'x' { 167 | t.Fatalf("bad body, expected %#v, got %#v", "x", string(body)) 168 | } 169 | if err = c.Close(); err != nil { 170 | t.Fatal(err) 171 | } 172 | } 173 | 174 | func TestPeekTwice(t *testing.T) { 175 | c := NewConn(mock( 176 | "peek 1\r\npeek 1\r\n", 177 | "FOUND 1 1\r\nx\r\nFOUND 1 1\r\nx\r\n", 178 | )) 179 | 180 | body, err := c.Peek(1) 181 | if err != nil { 182 | t.Fatal(err) 183 | } 184 | if len(body) != 1 || body[0] != 'x' { 185 | t.Fatalf("bad body, expected %#v, got %#v", "x", string(body)) 186 | } 187 | 188 | body, err = c.Peek(1) 189 | if err != nil { 190 | t.Fatal(err) 191 | } 192 | if len(body) != 1 || body[0] != 'x' { 193 | t.Fatalf("bad body, expected %#v, got %#v", "x", string(body)) 194 | } 195 | if err = c.Close(); err != nil { 196 | t.Fatal(err) 197 | } 198 | } 199 | 200 | func TestReserveJob(t *testing.T) { 201 | c := NewConn(mock("reserve-job 1\r\n", "RESERVED 1 1\r\nx\r\n")) 202 | 203 | body, err := c.ReserveJob(1) 204 | if err != nil { 205 | t.Fatal(err) 206 | } 207 | if len(body) != 1 || body[0] != 'x' { 208 | t.Fatalf("bad body, expected %#v, got %#v", "x", string(body)) 209 | } 210 | if err = c.Close(); err != nil { 211 | t.Fatal(err) 212 | } 213 | } 214 | 215 | func TestRelease(t *testing.T) { 216 | c := NewConn(mock("release 1 3 2\r\n", "RELEASED\r\n")) 217 | 218 | err := c.Release(1, 3, 2*time.Second) 219 | if err != nil { 220 | t.Fatal(err) 221 | } 222 | if err = c.Close(); err != nil { 223 | t.Fatal(err) 224 | } 225 | } 226 | 227 | func TestStats(t *testing.T) { 228 | c := NewConn(mock("stats\r\n", "OK 10\r\n---\na: ok\n\r\n")) 229 | 230 | m, err := c.Stats() 231 | if err != nil { 232 | t.Fatal(err) 233 | } 234 | if len(m) != 1 || m["a"] != "ok" { 235 | t.Fatalf("expected %#v, got %#v", map[string]string{"a": "ok"}, m) 236 | } 237 | if err = c.Close(); err != nil { 238 | t.Fatal(err) 239 | } 240 | } 241 | 242 | func TestStatsJob(t *testing.T) { 243 | c := NewConn(mock("stats-job 1\r\n", "OK 10\r\n---\na: ok\n\r\n")) 244 | 245 | m, err := c.StatsJob(1) 246 | if err != nil { 247 | t.Fatal(err) 248 | } 249 | if len(m) != 1 || m["a"] != "ok" { 250 | t.Fatalf("expected %#v, got %#v", map[string]string{"a": "ok"}, m) 251 | } 252 | if err = c.Close(); err != nil { 253 | t.Fatal(err) 254 | } 255 | } 256 | 257 | func TestTouch(t *testing.T) { 258 | c := NewConn(mock("touch 1\r\n", "TOUCHED\r\n")) 259 | 260 | err := c.Touch(1) 261 | if err != nil { 262 | t.Fatal(err) 263 | } 264 | if err = c.Close(); err != nil { 265 | t.Fatal(err) 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package beanstalk provides a client for the beanstalk protocol. 2 | // See http://kr.github.com/beanstalkd/ for the server. 3 | // 4 | // This package is synchronized internally and safe to use from 5 | // multiple goroutines without other coordination. 6 | package beanstalk 7 | -------------------------------------------------------------------------------- /err.go: -------------------------------------------------------------------------------- 1 | package beanstalk 2 | 3 | import "errors" 4 | 5 | // ConnError records an error message from the server and the operation 6 | // and connection that caused it. 7 | type ConnError struct { 8 | Conn *Conn 9 | Op string 10 | Err error 11 | } 12 | 13 | func (e ConnError) Error() string { 14 | return e.Op + ": " + e.Err.Error() 15 | } 16 | 17 | func (e ConnError) Unwrap() error { 18 | return e.Err 19 | } 20 | 21 | // Error messages returned by the server. 22 | var ( 23 | ErrBadFormat = errors.New("bad command format") 24 | ErrBuried = errors.New("buried") 25 | ErrDeadline = errors.New("deadline soon") 26 | ErrDraining = errors.New("draining") 27 | ErrInternal = errors.New("internal error") 28 | ErrJobTooBig = errors.New("job too big") 29 | ErrNoCRLF = errors.New("expected CR LF") 30 | ErrNotFound = errors.New("not found") 31 | ErrNotIgnored = errors.New("not ignored") 32 | ErrOOM = errors.New("server is out of memory") 33 | ErrTimeout = errors.New("timeout") 34 | ErrUnknown = errors.New("unknown command") 35 | ) 36 | 37 | var respError = map[string]error{ 38 | "BAD_FORMAT": ErrBadFormat, 39 | "BURIED": ErrBuried, 40 | "DEADLINE_SOON": ErrDeadline, 41 | "DRAINING": ErrDraining, 42 | "EXPECTED_CRLF": ErrNoCRLF, 43 | "INTERNAL_ERROR": ErrInternal, 44 | "JOB_TOO_BIG": ErrJobTooBig, 45 | "NOT_FOUND": ErrNotFound, 46 | "NOT_IGNORED": ErrNotIgnored, 47 | "OUT_OF_MEMORY": ErrOOM, 48 | "TIMED_OUT": ErrTimeout, 49 | "UNKNOWN_COMMAND": ErrUnknown, 50 | } 51 | 52 | type unknownRespError string 53 | 54 | func (e unknownRespError) Error() string { 55 | return "unknown response: " + string(e) 56 | } 57 | 58 | func findRespError(s string) error { 59 | if err := respError[s]; err != nil { 60 | return err 61 | } 62 | return unknownRespError(s) 63 | } 64 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package beanstalk_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/beanstalkd/go-beanstalk" 6 | "time" 7 | ) 8 | 9 | var conn, _ = beanstalk.Dial("tcp", "127.0.0.1:11300") 10 | 11 | func Example_reserve() { 12 | id, body, err := conn.Reserve(5 * time.Second) 13 | if err != nil { 14 | panic(err) 15 | } 16 | fmt.Println("job", id) 17 | fmt.Println(string(body)) 18 | } 19 | 20 | func Example_reserveOtherTubeSet() { 21 | tubeSet := beanstalk.NewTubeSet(conn, "mytube1", "mytube2") 22 | id, body, err := tubeSet.Reserve(10 * time.Hour) 23 | if err != nil { 24 | panic(err) 25 | } 26 | fmt.Println("job", id) 27 | fmt.Println(string(body)) 28 | } 29 | 30 | func Example_put() { 31 | id, err := conn.Put([]byte("myjob"), 1, 0, time.Minute) 32 | if err != nil { 33 | panic(err) 34 | } 35 | fmt.Println("job", id) 36 | } 37 | 38 | func Example_putOtherTube() { 39 | tube := beanstalk.NewTube(conn, "mytube") 40 | id, err := tube.Put([]byte("myjob"), 1, 0, time.Minute) 41 | if err != nil { 42 | panic(err) 43 | } 44 | fmt.Println("job", id) 45 | } 46 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/beanstalkd/go-beanstalk 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /name.go: -------------------------------------------------------------------------------- 1 | package beanstalk 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // NameChars are the allowed name characters in the beanstalkd protocol. 8 | const NameChars = `\-+/;.$_()0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz` 9 | 10 | // NameError indicates that a name was malformed and the specific error 11 | // describing how. 12 | type NameError struct { 13 | Name string 14 | Err error 15 | } 16 | 17 | func (e NameError) Error() string { 18 | return e.Err.Error() + ": " + e.Name 19 | } 20 | 21 | func (e NameError) Unwrap() error { 22 | return e.Err 23 | } 24 | 25 | // Name format errors. The Err field of NameError contains one of these. 26 | var ( 27 | ErrEmpty = errors.New("name is empty") 28 | ErrBadChar = errors.New("name has bad char") // contains a character not in NameChars 29 | ErrTooLong = errors.New("name is too long") 30 | ) 31 | 32 | func checkName(s string) error { 33 | switch { 34 | case len(s) == 0: 35 | return NameError{s, ErrEmpty} 36 | case len(s) >= 200: 37 | return NameError{s, ErrTooLong} 38 | case !containsOnly(s, NameChars): 39 | return NameError{s, ErrBadChar} 40 | } 41 | return nil 42 | } 43 | 44 | func containsOnly(s, chars string) bool { 45 | outer: 46 | for _, c := range s { 47 | for _, m := range chars { 48 | if c == m { 49 | continue outer 50 | } 51 | } 52 | return false 53 | } 54 | return true 55 | } 56 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package beanstalk 2 | 3 | import ( 4 | "bytes" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | func parseDict(dat []byte) map[string]string { 10 | if dat == nil { 11 | return nil 12 | } 13 | d := make(map[string]string) 14 | if bytes.HasPrefix(dat, yamlHead) { 15 | dat = dat[4:] 16 | } 17 | for _, s := range bytes.Split(dat, nl) { 18 | kv := bytes.SplitN(s, colonSpace, 2) 19 | if len(kv) != 2 { 20 | continue 21 | } 22 | d[string(kv[0])] = string(kv[1]) 23 | } 24 | return d 25 | } 26 | 27 | func parseList(dat []byte) []string { 28 | if dat == nil { 29 | return nil 30 | } 31 | l := []string{} 32 | if bytes.HasPrefix(dat, yamlHead) { 33 | dat = dat[4:] 34 | } 35 | for _, s := range bytes.Split(dat, nl) { 36 | if !bytes.HasPrefix(s, minusSpace) { 37 | continue 38 | } 39 | l = append(l, string(s[2:])) 40 | } 41 | return l 42 | } 43 | 44 | func parseSize(s string) (string, int, error) { 45 | i := strings.LastIndex(s, " ") 46 | if i == -1 { 47 | return "", 0, findRespError(s) 48 | } 49 | n, err := strconv.Atoi(s[i+1:]) 50 | if err != nil { 51 | return "", 0, err 52 | } 53 | return s[:i], n, nil 54 | } 55 | -------------------------------------------------------------------------------- /parse_test.go: -------------------------------------------------------------------------------- 1 | package beanstalk 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestParseDict(t *testing.T) { 9 | d := parseDict([]byte("---\na: 1\nb: 2\n")) 10 | if !reflect.DeepEqual(d, map[string]string{"a": "1", "b": "2"}) { 11 | t.Fatalf("got %v", d) 12 | } 13 | } 14 | 15 | func TestParseDictEmpty(t *testing.T) { 16 | d := parseDict([]byte{}) 17 | if !reflect.DeepEqual(d, map[string]string{}) { 18 | t.Fatalf("got %v", d) 19 | } 20 | } 21 | 22 | func TestParseDictNil(t *testing.T) { 23 | d := parseDict(nil) 24 | if d != nil { 25 | t.Fatalf("got %v", d) 26 | } 27 | } 28 | 29 | func TestParseList(t *testing.T) { 30 | l := parseList([]byte("---\n- 1\n- 2\n")) 31 | if !reflect.DeepEqual(l, []string{"1", "2"}) { 32 | t.Fatalf("got %v", l) 33 | } 34 | } 35 | 36 | func TestParseListEmpty(t *testing.T) { 37 | l := parseList([]byte{}) 38 | if !reflect.DeepEqual(l, []string{}) { 39 | t.Fatalf("got %v", l) 40 | } 41 | } 42 | 43 | func TestParseListNil(t *testing.T) { 44 | l := parseList(nil) 45 | if l != nil { 46 | t.Fatalf("got %v", l) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /time.go: -------------------------------------------------------------------------------- 1 | package beanstalk 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | type dur time.Duration 9 | 10 | func (d dur) String() string { 11 | return strconv.FormatInt(int64(time.Duration(d)/time.Second), 10) 12 | } 13 | -------------------------------------------------------------------------------- /time_test.go: -------------------------------------------------------------------------------- 1 | package beanstalk 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestFormatDuration(t *testing.T) { 9 | var d dur = 100e9 10 | s := fmt.Sprint(d) 11 | if s != "100" { 12 | t.Fatal("got", s, "expected 100") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tube.go: -------------------------------------------------------------------------------- 1 | package beanstalk 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Tube represents tube Name on the server connected to by Conn. 8 | // It has methods for commands that operate on a single tube. 9 | type Tube struct { 10 | Conn *Conn 11 | Name string 12 | } 13 | 14 | // NewTube returns a new Tube representing the given name. 15 | func NewTube(c *Conn, name string) *Tube { 16 | return &Tube{c, name} 17 | } 18 | 19 | // Put puts a job into tube t with priority pri and TTR ttr, and returns 20 | // the id of the newly-created job. If delay is nonzero, the server will 21 | // wait the given amount of time after returning to the client and before 22 | // putting the job into the ready queue. 23 | func (t *Tube) Put(body []byte, pri uint32, delay, ttr time.Duration) (id uint64, err error) { 24 | r, err := t.Conn.cmd(t, nil, body, "put", pri, dur(delay), dur(ttr)) 25 | if err != nil { 26 | return 0, err 27 | } 28 | _, err = t.Conn.readResp(r, false, "INSERTED %d", &id) 29 | if err != nil { 30 | return 0, err 31 | } 32 | return id, nil 33 | } 34 | 35 | // PeekReady gets a copy of the job at the front of t's ready queue. 36 | func (t *Tube) PeekReady() (id uint64, body []byte, err error) { 37 | r, err := t.Conn.cmd(t, nil, nil, "peek-ready") 38 | if err != nil { 39 | return 0, nil, err 40 | } 41 | body, err = t.Conn.readResp(r, true, "FOUND %d", &id) 42 | if err != nil { 43 | return 0, nil, err 44 | } 45 | return id, body, nil 46 | } 47 | 48 | // PeekDelayed gets a copy of the delayed job that is next to be 49 | // put in t's ready queue. 50 | func (t *Tube) PeekDelayed() (id uint64, body []byte, err error) { 51 | r, err := t.Conn.cmd(t, nil, nil, "peek-delayed") 52 | if err != nil { 53 | return 0, nil, err 54 | } 55 | body, err = t.Conn.readResp(r, true, "FOUND %d", &id) 56 | if err != nil { 57 | return 0, nil, err 58 | } 59 | return id, body, nil 60 | } 61 | 62 | // PeekBuried gets a copy of the job in the holding area that would 63 | // be kicked next by Kick. 64 | func (t *Tube) PeekBuried() (id uint64, body []byte, err error) { 65 | r, err := t.Conn.cmd(t, nil, nil, "peek-buried") 66 | if err != nil { 67 | return 0, nil, err 68 | } 69 | body, err = t.Conn.readResp(r, true, "FOUND %d", &id) 70 | if err != nil { 71 | return 0, nil, err 72 | } 73 | return id, body, nil 74 | } 75 | 76 | // Kick takes up to bound jobs from the holding area and moves them into 77 | // the ready queue, then returns the number of jobs moved. Jobs will be 78 | // taken in the order in which they were last buried. 79 | func (t *Tube) Kick(bound int) (n int, err error) { 80 | r, err := t.Conn.cmd(t, nil, nil, "kick", bound) 81 | if err != nil { 82 | return 0, err 83 | } 84 | _, err = t.Conn.readResp(r, false, "KICKED %d", &n) 85 | if err != nil { 86 | return 0, err 87 | } 88 | return n, nil 89 | } 90 | 91 | // Stats retrieves statistics about tube t. 92 | func (t *Tube) Stats() (map[string]string, error) { 93 | r, err := t.Conn.cmd(nil, nil, nil, "stats-tube", t.Name) 94 | if err != nil { 95 | return nil, err 96 | } 97 | body, err := t.Conn.readResp(r, true, "OK") 98 | return parseDict(body), err 99 | } 100 | 101 | // Pause pauses new reservations in t for time d. 102 | func (t *Tube) Pause(d time.Duration) error { 103 | r, err := t.Conn.cmd(nil, nil, nil, "pause-tube", t.Name, dur(d)) 104 | if err != nil { 105 | return err 106 | } 107 | _, err = t.Conn.readResp(r, false, "PAUSED") 108 | if err != nil { 109 | return err 110 | } 111 | return nil 112 | } 113 | -------------------------------------------------------------------------------- /tube_test.go: -------------------------------------------------------------------------------- 1 | package beanstalk 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestTubePut(t *testing.T) { 9 | c := NewConn(mock("put 0 0 0 3\r\nfoo\r\n", "INSERTED 1\r\n")) 10 | 11 | id, err := c.Put([]byte("foo"), 0, 0, 0) 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | if id != 1 { 16 | t.Fatal("expected 1, got", id) 17 | } 18 | if err = c.Close(); err != nil { 19 | t.Fatal(err) 20 | } 21 | } 22 | 23 | func TestTubePeekReady(t *testing.T) { 24 | c := NewConn(mock("peek-ready\r\n", "FOUND 1 1\r\nx\r\n")) 25 | 26 | id, body, err := c.PeekReady() 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | if id != 1 { 31 | t.Fatal("expected 1, got", id) 32 | } 33 | if len(body) != 1 || body[0] != 'x' { 34 | t.Fatalf("bad body, expected %#v, got %#v", "x", string(body)) 35 | } 36 | if err = c.Close(); err != nil { 37 | t.Fatal(err) 38 | } 39 | } 40 | 41 | func TestTubePeekDelayed(t *testing.T) { 42 | c := NewConn(mock("peek-delayed\r\n", "FOUND 1 1\r\nx\r\n")) 43 | 44 | id, body, err := c.PeekDelayed() 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | if id != 1 { 49 | t.Fatal("expected 1, got", id) 50 | } 51 | if len(body) != 1 || body[0] != 'x' { 52 | t.Fatalf("bad body, expected %#v, got %#v", "x", string(body)) 53 | } 54 | if err = c.Close(); err != nil { 55 | t.Fatal(err) 56 | } 57 | } 58 | 59 | func TestTubePeekBuried(t *testing.T) { 60 | c := NewConn(mock("peek-buried\r\n", "FOUND 1 1\r\nx\r\n")) 61 | 62 | id, body, err := c.PeekBuried() 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | if id != 1 { 67 | t.Fatal("expected 1, got", id) 68 | } 69 | if len(body) != 1 || body[0] != 'x' { 70 | t.Fatalf("bad body, expected %#v, got %#v", "x", string(body)) 71 | } 72 | if err = c.Close(); err != nil { 73 | t.Fatal(err) 74 | } 75 | } 76 | 77 | func TestTubeKick(t *testing.T) { 78 | c := NewConn(mock("kick 2\r\n", "KICKED 1\r\n")) 79 | 80 | n, err := c.Kick(2) 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | if n != 1 { 85 | t.Fatal("expected 1, got", n) 86 | } 87 | if err = c.Close(); err != nil { 88 | t.Fatal(err) 89 | } 90 | } 91 | 92 | func TestTubeStats(t *testing.T) { 93 | c := NewConn(mock("stats-tube default\r\n", "OK 10\r\n---\na: ok\n\r\n")) 94 | 95 | m, err := c.Tube.Stats() 96 | if err != nil { 97 | t.Fatal(err) 98 | } 99 | if len(m) != 1 || m["a"] != "ok" { 100 | t.Fatalf("expected %#v, got %#v", map[string]string{"a": "ok"}, m) 101 | } 102 | if err = c.Close(); err != nil { 103 | t.Fatal(err) 104 | } 105 | } 106 | 107 | func TestTubePause(t *testing.T) { 108 | c := NewConn(mock("pause-tube default 5\r\n", "PAUSED\r\n")) 109 | 110 | err := c.Pause(5 * time.Second) 111 | if err != nil { 112 | t.Fatal(err) 113 | } 114 | if err = c.Close(); err != nil { 115 | t.Fatal(err) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tubeset.go: -------------------------------------------------------------------------------- 1 | package beanstalk 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // TubeSet represents a set of tubes on the server connected to by Conn. 8 | // Name names the tubes represented. 9 | type TubeSet struct { 10 | Conn *Conn 11 | Name map[string]bool 12 | } 13 | 14 | // NewTubeSet returns a new TubeSet representing the given names. 15 | func NewTubeSet(c *Conn, name ...string) *TubeSet { 16 | ts := &TubeSet{c, make(map[string]bool)} 17 | for _, s := range name { 18 | ts.Name[s] = true 19 | } 20 | return ts 21 | } 22 | 23 | // Reserve reserves and returns a job from one of the tubes in t. If no 24 | // job is available before time timeout has passed, Reserve returns a 25 | // ConnError recording ErrTimeout. 26 | // 27 | // Typically, a client will reserve a job, perform some work, then delete 28 | // the job with Conn.Delete. 29 | func (t *TubeSet) Reserve(timeout time.Duration) (id uint64, body []byte, err error) { 30 | r, err := t.Conn.cmd(nil, t, nil, "reserve-with-timeout", dur(timeout)) 31 | if err != nil { 32 | return 0, nil, err 33 | } 34 | body, err = t.Conn.readResp(r, true, "RESERVED %d", &id) 35 | if err != nil { 36 | return 0, nil, err 37 | } 38 | return id, body, nil 39 | } 40 | -------------------------------------------------------------------------------- /tubeset_test.go: -------------------------------------------------------------------------------- 1 | package beanstalk 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestTubeSetReserve(t *testing.T) { 9 | c := NewConn(mock("reserve-with-timeout 1\r\n", "RESERVED 1 1\r\nx\r\n")) 10 | id, body, err := c.Reserve(time.Second) 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | if id != 1 { 15 | t.Fatal("expected 1, got", id) 16 | } 17 | if len(body) != 1 || body[0] != 'x' { 18 | t.Fatalf("bad body, expected %#v, got %#v", "x", string(body)) 19 | } 20 | if err = c.Close(); err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | 25 | func TestTubeSetReserveTimeout(t *testing.T) { 26 | c := NewConn(mock("reserve-with-timeout 1\r\n", "TIMED_OUT\r\n")) 27 | _, _, err := c.Reserve(time.Second) 28 | if cerr, ok := err.(ConnError); !ok { 29 | t.Log(err) 30 | t.Logf("%#v", err) 31 | t.Fatal("expected ConnError") 32 | } else if cerr.Err != ErrTimeout { 33 | t.Log(err) 34 | t.Logf("%#v", err) 35 | t.Fatal("expected ErrTimeout") 36 | } 37 | if err = c.Close(); err != nil { 38 | t.Fatal(err) 39 | } 40 | } 41 | --------------------------------------------------------------------------------