├── circle.yml ├── .gitignore ├── cli ├── put_test.go ├── kick_test.go ├── peek.go ├── command_test.go ├── put.go ├── kick.go ├── delete_test.go ├── delete.go ├── bury.go ├── bury_test.go ├── tail.go ├── command.go └── stats.go ├── LICENSE ├── beanstool.go ├── Makefile └── README.md /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | services: 3 | - beanstalkd 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /cli/put_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/kr/beanstalk" 7 | . "gopkg.in/check.v1" 8 | ) 9 | 10 | type PutCommandSuite struct { 11 | c *PutCommand 12 | t *beanstalk.Tube 13 | } 14 | 15 | var _ = Suite(&PutCommandSuite{}) 16 | 17 | func (s *PutCommandSuite) SetUpTest(c *C) { 18 | s.c = &PutCommand{} 19 | s.c.Host = "localhost:11300" 20 | s.c.Init() 21 | 22 | s.t = getRandomTube(s.c.conn) 23 | s.c.Tube = s.t.Name 24 | } 25 | 26 | func (s *PutCommandSuite) TestPutCommand_Put(c *C) { 27 | s.c.Body = "foo" 28 | s.c.Priority = 1024 29 | s.c.Delay = time.Second 30 | 31 | err := s.c.Put() 32 | c.Assert(err, IsNil) 33 | 34 | stats, _ := s.c.GetStatsForTube(s.c.Tube) 35 | c.Assert(stats.JobsDelayed, Equals, 1) 36 | } 37 | -------------------------------------------------------------------------------- /cli/kick_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/kr/beanstalk" 5 | . "gopkg.in/check.v1" 6 | ) 7 | 8 | type KickCommandSuite struct { 9 | c *KickCommand 10 | t *beanstalk.Tube 11 | } 12 | 13 | var _ = Suite(&KickCommandSuite{}) 14 | 15 | func (s *KickCommandSuite) SetUpTest(c *C) { 16 | s.c = &KickCommand{} 17 | s.c.Host = "localhost:11300" 18 | s.c.Init() 19 | 20 | s.t = getRandomTube(s.c.conn) 21 | s.c.Tube = s.t.Name 22 | } 23 | 24 | func (s *KickCommandSuite) TestKickCommand_Kick(c *C) { 25 | s.t.Put([]byte(""), 1024, 0, 0) 26 | 27 | bury := &BuryCommand{} 28 | bury.conn = s.c.conn 29 | bury.Tube = s.c.Tube 30 | bury.Bury() 31 | 32 | stats, _ := s.c.GetStatsForTube(s.c.Tube) 33 | c.Assert(stats.JobsBuried, Equals, 1) 34 | 35 | err := s.c.Kick() 36 | c.Assert(err, IsNil) 37 | 38 | stats, _ = s.c.GetStatsForTube(s.c.Tube) 39 | c.Assert(stats.JobsBuried, Equals, 0) 40 | c.Assert(stats.JobsReady, Equals, 1) 41 | } 42 | -------------------------------------------------------------------------------- /cli/peek.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/kr/beanstalk" 5 | ) 6 | 7 | type PeekCommand struct { 8 | Tube string `short:"t" long:"tube" description:"tube to be tailed." required:"true"` 9 | State string `short:"" long:"state" description:"peek from 'buried', 'ready' or 'delayed' queues." default:"buried"` 10 | Command 11 | } 12 | 13 | func (c *PeekCommand) Execute(args []string) error { 14 | if err := c.Init(); err != nil { 15 | return err 16 | } 17 | 18 | return c.Peek() 19 | } 20 | 21 | func (c *PeekCommand) Peek() error { 22 | t := &beanstalk.Tube{Conn: c.conn, Name: c.Tube} 23 | var id uint64 24 | var body []byte 25 | var err error 26 | 27 | switch c.State { 28 | case "buried": 29 | id, body, err = t.PeekBuried() 30 | case "ready": 31 | id, body, err = t.PeekReady() 32 | case "delayed": 33 | id, body, err = t.PeekDelayed() 34 | } 35 | 36 | if err != nil { 37 | return err 38 | } 39 | 40 | c.PrintJob(id, body) 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /cli/command_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/kr/beanstalk" 10 | . "gopkg.in/check.v1" 11 | ) 12 | 13 | func Test(t *testing.T) { TestingT(t) } 14 | 15 | type CommandSuite struct{} 16 | 17 | var _ = Suite(&CommandSuite{}) 18 | 19 | func (s *CommandSuite) TestCommand_GetStatsForTube(c *C) { 20 | cmd := &Command{Host: "localhost:11300"} 21 | cmd.Init() 22 | 23 | tube := getRandomTube(cmd.conn) 24 | tube.Put([]byte(""), 1024, 0, 0) 25 | 26 | stats, err := cmd.GetStatsForTube(tube.Name) 27 | c.Assert(err, IsNil) 28 | c.Assert(stats.JobsReady, Equals, 1) 29 | c.Assert(stats.JobsBuried, Equals, 0) 30 | } 31 | 32 | func getRandomTube(conn *beanstalk.Conn) *beanstalk.Tube { 33 | rb := make([]byte, 32) 34 | if _, err := rand.Read(rb); err != nil { 35 | panic(err) 36 | } 37 | 38 | name := strings.Replace(base64.URLEncoding.EncodeToString(rb), "=", "0", -1) 39 | 40 | return &beanstalk.Tube{conn, name} 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 source{d} 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 | 23 | -------------------------------------------------------------------------------- /beanstool.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/agtorre/gocolorize" 7 | "github.com/jessevdk/go-flags" 8 | "github.com/src-d/beanstool/cli" 9 | "golang.org/x/crypto/ssh/terminal" 10 | ) 11 | 12 | func main() { 13 | if !terminal.IsTerminal(int(os.Stdout.Fd())) { 14 | gocolorize.SetPlain(true) 15 | } 16 | 17 | parser := flags.NewNamedParser("beanstool", flags.Default) 18 | parser.AddCommand("stats", "print stats on all tubes", "", &cli.StatsCommand{}) 19 | parser.AddCommand("tail", "tails a tube and prints his content", "", &cli.TailCommand{}) 20 | parser.AddCommand("peek", "peeks a job from a queue", "", &cli.PeekCommand{}) 21 | parser.AddCommand("delete", "delete a job from a queue", "", &cli.DeleteCommand{}) 22 | parser.AddCommand("kick", "kicks jobs from buried back into ready", "", &cli.KickCommand{}) 23 | parser.AddCommand("put", "put a job into a tube", "", &cli.PutCommand{}) 24 | parser.AddCommand("bury", "bury existing jobs from ready state", "", &cli.BuryCommand{}) 25 | 26 | _, err := parser.Parse() 27 | if err != nil { 28 | if flagErr, ok := err.(*flags.Error); ok && flagErr.Type != flags.ErrHelp { 29 | parser.WriteHelp(os.Stdout) 30 | } 31 | 32 | os.Exit(1) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /cli/put.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/kr/beanstalk" 8 | ) 9 | 10 | type PutCommand struct { 11 | Tube string `short:"t" long:"tube" description:"tube to be tailed." required:"true"` 12 | Body string `short:"b" long:"body" description:"plain text data for the job." required:"true"` 13 | Priority uint32 `short:"" long:"priority" description:"priority for the job." default:"1024"` 14 | Delay time.Duration `short:"" long:"delay" description:"delay for the job." default:"0"` 15 | TTR time.Duration `short:"" long:"ttr" description:"TTR for the job." default:"60"` 16 | 17 | Command 18 | } 19 | 20 | func (c *PutCommand) Execute(args []string) error { 21 | if err := c.Init(); err != nil { 22 | return err 23 | } 24 | 25 | return c.Put() 26 | } 27 | 28 | func (c *PutCommand) Put() error { 29 | t := beanstalk.Tube{Conn: c.conn, Name: c.Tube} 30 | 31 | id, err := t.Put([]byte(c.Body), c.Priority, c.Delay, c.TTR) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | fmt.Printf( 37 | "Added job with id %d to %s with priority %d, delay %s, TTR %d\n", 38 | id, c.Tube, c.Priority, c.Delay.String(), c.TTR, 39 | ) 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /cli/kick.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kr/beanstalk" 7 | ) 8 | 9 | type KickCommand struct { 10 | Tube string `short:"t" long:"tube" description:"tube to kick jobs in." required:"true"` 11 | Num int `short:"" long:"num" description:"number of jobs to kick."` 12 | Command 13 | } 14 | 15 | func (c *KickCommand) Execute(args []string) error { 16 | if err := c.Init(); err != nil { 17 | return err 18 | } 19 | 20 | return c.Kick() 21 | } 22 | 23 | func (c *KickCommand) Kick() error { 24 | if err := c.calcNumIfNeeded(); err != nil { 25 | return err 26 | } 27 | 28 | if c.Num == 0 { 29 | fmt.Printf("Empty buried queue at tube %q.\n", c.Tube) 30 | return nil 31 | } 32 | 33 | fmt.Printf("Trying to kick %d jobs from %q ...\n", c.Num, c.Tube) 34 | 35 | t := &beanstalk.Tube{Conn: c.conn, Name: c.Tube} 36 | kicked, err := t.Kick(c.Num) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | fmt.Printf("Actually kicked %d.\n", kicked) 42 | return nil 43 | } 44 | 45 | func (c *KickCommand) calcNumIfNeeded() error { 46 | if c.Num == 0 { 47 | s, err := c.GetStatsForTube(c.Tube) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | c.Num = s.JobsBuried 53 | } 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /cli/delete_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/kr/beanstalk" 5 | . "gopkg.in/check.v1" 6 | ) 7 | 8 | type DeleteCommandSuite struct { 9 | c *DeleteCommand 10 | t *beanstalk.Tube 11 | } 12 | 13 | var _ = Suite(&DeleteCommandSuite{}) 14 | 15 | func (s *DeleteCommandSuite) SetUpTest(c *C) { 16 | s.c = &DeleteCommand{} 17 | s.c.Host = "localhost:11300" 18 | s.c.Init() 19 | 20 | s.t = getRandomTube(s.c.conn) 21 | s.c.Tube = s.t.Name 22 | } 23 | 24 | func (s *DeleteCommandSuite) TestDeleteReady(c *C) { 25 | s.t.Put([]byte(""), 1024, 0, 0) 26 | s.t.Put([]byte(""), 1024, 0, 0) 27 | s.t.Put([]byte(""), 1024, 0, 0) 28 | 29 | stats, _ := s.c.GetStatsForTube(s.c.Tube) 30 | c.Assert(stats.JobsReady, Equals, 3) 31 | 32 | s.c.Empty = false 33 | s.c.State = "ready" 34 | 35 | err := s.c.Delete() 36 | c.Assert(err, IsNil) 37 | 38 | stats, _ = s.c.GetStatsForTube(s.c.Tube) 39 | c.Assert(stats.JobsReady, Equals, 2) 40 | } 41 | 42 | func (s *DeleteCommandSuite) TestDeleteReadyEmpty(c *C) { 43 | s.t.Put([]byte(""), 1024, 0, 0) 44 | s.t.Put([]byte(""), 1024, 0, 0) 45 | s.t.Put([]byte(""), 1024, 0, 0) 46 | 47 | stats, _ := s.c.GetStatsForTube(s.c.Tube) 48 | c.Assert(stats.JobsReady, Equals, 3) 49 | 50 | s.c.Empty = true 51 | s.c.State = "ready" 52 | 53 | err := s.c.Delete() 54 | c.Assert(err, IsNil) 55 | 56 | stats, _ = s.c.GetStatsForTube(s.c.Tube) 57 | c.Assert(stats.JobsReady, Equals, 0) 58 | } 59 | -------------------------------------------------------------------------------- /cli/delete.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/kr/beanstalk" 5 | ) 6 | 7 | type DeleteCommand struct { 8 | Tube string `short:"t" long:"tube" description:"tube to be delete." required:"true"` 9 | State string `short:"" long:"state" description:"peek from 'buried', 'ready' or 'delayed' queues." default:"buried"` 10 | Print bool `short:"" long:"print" description:"prints the jobs after delete it."` 11 | Empty bool `short:"" long:"empty" description:"delete all jobs with the given status in the given tube."` 12 | Command 13 | } 14 | 15 | func (c *DeleteCommand) Execute(args []string) error { 16 | if err := c.Init(); err != nil { 17 | return err 18 | } 19 | 20 | return c.Delete() 21 | } 22 | 23 | func (c *DeleteCommand) Delete() error { 24 | t := &beanstalk.Tube{Conn: c.conn, Name: c.Tube} 25 | for { 26 | if err := c.deleteJob(t); err != nil { 27 | if err.Error() == "peek-ready: not found" { 28 | return nil 29 | } 30 | 31 | return err 32 | } 33 | 34 | if !c.Empty { 35 | return nil 36 | } 37 | } 38 | } 39 | 40 | func (c *DeleteCommand) deleteJob(t *beanstalk.Tube) error { 41 | var id uint64 42 | var body []byte 43 | var err error 44 | 45 | switch c.State { 46 | case "buried": 47 | id, body, err = t.PeekBuried() 48 | case "ready": 49 | id, body, err = t.PeekReady() 50 | case "delayed": 51 | id, body, err = t.PeekDelayed() 52 | } 53 | 54 | if err != nil { 55 | return err 56 | } 57 | 58 | if c.Print { 59 | c.PrintJob(id, body) 60 | } 61 | 62 | c.conn.Delete(id) 63 | 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /cli/bury.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/kr/beanstalk" 9 | ) 10 | 11 | type BuryCommand struct { 12 | Tube string `short:"t" long:"tube" description:"tube to bury jobs in." required:"true"` 13 | Num int `short:"" long:"num" description:"number of jobs to bury."` 14 | Command 15 | } 16 | 17 | func (c *BuryCommand) Execute(args []string) error { 18 | if err := c.Init(); err != nil { 19 | return err 20 | } 21 | 22 | return c.Bury() 23 | } 24 | 25 | func (c *BuryCommand) Bury() error { 26 | if err := c.calcNum(); err != nil { 27 | return err 28 | } 29 | 30 | if c.Num == 0 { 31 | fmt.Printf("Empty ready queue at tube %q.\n", c.Tube) 32 | return nil 33 | } 34 | 35 | fmt.Printf("Trying to bury %d jobs from %q ...\n", c.Num, c.Tube) 36 | 37 | count := 0 38 | ts := beanstalk.NewTubeSet(c.conn, c.Tube) 39 | for count < c.Num { 40 | id, _, err := ts.Reserve(time.Second) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | s, err := c.conn.StatsJob(id) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | pri, err := strconv.ParseUint(s["pri"], 10, 32) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | if err := c.conn.Bury(id, uint32(pri)); err != nil { 56 | return err 57 | } 58 | 59 | count++ 60 | } 61 | 62 | fmt.Printf("Actually buried %d.\n", count) 63 | return nil 64 | } 65 | 66 | func (c *BuryCommand) calcNum() error { 67 | s, err := c.GetStatsForTube(c.Tube) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | if c.Num == 0 || c.Num > s.JobsReady { 73 | c.Num = s.JobsReady 74 | } 75 | 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /cli/bury_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/kr/beanstalk" 5 | . "gopkg.in/check.v1" 6 | ) 7 | 8 | type BuryCommandSuite struct { 9 | c *BuryCommand 10 | t *beanstalk.Tube 11 | } 12 | 13 | var _ = Suite(&BuryCommandSuite{}) 14 | 15 | func (s *BuryCommandSuite) SetUpTest(c *C) { 16 | s.c = &BuryCommand{} 17 | s.c.Host = "localhost:11300" 18 | s.c.Init() 19 | 20 | s.t = getRandomTube(s.c.conn) 21 | s.c.Tube = s.t.Name 22 | } 23 | 24 | func (s *BuryCommandSuite) TestBuryCommand_Bury(c *C) { 25 | s.t.Put([]byte(""), 1024, 0, 0) 26 | 27 | err := s.c.Bury() 28 | c.Assert(err, IsNil) 29 | 30 | stats, _ := s.c.GetStatsForTube(s.c.Tube) 31 | c.Assert(stats.JobsBuried, Equals, 1) 32 | } 33 | 34 | func (s *BuryCommandSuite) TestBuryCommand_BuryWithLimitUnder(c *C) { 35 | s.t.Put([]byte(""), 1024, 0, 0) 36 | s.t.Put([]byte(""), 1024, 0, 0) 37 | 38 | s.c.Num = 1 39 | err := s.c.Bury() 40 | c.Assert(err, IsNil) 41 | 42 | stats, _ := s.c.GetStatsForTube(s.c.Tube) 43 | c.Assert(stats.JobsBuried, Equals, 1) 44 | c.Assert(stats.JobsReady, Equals, 1) 45 | } 46 | 47 | func (s *BuryCommandSuite) TestBuryCommand_BuryWithLimitOver(c *C) { 48 | s.t.Put([]byte(""), 1024, 0, 0) 49 | 50 | s.c.Num = 10 51 | err := s.c.Bury() 52 | c.Assert(err, IsNil) 53 | 54 | stats, _ := s.c.GetStatsForTube(s.c.Tube) 55 | c.Assert(stats.JobsBuried, Equals, 1) 56 | c.Assert(stats.JobsReady, Equals, 0) 57 | } 58 | 59 | func (s *BuryCommandSuite) TestBuryCommand_BuryWithPriorityBeyondInt32(c *C) { 60 | s.t.Put([]byte(""), 2147483648, 0, 0) 61 | 62 | err := s.c.Bury() 63 | c.Assert(err, IsNil) 64 | 65 | stats, _ := s.c.GetStatsForTube(s.c.Tube) 66 | c.Assert(stats.JobsBuried, Equals, 1) 67 | } 68 | -------------------------------------------------------------------------------- /cli/tail.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/kr/beanstalk" 10 | ) 11 | 12 | var TooManyErrorsError = errors.New("Too many errors") 13 | 14 | type TailCommand struct { 15 | Tube string `short:"t" long:"tube" description:"tube to be tailed." required:"true"` 16 | Action string `short:"" long:"action" description:"action to perform after reserver the job. (release, bury, delete)" default:"release"` 17 | 18 | Command 19 | } 20 | 21 | func (c *TailCommand) Execute(args []string) error { 22 | if err := c.Init(); err != nil { 23 | return err 24 | } 25 | 26 | return c.Tail() 27 | } 28 | 29 | func (c *TailCommand) Tail() error { 30 | ts := beanstalk.NewTubeSet(c.conn, c.Tube) 31 | 32 | errors := 0 33 | for { 34 | if errors > 100 { 35 | return TooManyErrorsError 36 | } 37 | 38 | id, body, err := ts.Reserve(time.Hour * 24) 39 | if err != nil { 40 | if err.Error() != "reserve-with-timeout: deadline soon" { 41 | errors++ 42 | fmt.Println("Error", err) 43 | } 44 | 45 | continue 46 | } 47 | 48 | if err := c.PrintJob(id, body); err != nil { 49 | errors++ 50 | fmt.Println("Error", err) 51 | continue 52 | } 53 | 54 | if err := c.postPrintAction(id); err != nil { 55 | return err 56 | } 57 | 58 | fmt.Println(strings.Repeat("-", 80)) 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func (c *TailCommand) postPrintAction(id uint64) error { 65 | var err error 66 | 67 | switch c.Action { 68 | case "release": 69 | err = c.conn.Release(id, 1024, 0) 70 | case "bury": 71 | err = c.conn.Bury(id, 1024) 72 | case "delete": 73 | err = c.conn.Delete(id) 74 | } 75 | 76 | return err 77 | } 78 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Package configuration 2 | PROJECT = beanstool 3 | COMMANDS = beanstool 4 | DEPENDENCIES = 5 | 6 | # Environment 7 | BASE_PATH := $(shell pwd) 8 | BUILD_PATH := $(BASE_PATH)/build 9 | VERSION ?= $(shell git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/') 10 | BUILD ?= $(shell date) 11 | ASSETS := static 12 | 13 | # PACKAGES 14 | PKG_OS = darwin linux 15 | PKG_ARCH = amd64 16 | PKG_CONTENT = LICENSE 17 | 18 | # Go parameters 19 | GOCMD = go 20 | GOBUILD = $(GOCMD) build 21 | GOCLEAN = $(GOCMD) clean 22 | GOGET = $(GOCMD) get 23 | GOTEST = $(GOCMD) test 24 | 25 | # Rules 26 | all: test build 27 | 28 | build: dependencies 29 | for cmd in $(COMMANDS); do \ 30 | $(GOCMD) build -ldflags "-X main.version $(VERSION) -X main.build \"$(BUILD)\"" $${cmd}.go; \ 31 | done 32 | 33 | test: dependencies 34 | cd $(BASE_PATH); $(GOTEST) -v ./... 35 | 36 | dependencies: 37 | $(GOGET) -d -v ./... 38 | for i in $(DEPENDENCIES); do $(GOGET) $$i; done 39 | 40 | install: 41 | for cmd in $(COMMANDS); do \ 42 | cp -rf $${cmd} /usr/bin/; \ 43 | done 44 | 45 | packages: clean 46 | for os in $(PKG_OS); do \ 47 | for arch in $(PKG_ARCH); do \ 48 | cd $(BASE_PATH); \ 49 | mkdir -p $(BUILD_PATH)/$(PROJECT)_$(VERSION)_$${os}_$${arch}; \ 50 | for cmd in $(COMMANDS); do \ 51 | GOOS=$${os} GOARCH=$${arch} $(GOCMD) build -ldflags "-X main.version $(VERSION) -X main.build \"$(BUILD)\"" -o $(BUILD_PATH)/$(PROJECT)_$(VERSION)_$${os}_$${arch}/$${cmd} $${cmd}.go ; \ 52 | done; \ 53 | for content in $(PKG_CONTENT); do \ 54 | cp -rf $${content} $(BUILD_PATH)/$(PROJECT)_$(VERSION)_$${os}_$${arch}/; \ 55 | done; \ 56 | cd $(BUILD_PATH) && tar -cvzf $(BUILD_PATH)/$(PROJECT)_$(VERSION)_$${os}_$${arch}.tar.gz $(PROJECT)_$(VERSION)_$${os}_$${arch}/; \ 57 | done; \ 58 | done; 59 | 60 | clean: 61 | echo $(VERSION) 62 | rm -rf $(BUILD_PATH) 63 | 64 | $(GOCLEAN) . 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | beanstool [![Circle CI](https://circleci.com/gh/src-d/beanstool.svg?style=svg)](https://circleci.com/gh/src-d/beanstool) 2 | ============================== 3 | 4 | Dependency free [beanstalkd](http://kr.github.io/beanstalkd/) admin tool. 5 | 6 | Basically this is a rework of the wonderful [bsTools](https://github.com/jimbojsb/bstools) with some extra features and of course without need to install any dependency. Very useful in companion of the server in a small docker container. 7 | 8 | Installation 9 | ------------ 10 | 11 | ``` 12 | wget https://github.com/src-d/beanstool/releases/download/v0.2.0/beanstool_v0.2.0_linux_amd64.tar.gz 13 | tar -xvzf beanstool_v0.2.0_linux_amd64.tar.gz 14 | cp beanstool_v0.2.0_linux_amd64/beanstool /usr/local/bin/ 15 | ``` 16 | 17 | browse the [`releases`](https://github.com/tyba/beanstool/releases) section to see other archs and versions 18 | 19 | 20 | Usage 21 | ----- 22 | 23 | ```sh 24 | Usage: 25 | beanstool [OPTIONS] 26 | 27 | Help Options: 28 | -h, --help Show this help message 29 | 30 | Available commands: 31 | bury bury existing jobs from ready state 32 | kick kicks jobs from buried back into ready 33 | delete a job from a queue 34 | peek peeks a job from a queue 35 | put put a job into a tube 36 | stats print stats on all tubes 37 | tail tails a tube and prints his content 38 | ``` 39 | 40 | As example this is the output of the command `./beanstool stats`: 41 | 42 | ``` 43 | +---------+----------+----------+----------+----------+----------+---------+-------+ 44 | | Name | Buried | Delayed | Ready | Reserved | Urgent | Waiting | Total | 45 | +---------+----------+----------+----------+----------+----------+---------+-------+ 46 | | foo | 20 | 0 | 5 | 0 | 0 | 0 | 28 | 47 | | default | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 48 | +---------+----------+----------+----------+----------+----------+---------+-------+ 49 | ``` 50 | 51 | License 52 | ------- 53 | 54 | MIT, see [LICENSE](LICENSE) 55 | -------------------------------------------------------------------------------- /cli/command.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/agtorre/gocolorize" 7 | "github.com/kr/beanstalk" 8 | ) 9 | 10 | var TitleStyle = gocolorize.NewColor("green") 11 | var InfoStyle = gocolorize.NewColor("yellow") 12 | 13 | type Command struct { 14 | Host string `short:"h" long:"host" description:"beanstalkd host addr." required:"true" default:"localhost:11300"` 15 | 16 | conn *beanstalk.Conn 17 | } 18 | 19 | func (c *Command) Init() error { 20 | var err error 21 | c.conn, err = beanstalk.Dial("tcp", c.Host) 22 | return err 23 | } 24 | 25 | func (c *Command) PrintJob(id uint64, body []byte) error { 26 | s, err := c.conn.StatsJob(id) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | fmt.Printf( 32 | "%s: %d, %s: %d, %s: %s, %s: %s, %s: %s, %s: %s\n"+ 33 | "%s: %s, %s: %s, %s: %s, %s: %s, %s: %s\n"+ 34 | "%s:\n%q\n", 35 | TitleStyle.Paint("id"), id, 36 | TitleStyle.Paint("length"), len(body), 37 | TitleStyle.Paint("priority"), s["pri"], 38 | TitleStyle.Paint("delay"), s["delay"], 39 | TitleStyle.Paint("age"), s["age"], 40 | TitleStyle.Paint("ttr"), s["ttr"], 41 | 42 | InfoStyle.Paint("reserves"), s["reserves"], 43 | InfoStyle.Paint("releases"), s["releases"], 44 | InfoStyle.Paint("buries"), s["buries"], 45 | InfoStyle.Paint("kicks"), s["kicks"], 46 | InfoStyle.Paint("timeouts"), s["timeouts"], 47 | 48 | InfoStyle.Paint("body"), body, 49 | ) 50 | 51 | return nil 52 | } 53 | 54 | func (c *Command) GetStatsForTube(tube string) (*TubeStats, error) { 55 | t := &beanstalk.Tube{Conn: c.conn, Name: tube} 56 | s, err := t.Stats() 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | if name, ok := s["name"]; !ok || name != tube { 62 | return nil, TubeStatsRetrievalError 63 | } 64 | 65 | return &TubeStats{ 66 | JobsBuried: mustConvertToInt(s["current-jobs-buried"]), 67 | JobsReady: mustConvertToInt(s["current-jobs-ready"]), 68 | JobsDelayed: mustConvertToInt(s["current-jobs-delayed"]), 69 | JobsReserved: mustConvertToInt(s["current-jobs-reserved"]), 70 | JobsUrgent: mustConvertToInt(s["current-jobs-urgent"]), 71 | Waiting: mustConvertToInt(s["current-waiting"]), 72 | TotalJobs: mustConvertToInt(s["total-jobs"]), 73 | }, nil 74 | } 75 | 76 | type TubeStats struct { 77 | JobsBuried int 78 | JobsDelayed int 79 | JobsReady int 80 | JobsReserved int 81 | JobsUrgent int 82 | Waiting int 83 | TotalJobs int 84 | } 85 | -------------------------------------------------------------------------------- /cli/stats.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sort" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/agtorre/gocolorize" 11 | "github.com/stevedomin/termtable" 12 | ) 13 | 14 | const ( 15 | HighSeverity = iota 16 | NormalSeverity 17 | LowSeverity 18 | DefaultTube = "default" 19 | ) 20 | 21 | var TubeStatsRetrievalError = errors.New("Unable to retrieve tube stats") 22 | var HighSeverityStyle = gocolorize.NewColor("white:red") 23 | var NormalSeverityStyle = gocolorize.NewColor("green") 24 | 25 | type StatsCommand struct { 26 | Tubes string `short:"t" long:"tubes" description:"tubes to be listed (separated by ,). By default all are listed"` 27 | 28 | Command 29 | } 30 | 31 | func (c *StatsCommand) Execute(args []string) error { 32 | if err := c.Init(); err != nil { 33 | return err 34 | } 35 | 36 | return c.PrintStats() 37 | } 38 | 39 | func (c *StatsCommand) PrintStats() error { 40 | stats, err := c.GetStats() 41 | if err != nil { 42 | return err 43 | } 44 | 45 | table := termtable.NewTable(nil, &termtable.TableOptions{ 46 | Padding: 1, 47 | UseSeparator: true, 48 | }) 49 | 50 | table.SetHeader([]string{ 51 | "Name", "Buried", "Delayed", "Ready", "Reserved", "Urgent", "Waiting", "Total", 52 | }) 53 | 54 | table.AddRow(c.buildLineFromTubeStats(DefaultTube, stats[DefaultTube])) 55 | 56 | for _, t := range sortedKeys(stats) { 57 | if t == DefaultTube { 58 | continue 59 | } 60 | 61 | table.AddRow(c.buildLineFromTubeStats(t, stats[t])) 62 | } 63 | 64 | fmt.Println(table.Render()) 65 | return nil 66 | } 67 | 68 | func (c *StatsCommand) buildLineFromTubeStats(name string, s *TubeStats) []string { 69 | var l []string 70 | 71 | l = append(l, name) 72 | l = append(l, addStyle(s.JobsBuried, 8, HighSeverity)) 73 | l = append(l, addStyle(s.JobsDelayed, 8, NormalSeverity)) 74 | l = append(l, addStyle(s.JobsReady, 8, NormalSeverity)) 75 | l = append(l, addStyle(s.JobsReserved, 8, NormalSeverity)) 76 | l = append(l, addStyle(s.JobsUrgent, 8, NormalSeverity)) 77 | l = append(l, addStyle(s.Waiting, 8, LowSeverity)) 78 | l = append(l, addStyle(s.TotalJobs, 8, LowSeverity)) 79 | 80 | return l 81 | } 82 | 83 | func addStyle(i int, l int, severity int) string { 84 | value := strconv.Itoa(i) 85 | needs := l - len(value) 86 | if needs <= 0 { 87 | return value 88 | } 89 | 90 | padded := value + strings.Repeat(" ", needs) 91 | if i > 0 { 92 | switch severity { 93 | case HighSeverity: 94 | return HighSeverityStyle.Paint(padded) 95 | case NormalSeverity: 96 | return NormalSeverityStyle.Paint(padded) 97 | } 98 | } 99 | 100 | return padded 101 | } 102 | 103 | func (c *StatsCommand) GetStats() (map[string]*TubeStats, error) { 104 | tubes, err := c.getTubesName() 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | stats := make(map[string]*TubeStats, 0) 110 | for _, tube := range tubes { 111 | s, err := c.GetStatsForTube(tube) 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | stats[tube] = s 117 | } 118 | 119 | return stats, nil 120 | } 121 | 122 | func (c *StatsCommand) getTubesName() ([]string, error) { 123 | if c.Tubes != "" { 124 | return strings.Split(strings.Replace(c.Tubes, " ", "", -1), ","), nil 125 | } 126 | 127 | return c.conn.ListTubes() 128 | } 129 | 130 | func mustConvertToInt(s string) int { 131 | i, err := strconv.Atoi(s) 132 | if err != nil { 133 | panic(err) 134 | } 135 | 136 | return i 137 | } 138 | 139 | func sortedKeys(m map[string]*TubeStats) []string { 140 | keys := make([]string, len(m)) 141 | 142 | i := 0 143 | for key := range m { 144 | keys[i] = key 145 | i++ 146 | } 147 | 148 | sort.Strings(keys) 149 | return keys 150 | } 151 | --------------------------------------------------------------------------------