├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── client.go ├── client_test.go ├── doc.go ├── handler.go ├── handler_test.go ├── job.go ├── job_test.go ├── mux.go └── mux_test.go /.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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | before_install: 4 | - sudo apt-get update -qq 5 | - sudo apt-get install -y beanstalkd 6 | 7 | before_script: 8 | - beanstalkd & 9 | 10 | go: 11 | - tip 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mike Gleason jr Couturier 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # workers 2 | 3 | [![Build Status](https://travis-ci.org/mikegleasonjr/workers.svg?branch=master)](https://travis-ci.org/mikegleasonjr/workers) 4 | 5 | A simple beanstalk client library to consume jobs written in go. Heavily inspired from the standard `net/http` package. 6 | 7 | ## Install 8 | 9 | ``` 10 | $ go get github.com/mikegleasonjr/workers 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```go 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | "github.com/mikegleasonjr/workers" 21 | ) 22 | 23 | func main() { 24 | mux := workers.NewWorkMux() 25 | 26 | mux.Handle("tube1", workers.HandlerFunc(func(job *workers.Job) { 27 | fmt.Println("deleting job:", job.ID, job.Tube) 28 | job.Delete() 29 | })) 30 | 31 | mux.Handle("tube2", workers.HandlerFunc(func(job *workers.Job) { 32 | job.Bury(1000) 33 | })) 34 | 35 | workers.ConnectAndWork("tcp", "127.0.0.1:11300", mux) 36 | } 37 | ``` 38 | 39 | Or if you would like to consume jobs only on the `default` tube: 40 | 41 | ```go 42 | package main 43 | 44 | import ( 45 | "fmt" 46 | "github.com/mikegleasonjr/workers" 47 | ) 48 | 49 | func main() { 50 | workers.ConnectAndWork("tcp", "127.0.0.1:11300", workers.HandlerFunc(func(job *workers.Job) { 51 | fmt.Println("deleting job:", job.ID, job.Tube) 52 | job.Delete() 53 | })) 54 | } 55 | ``` 56 | 57 | ## Job Handlers 58 | 59 | Jobs are serviced each in their own goroutines. Jobs are handled in parallel as fast as they are reserved from the server. 60 | 61 | You can handle jobs by providing an object implementing the `Handler` interface: 62 | 63 | ```go 64 | type Handler interface { 65 | Work(*Job) 66 | } 67 | ``` 68 | Or use the `HandlerFunc` adapter as seen in the examples above. 69 | 70 | ## Stopping workers 71 | 72 | The client will disconnect itself from the beanstalk server and return upon receiving a `SIGINT` or a `SIGTERM` signal, waiting for current jobs to be handled. 73 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package workers 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net" 7 | "os" 8 | "os/signal" 9 | "sync" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/kr/beanstalk" 14 | ) 15 | 16 | // ErrClientHasQuit is returned by Client when it is quitting 17 | var ErrClientHasQuit = errors.New("client has quit") 18 | 19 | // Client defines parameters for running an beanstalk client. 20 | type Client struct { 21 | Network string 22 | Addr string 23 | Handler Handler 24 | mu sync.Mutex // guards stop 25 | stop chan error 26 | } 27 | 28 | // ConnectAndWork connects on the c.Network and c.Addr and then 29 | // calls Reserve to handle jobs on the beanstalk instance. 30 | func (c *Client) ConnectAndWork() error { 31 | conn, err := net.Dial(c.Network, c.Addr) 32 | 33 | if err != nil { 34 | return err 35 | } 36 | 37 | return c.Reserve(conn) 38 | } 39 | 40 | // ConnectAndWork creates a client, connects to the beanstalk instance and 41 | // reserves jobs to be processed by Handler. 42 | func ConnectAndWork(network string, addr string, handler Handler) error { 43 | client := &Client{Network: network, Addr: addr, Handler: handler} 44 | return client.ConnectAndWork() 45 | } 46 | 47 | // Reserve accepts incoming jobs on the beanstalk.Conn conn, creating a 48 | // new service goroutine for each. The service goroutines read the job and 49 | // then call c.Handler to process them. 50 | func (c *Client) Reserve(conn io.ReadWriteCloser) error { 51 | c.mu.Lock() 52 | c.stop = make(chan error) 53 | c.mu.Unlock() 54 | bs := beanstalk.NewConn(conn) 55 | tubes := c.tubes(bs) 56 | wg := &sync.WaitGroup{} 57 | wg.Add(1) 58 | go c.quitOnSignal(wg) 59 | 60 | defer bs.Close() 61 | defer wg.Wait() 62 | 63 | for { 64 | wait := time.Second // how long to sleep when no jobs in queues 65 | 66 | for name, tube := range tubes { 67 | id, body, err := tube.Reserve(0 /* don't block others */) 68 | if err == nil { 69 | wait = 0 // drain the queue as fast as possible 70 | wg.Add(1) 71 | go c.work(wg, NewJob(bs, name, id, body)) 72 | } else if !isTimeoutOrDeadline(err) { 73 | c.Stop() 74 | return err 75 | } 76 | select { 77 | case <-c.stop: 78 | return ErrClientHasQuit 79 | default: 80 | } 81 | } 82 | 83 | select { 84 | case <-c.stop: 85 | return ErrClientHasQuit 86 | case <-time.After(wait): 87 | } 88 | } 89 | } 90 | 91 | // Stop stops reserving jobs and wait for current workers to finish their job. 92 | func (c *Client) Stop() { 93 | c.mu.Lock() 94 | close(c.stop) 95 | c.mu.Unlock() 96 | } 97 | 98 | func (c *Client) tubes(conn *beanstalk.Conn) map[string]*beanstalk.TubeSet { 99 | names := []string{"default"} 100 | 101 | if mux, isMux := c.Handler.(*WorkMux); isMux { 102 | names = mux.Tubes() 103 | } 104 | 105 | tubes := make(map[string]*beanstalk.TubeSet, len(names)) 106 | for _, name := range names { 107 | tubes[name] = beanstalk.NewTubeSet(conn, name) 108 | } 109 | 110 | return tubes 111 | } 112 | 113 | func (c *Client) work(wg *sync.WaitGroup, j *Job) { 114 | defer wg.Done() 115 | c.Handler.Work(j) 116 | } 117 | 118 | func (c *Client) quitOnSignal(wg *sync.WaitGroup) { 119 | defer wg.Done() 120 | 121 | sigchan := make(chan os.Signal, 1) 122 | signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM) 123 | 124 | select { 125 | case <-c.stop: 126 | case <-sigchan: 127 | c.Stop() 128 | } 129 | } 130 | 131 | func isTimeoutOrDeadline(err error) bool { 132 | if connerr, isConnErr := err.(beanstalk.ConnError); isConnErr { 133 | return connerr.Op == "reserve-with-timeout" && 134 | (connerr.Err == beanstalk.ErrTimeout || connerr.Err == beanstalk.ErrDeadline) 135 | } 136 | 137 | return false 138 | } 139 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package workers 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strconv" 7 | "strings" 8 | "sync/atomic" 9 | "syscall" 10 | "testing" 11 | "time" 12 | 13 | "github.com/kr/beanstalk" 14 | ) 15 | 16 | type closedConnection struct{} 17 | 18 | func (i *closedConnection) Read(p []byte) (int, error) { 19 | return 0, io.EOF 20 | } 21 | 22 | func (i *closedConnection) Write(p []byte) (int, error) { 23 | return 0, io.EOF 24 | } 25 | 26 | func (i *closedConnection) Close() error { 27 | return nil 28 | } 29 | 30 | func Example() { 31 | mux := NewWorkMux() 32 | 33 | mux.Handle("tube1", HandlerFunc(func(job *Job) { 34 | fmt.Printf("processing job %d with content %v\n", job.ID, job.Body) 35 | job.Delete() 36 | })) 37 | 38 | mux.Handle("tube2", HandlerFunc(func(job *Job) { 39 | job.Release(0, 0) 40 | })) 41 | 42 | ConnectAndWork("tcp", "localhost:11300", mux) 43 | } 44 | 45 | func TestStopClient(t *testing.T) { 46 | client := &Client{ 47 | Network: "tcp", 48 | Addr: "localhost:11300", 49 | Handler: HandlerFunc(func(job *Job) { 50 | }), 51 | } 52 | 53 | go func() { 54 | time.Sleep(100 * time.Millisecond) 55 | client.Stop() 56 | }() 57 | 58 | err := client.ConnectAndWork() 59 | if err != ErrClientHasQuit { 60 | t.Fail() 61 | } 62 | } 63 | 64 | func TestUnexpectedErrorReturned(t *testing.T) { 65 | client := &Client{ 66 | Handler: HandlerFunc(func(job *Job) { 67 | }), 68 | } 69 | 70 | // this test will deadlock if fails 71 | err := client.Reserve(&closedConnection{}) 72 | if err == nil || !strings.HasSuffix(err.Error(), io.EOF.Error()) { 73 | t.Fail() 74 | } 75 | } 76 | 77 | func TestClientStopsOnSIGTERM(t *testing.T) { 78 | go func() { 79 | time.Sleep(100 * time.Millisecond) 80 | syscall.Kill(syscall.Getpid(), syscall.SIGTERM) 81 | }() 82 | 83 | err := ConnectAndWork("tcp", "localhost:11300", HandlerFunc(func(job *Job) {})) 84 | if err != ErrClientHasQuit { 85 | t.Fail() 86 | } 87 | } 88 | 89 | func TestClientStopsOnSIGINT(t *testing.T) { 90 | go func() { 91 | time.Sleep(100 * time.Millisecond) 92 | syscall.Kill(syscall.Getpid(), syscall.SIGINT) 93 | }() 94 | 95 | err := ConnectAndWork("tcp", "localhost:11300", HandlerFunc(func(job *Job) {})) 96 | if err != ErrClientHasQuit { 97 | t.Fail() 98 | } 99 | } 100 | 101 | func TestReserveIsParallelAndWaits(t *testing.T) { 102 | count := int32(0) 103 | tubeName := strconv.Itoa(int(time.Now().Unix())) 104 | start := time.Now() 105 | 106 | mux := NewWorkMux() 107 | mux.Handle(tubeName, HandlerFunc(func(job *Job) { 108 | time.Sleep(time.Second) 109 | atomic.AddInt32(&count, 1) 110 | job.Delete() 111 | })) 112 | 113 | go func() { 114 | conn, _ := beanstalk.Dial("tcp", "localhost:11300") 115 | tube := &beanstalk.Tube{Conn: conn, Name: tubeName} 116 | tube.Put([]byte("job1"), 0, 0, time.Minute) 117 | tube.Put([]byte("job2"), 0, 0, time.Minute) 118 | tube.Put([]byte("job3"), 0, 0, time.Minute) 119 | tube.Put([]byte("job4"), 0, 0, time.Minute) 120 | tube.Put([]byte("job5"), 0, 0, time.Minute) 121 | time.Sleep(time.Millisecond * 1100) 122 | syscall.Kill(syscall.Getpid(), syscall.SIGTERM) 123 | }() 124 | 125 | ConnectAndWork("tcp", "localhost:11300", mux) 126 | 127 | if count != 5 || time.Since(start) > time.Duration(time.Millisecond*2200) { 128 | t.Fail() 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package workers provides a client for the beanstalk protocol. 2 | // See http://kr.github.com/beanstalkd/ for the server. 3 | package workers 4 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package workers 2 | 3 | // Handler defines a way for workers to handle jobs for a tube. 4 | // Objects implementing the Handler interface can be registered to 5 | // handle jobs for a particular tube. 6 | type Handler interface { 7 | Work(*Job) 8 | } 9 | 10 | // HandlerFunc type is an adapter to allow the use of 11 | // ordinary functions as Work handlers. If f is a function 12 | // with the appropriate signature, HandlerFunc(f) is a 13 | // Handler object that calls f. 14 | type HandlerFunc func(*Job) 15 | 16 | // Work makes HandlerFunc implement the Handler interface. 17 | func (f HandlerFunc) Work(j *Job) { 18 | f(j) 19 | } 20 | -------------------------------------------------------------------------------- /handler_test.go: -------------------------------------------------------------------------------- 1 | package workers 2 | 3 | import "testing" 4 | 5 | // Handler interface is defined and has a Work method 6 | func TestHandlerInterface(t *testing.T) { 7 | var h Handler 8 | h = &testHandler{} 9 | h.Work(nil) 10 | } 11 | 12 | // HandlerFunc is defined and has a Work method that calls the 13 | // function passed to HandlerFunc 14 | func TestHandlerFunc(t *testing.T) { 15 | var h Handler 16 | called := false 17 | 18 | h = HandlerFunc(func(*Job) { 19 | called = true 20 | }) 21 | 22 | h.Work(nil) 23 | 24 | if !called { 25 | t.Fail() 26 | } 27 | } 28 | 29 | type testHandler struct{} 30 | 31 | func (h *testHandler) Work(*Job) {} 32 | -------------------------------------------------------------------------------- /job.go: -------------------------------------------------------------------------------- 1 | package workers 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/kr/beanstalk" 8 | ) 9 | 10 | // Job represents a job received by a worker. 11 | type Job struct { 12 | ID uint64 13 | Tube string 14 | Body []byte 15 | conn *beanstalk.Conn 16 | } 17 | 18 | // JobStats represents statistical information about a job. 19 | type JobStats struct { 20 | Priority uint32 21 | Age time.Duration 22 | TimeLeft time.Duration 23 | } 24 | 25 | // NewJob creates a Job. 26 | func NewJob(conn *beanstalk.Conn, tube string, id uint64, body []byte) *Job { 27 | return &Job{ 28 | ID: id, 29 | Tube: tube, 30 | Body: body, 31 | conn: conn, 32 | } 33 | } 34 | 35 | // Delete deletes the current job. 36 | // It removes the job from the server entirely. 37 | func (j *Job) Delete() error { 38 | return j.conn.Delete(j.ID) 39 | } 40 | 41 | // Release releases the current job. Release puts the reserved job back 42 | // into the ready queue (and marks its state as ready) to be run by any client. 43 | func (j *Job) Release(pri uint32, delay time.Duration) error { 44 | return j.conn.Release(j.ID, pri, delay) 45 | } 46 | 47 | // Touch touches the current job. It allows the worker to request more 48 | // time to work on the job. 49 | func (j *Job) Touch() error { 50 | return j.conn.Touch(j.ID) 51 | } 52 | 53 | // Bury buries the current job. Bury puts the job into the "buried" state. 54 | // Buried jobs are put into a FIFO linked list and will not be touched by 55 | // the server again until a client kicks them manually. 56 | func (j *Job) Bury(pri uint32) error { 57 | return j.conn.Bury(j.ID, pri) 58 | } 59 | 60 | // Stats gives statistical information about the current job. 61 | func (j *Job) Stats() (*JobStats, error) { 62 | m, err := j.conn.StatsJob(j.ID) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | pri, err := strconv.Atoi(m["pri"]) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | age, err := strconv.Atoi(m["age"]) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | left, err := strconv.Atoi(m["time-left"]) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | return &JobStats{ 83 | Priority: uint32(pri), 84 | Age: time.Duration(time.Duration(age) * time.Second), 85 | TimeLeft: time.Duration(time.Duration(left) * time.Second), 86 | }, nil 87 | } 88 | -------------------------------------------------------------------------------- /job_test.go: -------------------------------------------------------------------------------- 1 | package workers 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/kr/beanstalk" 10 | ) 11 | 12 | // Job is defined and can be instanciated 13 | func TestNewJob(t *testing.T) { 14 | var job *Job 15 | job = NewJob(&beanstalk.Conn{}, "tube1", 123, []byte{}) 16 | if job == nil { 17 | t.Fail() 18 | } 19 | } 20 | 21 | func TestJobCommands(t *testing.T) { 22 | conn := getTestConn(t) 23 | defer conn.Close() 24 | var job *Job 25 | 26 | // Delete 27 | job = withRandomJob(t, conn) 28 | if job.Delete() != nil || jobExists(t, conn, job.ID) { 29 | t.Fail() 30 | } 31 | 32 | // Release 33 | job = withRandomReservedJob(t, conn) 34 | job.Release(0, 0) 35 | stats, err := conn.StatsJob(job.ID) 36 | if err != nil || stats["state"] != "ready" { 37 | t.Fail() 38 | } 39 | 40 | // Touch 41 | job = withRandomReservedJob(t, conn) 42 | statsBefore, err := conn.StatsJob(job.ID) 43 | time.Sleep(time.Second) 44 | job.Touch() 45 | stats, err = conn.StatsJob(job.ID) 46 | timeLeftBefore, _ := strconv.Atoi(statsBefore["time-left"]) 47 | timeLeft, _ := strconv.Atoi(stats["time-left"]) 48 | if err != nil || timeLeft < timeLeftBefore { 49 | t.Fail() 50 | } 51 | 52 | // Bury 53 | job = withRandomReservedJob(t, conn) 54 | job.Bury(0) 55 | stats, err = conn.StatsJob(job.ID) 56 | if err != nil || stats["state"] != "buried" { 57 | t.Fail() 58 | } 59 | } 60 | 61 | func TestJobStats(t *testing.T) { 62 | conn := getTestConn(t) 63 | defer conn.Close() 64 | 65 | job := withRandomReservedJob(t, conn) 66 | stats, err := job.Stats() 67 | if err != nil { 68 | t.Fail() 69 | } 70 | statsOrigi, err := conn.StatsJob(job.ID) 71 | if err != nil || 72 | strconv.Itoa(int(stats.Age.Seconds())) != statsOrigi["age"] || 73 | strconv.Itoa(int(stats.TimeLeft.Seconds())) != statsOrigi["time-left"] || 74 | strconv.Itoa(int(stats.Priority)) != statsOrigi["pri"] { 75 | t.Fail() 76 | } 77 | } 78 | 79 | func getTestConn(t *testing.T) *beanstalk.Conn { 80 | conn, err := beanstalk.Dial("tcp", "localhost:11300") 81 | if err != nil { 82 | t.Fail() 83 | } 84 | return conn 85 | } 86 | 87 | func withRandomJob(t *testing.T, conn *beanstalk.Conn) *Job { 88 | id, err := conn.Put([]byte{}, 0, 0, time.Minute*5) 89 | if err != nil { 90 | t.Fail() 91 | } 92 | return NewJob(conn, "default", id, []byte{}) 93 | } 94 | 95 | func withRandomReservedJob(t *testing.T, conn *beanstalk.Conn) *Job { 96 | withRandomJob(t, conn) 97 | id, body, err := conn.Reserve(0) 98 | if err != nil { 99 | t.Fail() 100 | } 101 | return NewJob(conn, "default", id, body) 102 | } 103 | 104 | func jobExists(t *testing.T, conn *beanstalk.Conn, id uint64) bool { 105 | _, err := conn.Peek(id) 106 | return err == nil || !strings.HasSuffix(err.Error(), "not found") 107 | } 108 | -------------------------------------------------------------------------------- /mux.go: -------------------------------------------------------------------------------- 1 | package workers 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // WorkMux is a Beanstalkd Job multiplexer. 8 | // It matches the tube of each incoming job against a list 9 | // of registered tubes and calls the handler of that tube. 10 | type WorkMux struct { 11 | mu sync.RWMutex // guards muxEntry 12 | m map[string]muxEntry 13 | } 14 | 15 | type muxEntry struct { 16 | h Handler 17 | tube string 18 | } 19 | 20 | // NewWorkMux allocates and returns a new WorkMux. 21 | func NewWorkMux() *WorkMux { 22 | return &WorkMux{m: make(map[string]muxEntry)} 23 | } 24 | 25 | // Handle registers the job handler for the given tube. 26 | // If a handler already exists for tube, Handle panics. 27 | func (mux *WorkMux) Handle(tube string, handler Handler) { 28 | mux.mu.Lock() 29 | defer mux.mu.Unlock() 30 | 31 | if tube == "" { 32 | panic("invalid tube") 33 | } 34 | 35 | if handler == nil { 36 | panic("nil handler") 37 | } 38 | 39 | if _, found := mux.m[tube]; found { 40 | panic("multiple registrations for " + tube) 41 | } 42 | 43 | mux.m[tube] = muxEntry{ 44 | h: handler, 45 | tube: tube, 46 | } 47 | } 48 | 49 | // Handler returns the handler to use for the given job. If there is no 50 | // registered handler that applies to the job, Handler returns nil. 51 | func (mux *WorkMux) Handler(tube string) Handler { 52 | mux.mu.RLock() 53 | defer mux.mu.RUnlock() 54 | 55 | if handler, found := mux.m[tube]; found { 56 | return handler.h 57 | } 58 | 59 | return nil 60 | } 61 | 62 | // Tubes returns a list of tubes handled by the WorkMux. 63 | func (mux *WorkMux) Tubes() []string { 64 | mux.mu.RLock() 65 | defer mux.mu.RUnlock() 66 | 67 | tubes := make([]string, len(mux.m)) 68 | i := 0 69 | 70 | for k := range mux.m { 71 | tubes[i] = k 72 | i++ 73 | } 74 | 75 | return tubes 76 | } 77 | 78 | // Work dispatches the job to the proper handler. Makes WorkMux Implements 79 | // the Handler interface. Work panics if no handler is defined to handle the 80 | // job. 81 | func (mux WorkMux) Work(j *Job) { 82 | h := mux.Handler(j.Tube) 83 | 84 | if h == nil { 85 | panic("no handler for tube " + j.Tube) 86 | } 87 | 88 | h.Work(j) 89 | } 90 | -------------------------------------------------------------------------------- /mux_test.go: -------------------------------------------------------------------------------- 1 | package workers 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // WorkMux is defined and can be instanciated 8 | func TestNewWorkMux(t *testing.T) { 9 | var mux *WorkMux 10 | mux = NewWorkMux() 11 | if mux == nil { 12 | t.Fail() 13 | } 14 | } 15 | 16 | func TestHandleInvalidTube(t *testing.T) { 17 | defer func() { 18 | if err := recover(); err == nil || err != "invalid tube" { 19 | t.Fail() 20 | } 21 | }() 22 | mux := NewWorkMux() 23 | mux.Handle("", HandlerFunc(func(*Job) {})) 24 | } 25 | 26 | func TestHandleNilHandler(t *testing.T) { 27 | defer func() { 28 | if err := recover(); err == nil || err != "nil handler" { 29 | t.Fail() 30 | } 31 | }() 32 | mux := NewWorkMux() 33 | mux.Handle("tube1", nil) 34 | } 35 | 36 | func TestHandleDuplicate(t *testing.T) { 37 | defer func() { 38 | if err := recover(); err == nil || err != "multiple registrations for tube1" { 39 | t.Fail() 40 | } 41 | }() 42 | mux := NewWorkMux() 43 | mux.Handle("tube1", HandlerFunc(func(*Job) {})) 44 | mux.Handle("tube1", HandlerFunc(func(*Job) {})) 45 | } 46 | 47 | // Checks for unknown registered handler 48 | func TestHandlerUnknown(t *testing.T) { 49 | mux := NewWorkMux() 50 | if mux.Handler("tube1") != nil { 51 | t.Fail() 52 | } 53 | } 54 | 55 | // Checks for a known registered handler 56 | func TestHandlerRegistered(t *testing.T) { 57 | mux := NewWorkMux() 58 | handled := false 59 | mux.Handle("tube1", HandlerFunc(func(*Job) { handled = true })) 60 | mux.Handler("tube1").Work(nil) 61 | if !handled { 62 | t.Fail() 63 | } 64 | } 65 | 66 | func TestEmptyTubes(t *testing.T) { 67 | mux := NewWorkMux() 68 | if len(mux.Tubes()) != 0 { 69 | t.Fail() 70 | } 71 | } 72 | 73 | func TestTubes(t *testing.T) { 74 | contains := func(s []string, e string) bool { 75 | for _, a := range s { 76 | if a == e { 77 | return true 78 | } 79 | } 80 | return false 81 | } 82 | 83 | mux := NewWorkMux() 84 | mux.Handle("tube1", HandlerFunc(func(*Job) {})) 85 | mux.Handle("tube2", HandlerFunc(func(*Job) {})) 86 | mux.Handle("tubeN", HandlerFunc(func(*Job) {})) 87 | tubes := mux.Tubes() 88 | if len(tubes) != 3 || !contains(tubes, "tube1") || !contains(tubes, "tube2") || !contains(tubes, "tubeN") { 89 | t.Fail() 90 | } 91 | } 92 | 93 | func TestWork(t *testing.T) { 94 | handled := false 95 | mux := NewWorkMux() 96 | mux.Handle("tube1", HandlerFunc(func(*Job) {})) 97 | mux.Handle("tube2", HandlerFunc(func(*Job) { handled = true })) 98 | mux.Handle("tubeN", HandlerFunc(func(*Job) {})) 99 | mux.Work(&Job{Tube: "tube2"}) 100 | if !handled { 101 | t.Fail() 102 | } 103 | } 104 | 105 | func TestWorkUnhandledTube(t *testing.T) { 106 | defer func() { 107 | if err := recover(); err == nil || err != "no handler for tube tubeX" { 108 | t.Fail() 109 | } 110 | }() 111 | mux := NewWorkMux() 112 | mux.Work(&Job{Tube: "tubeX"}) 113 | } 114 | --------------------------------------------------------------------------------