├── Makefile ├── types.go ├── go.mod ├── .gitignore ├── runner_windows.go ├── .github └── workflows │ └── ci.yml ├── runner_unix.go ├── middleware.go ├── .golangci.yml ├── go.sum ├── testing.go ├── testing_test.go ├── middleware_test.go ├── log.go ├── context_test.go ├── Changes.md ├── runner_test.go ├── manager_test.go ├── context.go ├── test └── main.go ├── runner.go ├── README.md ├── manager.go └── LICENSE /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go test ./... 3 | 4 | work: 5 | go run test/main.go 6 | 7 | cover: 8 | go test -cover -coverprofile .cover.out . 9 | go tool cover -html=.cover.out -o coverage.html 10 | open coverage.html 11 | 12 | lint: 13 | golangci-lint run 14 | 15 | .PHONY: work test cover 16 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package faktory_worker 2 | 3 | import "context" 4 | 5 | const ( 6 | Version = "1.7.0" 7 | ) 8 | 9 | // Perform actually executes the job. 10 | // It must be thread-safe. 11 | type Perform func(ctx context.Context, args ...interface{}) error 12 | 13 | type LifecycleEventHandler func(*Manager) error 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/contribsys/faktory_worker_go 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/contribsys/faktory v1.8.0 7 | github.com/stretchr/testify v1.8.4 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | # Folder by any jetbrains IDE 17 | /.idea 18 | -------------------------------------------------------------------------------- /runner_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package faktory_worker 4 | 5 | import ( 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | ) 10 | 11 | var ( 12 | // SIGTERM is an alias for syscall.SIGTERM 13 | SIGTERM os.Signal = syscall.SIGTERM 14 | // SIGINT is and alias for syscall.SIGINT 15 | SIGINT os.Signal = os.Interrupt 16 | 17 | signalMap = map[os.Signal]string{ 18 | SIGTERM: "terminate", 19 | SIGINT: "terminate", 20 | } 21 | ) 22 | 23 | func hookSignals() chan os.Signal { 24 | sigchan := make(chan os.Signal) 25 | signal.Notify(sigchan, SIGINT) 26 | signal.Notify(sigchan, SIGTERM) 27 | return sigchan 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | services: 14 | faktory: 15 | image: contribsys/faktory:latest 16 | ports: ['7419:7419'] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@v4 23 | with: 24 | go-version: 1.17 25 | 26 | - name: Install linter 27 | run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.56.1 28 | 29 | - name: Test 30 | run: make lint test 31 | -------------------------------------------------------------------------------- /runner_unix.go: -------------------------------------------------------------------------------- 1 | // +build linux freebsd netbsd openbsd dragonfly solaris illumos aix darwin 2 | 3 | package faktory_worker 4 | 5 | import ( 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | ) 10 | 11 | var ( 12 | SIGTERM os.Signal = syscall.SIGTERM 13 | SIGTSTP os.Signal = syscall.SIGTSTP 14 | SIGTTIN os.Signal = syscall.SIGTTIN 15 | SIGINT os.Signal = os.Interrupt 16 | 17 | signalMap = map[os.Signal]string{ 18 | SIGTERM: "terminate", 19 | SIGINT: "terminate", 20 | SIGTSTP: "quiet", 21 | SIGTTIN: "dump", 22 | } 23 | ) 24 | 25 | func hookSignals() chan os.Signal { 26 | sigchan := make(chan os.Signal, 1) 27 | signal.Notify(sigchan, SIGINT) 28 | signal.Notify(sigchan, SIGTERM) 29 | signal.Notify(sigchan, SIGTSTP) 30 | signal.Notify(sigchan, SIGTTIN) 31 | return sigchan 32 | } 33 | -------------------------------------------------------------------------------- /middleware.go: -------------------------------------------------------------------------------- 1 | package faktory_worker 2 | 3 | import ( 4 | "context" 5 | 6 | faktory "github.com/contribsys/faktory/client" 7 | ) 8 | 9 | type Handler func(ctx context.Context, job *faktory.Job) error 10 | type MiddlewareFunc func(ctx context.Context, job *faktory.Job, next func(ctx context.Context) error) error 11 | 12 | // Use(...) adds middleware to the chain. 13 | func (mgr *Manager) Use(middleware ...MiddlewareFunc) { 14 | mgr.middleware = append(mgr.middleware, middleware...) 15 | } 16 | 17 | func dispatch(ctx context.Context, chain []MiddlewareFunc, job *faktory.Job, perform Handler) error { 18 | if len(chain) == 0 { 19 | return perform(ctx, job) 20 | } 21 | 22 | link := chain[0] 23 | rest := chain[1:] 24 | return link(ctx, job, func(ctx context.Context) error { 25 | return dispatch(ctx, rest, job, perform) 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | gocritic: 3 | # Which checks should be enabled; can't be combined with 'disabled-checks'; 4 | # See https://go-critic.github.io/overview#checks-overview 5 | # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run` 6 | # By default list of stable checks is used. 7 | disabled-checks: 8 | - commentedOutCode 9 | - commentFormatting 10 | 11 | # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. 12 | # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". 13 | enabled-tags: 14 | - performance 15 | - diagnostic 16 | - opinionated 17 | 18 | settings: # settings passed to gocritic 19 | rangeExprCopy: 20 | sizeThreshold: 16 21 | rangeValCopy: 22 | sizeThreshold: 16 23 | linters: 24 | enable: 25 | - gocritic 26 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/contribsys/faktory v1.8.0 h1:Rkxdph/1Tv9g60J8pyO2F7jKao77sRECh7HDBhgtuvI= 2 | github.com/contribsys/faktory v1.8.0/go.mod h1:SP+Y2Pr+JqLY9YJL3YNlJhdzTinm/oUe2CcOpwDHQR0= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 8 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /testing.go: -------------------------------------------------------------------------------- 1 | package faktory_worker 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | faktory "github.com/contribsys/faktory/client" 8 | ) 9 | 10 | type PerformExecutor interface { 11 | Execute(*faktory.Job, Perform) error 12 | ExecuteContext(context.Context, *faktory.Job, Perform) error 13 | } 14 | 15 | type testExecutor struct { 16 | *faktory.Pool 17 | } 18 | 19 | func NewTestExecutor(p *faktory.Pool) PerformExecutor { 20 | return &testExecutor{Pool: p} 21 | } 22 | 23 | func (tp *testExecutor) Execute(specjob *faktory.Job, p Perform) error { 24 | ctx := context.Background() 25 | return tp.ExecuteContext(ctx, specjob, p) 26 | } 27 | 28 | func (tp *testExecutor) ExecuteContext(ctx context.Context, specjob *faktory.Job, p Perform) error { 29 | // perform a JSON round trip to ensure Perform gets the arguments 30 | // exactly how a round trip to Faktory would look. 31 | data, err := json.Marshal(specjob) 32 | if err != nil { 33 | return err 34 | } 35 | var job faktory.Job 36 | err = json.Unmarshal(data, &job) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | c := jobContext(ctx, tp.Pool, &job) 42 | return p(c, job.Args...) 43 | } 44 | -------------------------------------------------------------------------------- /testing_test.go: -------------------------------------------------------------------------------- 1 | package faktory_worker 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | faktory "github.com/contribsys/faktory/client" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | var ( 13 | badGlobal = 1 14 | ) 15 | 16 | func myFunc(ctx context.Context, args ...interface{}) error { 17 | badGlobal += 1 18 | return nil 19 | } 20 | 21 | func TestTesting(t *testing.T) { 22 | pool, err := faktory.NewPool(5) 23 | assert.NoError(t, err) 24 | perf := NewTestExecutor(pool) 25 | 26 | assert.EqualValues(t, 1, badGlobal) 27 | somejob := faktory.NewJob("sometype", 12, "foobar") 28 | err = perf.Execute(somejob, myFunc) 29 | assert.NoError(t, err) 30 | assert.EqualValues(t, 2, badGlobal) 31 | 32 | ajob := faktory.NewJob("sometype", 12, "foobar") 33 | err = perf.Execute(ajob, func(ctx context.Context, args ...interface{}) error { 34 | assert.Equal(t, 2, len(args)) 35 | assert.EqualValues(t, 12, args[0]) 36 | assert.EqualValues(t, "foobar", args[1]) 37 | return nil 38 | }) 39 | assert.NoError(t, err) 40 | err = perf.Execute(ajob, func(ctx context.Context, args ...interface{}) error { 41 | return fmt.Errorf("Oops") 42 | }) 43 | assert.Equal(t, "Oops", err.Error()) 44 | } 45 | -------------------------------------------------------------------------------- /middleware_test.go: -------------------------------------------------------------------------------- 1 | package faktory_worker 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | faktory "github.com/contribsys/faktory/client" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | type middlewareValue string 13 | 14 | var EXAMPLE = middlewareValue("a") 15 | 16 | func TestMiddleware(t *testing.T) { 17 | mgr := NewManager() 18 | pool, err := faktory.NewPool(2) 19 | assert.NoError(t, err) 20 | mgr.Pool = pool 21 | 22 | mgr.Use(func(ctx context.Context, job *faktory.Job, next func(context.Context) error) error { 23 | modctx := context.WithValue(ctx, EXAMPLE, 4.0) 24 | return next(modctx) 25 | }) 26 | 27 | counter := 0 28 | blahFunc := func(ctx context.Context, job *faktory.Job) error { 29 | assert.EqualValues(t, 4.0, ctx.Value(EXAMPLE)) 30 | help := HelperFor(ctx) 31 | assert.Equal(t, job.Jid, help.Jid()) 32 | assert.Equal(t, job.Type, help.JobType()) 33 | assert.Equal(t, "", help.Bid()) 34 | counter += 1 35 | return nil 36 | } 37 | 38 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 39 | defer cancel() 40 | job := faktory.NewJob("blah", 1, 2) 41 | ctx = jobContext(ctx, mgr.Pool, job) 42 | assert.Nil(t, ctx.Value(EXAMPLE)) 43 | assert.EqualValues(t, 0, counter) 44 | 45 | err = dispatch(ctx, mgr.middleware, job, blahFunc) 46 | 47 | assert.NoError(t, err) 48 | assert.EqualValues(t, 1, counter) 49 | assert.Nil(t, ctx.Value(EXAMPLE)) 50 | 51 | } 52 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package faktory_worker 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | type Logger interface { 9 | Debug(v ...interface{}) 10 | Debugf(format string, args ...interface{}) 11 | Info(v ...interface{}) 12 | Infof(format string, args ...interface{}) 13 | Warn(v ...interface{}) 14 | Warnf(format string, args ...interface{}) 15 | Error(v ...interface{}) 16 | Errorf(format string, args ...interface{}) 17 | Fatal(v ...interface{}) 18 | Fatalf(format string, args ...interface{}) 19 | } 20 | 21 | type StdLogger struct { 22 | *log.Logger 23 | } 24 | 25 | func NewStdLogger() Logger { 26 | flags := log.Ldate | log.Ltime | log.Lmicroseconds | log.LUTC 27 | return &StdLogger{log.New(os.Stdout, "", flags)} 28 | } 29 | 30 | func (l *StdLogger) Debug(v ...interface{}) { 31 | l.Println(v...) 32 | } 33 | 34 | func (l *StdLogger) Debugf(format string, v ...interface{}) { 35 | l.Printf(format+"\n", v...) 36 | } 37 | 38 | func (l *StdLogger) Error(v ...interface{}) { 39 | l.Println(v...) 40 | } 41 | 42 | func (l *StdLogger) Errorf(format string, v ...interface{}) { 43 | l.Printf(format+"\n", v...) 44 | } 45 | 46 | func (l *StdLogger) Info(v ...interface{}) { 47 | l.Println(v...) 48 | } 49 | 50 | func (l *StdLogger) Infof(format string, v ...interface{}) { 51 | l.Printf(format+"\n", v...) 52 | } 53 | 54 | func (l *StdLogger) Warn(v ...interface{}) { 55 | l.Println(v...) 56 | } 57 | 58 | func (l *StdLogger) Warnf(format string, v ...interface{}) { 59 | l.Printf(format+"\n", v...) 60 | } 61 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package faktory_worker 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "regexp" 7 | "testing" 8 | "time" 9 | 10 | faktory "github.com/contribsys/faktory/client" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestSimpleContext(t *testing.T) { 15 | t.Parallel() 16 | 17 | pool, err := faktory.NewPool(1) 18 | assert.NoError(t, err) 19 | 20 | job := faktory.NewJob("something", 1, 2) 21 | job.SetCustom("track", 1) 22 | 23 | //cl, err := faktory.Open() 24 | //assert.NoError(t, err) 25 | //cl.Push(job) 26 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 27 | defer cancel() 28 | ctx = jobContext(ctx, pool, job) 29 | help := HelperFor(ctx) 30 | assert.Equal(t, help.Jid(), job.Jid) 31 | assert.Empty(t, help.Bid()) 32 | assert.Equal(t, "something", help.JobType()) 33 | 34 | _, ok := ctx.Deadline() 35 | assert.True(t, ok) 36 | 37 | //assert.NoError(t, ctx.TrackProgress(45, "Working....", nil)) 38 | 39 | err = help.Batch(func(b *faktory.Batch) error { 40 | return nil 41 | }) 42 | assert.Error(t, err) 43 | assert.True(t, errors.Is(err, NoAssociatedBatchError)) 44 | 45 | } 46 | 47 | func TestBatchContext(t *testing.T) { 48 | t.Parallel() 49 | 50 | pool, err := faktory.NewPool(1) 51 | assert.NoError(t, err) 52 | 53 | job := faktory.NewJob("something", 1, 2) 54 | job.SetCustom("track", 1) 55 | job.SetCustom("bid", "nosuchbatch") 56 | 57 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 58 | defer cancel() 59 | ctx = jobContext(ctx, pool, job) 60 | help := HelperFor(ctx) 61 | assert.Equal(t, help.Jid(), job.Jid) 62 | assert.Equal(t, "nosuchbatch", help.Bid()) 63 | assert.Equal(t, "something", help.JobType()) 64 | 65 | mgr := NewManager() 66 | mgr.Pool = pool 67 | 68 | withServer(t, "ent", mgr, func(cl *faktory.Client) error { 69 | err = help.Batch(func(b *faktory.Batch) error { 70 | return nil 71 | }) 72 | assert.Error(t, err) 73 | assert.Regexp(t, regexp.MustCompile("No such batch"), err.Error()) 74 | return nil 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /Changes.md: -------------------------------------------------------------------------------- 1 | # faktory\_worker\_go 2 | 3 | ## 1.7.0 4 | 5 | - Implement hard shutdown timeout of 25 seconds. [#76] 6 | Your job funcs should implement `context` package semantics. 7 | If you use `Manager.Run()`, FWG will now gracefully shutdown. 8 | After a default delay of 25 seconds, FWG will cancel the root Context which should quickly cancel any lingering jobs running under that Manager. 9 | If your jobs run long and do not respond to context cancellation, you risk orphaning any jobs in-progress. 10 | They will linger on the Busy tab until the job's `reserve_for` timeout. 11 | 12 | Please also note that `RunWithContext` added in `1.6.0` does not implement the shutdown delay but the README example contains the code to implement it. 13 | 14 | ## 1.6.0 15 | 16 | - Upgrade to Go 1.17 and Faktory 1.6.0. 17 | - Add `Manager.RunWithContext(ctx context.Context) error` [#58] 18 | Allows the caller to directly control when FWG stops. See README for usage. 19 | 20 | ## 1.5.0 21 | 22 | - Auto-shutdown worker process if heartbeat expires due to network issues [#57] 23 | - Send process RSS to Faktory (only available on Linux) 24 | - Fix connection issue which causes worker processes to appear and disappear on 25 | Busy tab. [#47] 26 | 27 | ## 1.4.0 28 | 29 | - **Breaking API changes due to misunderstanding the `context` package.** 30 | I've had to make significant changes to FWG's public APIs to allow 31 | for mutable contexts. This unfortunately requires breaking changes: 32 | ``` 33 | Job Handler 34 | Before: func(ctx worker.Context, args ...interface{}) error 35 | After: func(ctx context.Context, args ...interface{}) error 36 | 37 | Middleware Handler 38 | Before: func(ctx worker.Context, job *faktory.Job) error 39 | After: func(ctx context.Context, job *faktory.Job, next func(context.Context) error) error 40 | ``` 41 | Middleware funcs now need to call `next` to continue job dispatch. 42 | Use `help := worker.HelperFor(ctx)` to get the old APIs provided by `worker.Context` 43 | within your handlers. 44 | - Fix issue reporting job errors back to Faktory 45 | - Add helpers for testing `Perform` funcs 46 | ```go 47 | myFunc := func(ctx context.Context, args ...interface{}) error { 48 | return nil 49 | } 50 | 51 | pool, err := faktory.NewPool(5) 52 | perf := worker.NewTestExecutor(pool) 53 | 54 | somejob := faktory.NewJob("sometype", 12, "foobar") 55 | err = perf.Execute(somejob, myFunc) 56 | ``` 57 | 58 | ## 1.3.0 59 | 60 | - Add new job context APIs for Faktory Enterprise features 61 | - Misc API refactoring to improve test coverage 62 | - Remove `faktory_worker_go` Pool interface in favor of the Pool now in `faktory/client` 63 | 64 | ## 1.0.1 65 | 66 | - FWG will now dump all thread backtraces upon `kill -TTIN `, 67 | useful for debugging stuck job processing. 68 | - Send current state back to Faktory so process state changes are visible on Busy page. 69 | - Tweak log output formatting 70 | 71 | ## 1.0.0 72 | 73 | - Allow process labels (visible in Web UI) to be set [#32] 74 | - Add APIs to manage [Batches](https://github.com/contribsys/faktory/wiki/Ent-Batches). 75 | 76 | ## 0.7.0 77 | 78 | - Implement weighted queue fetch [#20, nickpoorman] 79 | - Initial version. 80 | -------------------------------------------------------------------------------- /runner_test.go: -------------------------------------------------------------------------------- 1 | package faktory_worker 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "math/rand" 7 | "os" 8 | "testing" 9 | "time" 10 | 11 | faktory "github.com/contribsys/faktory/client" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func sometask(ctx context.Context, args ...interface{}) error { 16 | return nil 17 | } 18 | 19 | func TestRegistration(t *testing.T) { 20 | t.Parallel() 21 | mgr := NewManager() 22 | mgr.Register("somejob", sometask) 23 | } 24 | 25 | func TestWeightedQueues(t *testing.T) { 26 | rand.Seed(42) 27 | 28 | mgr := NewManager() 29 | mgr.ProcessWeightedPriorityQueues(map[string]int{"critical": 3, "default": 2, "bulk": 1}) 30 | assert.Equal(t, []string{"critical", "default", "bulk"}, mgr.queueList()) 31 | assert.Equal(t, []string{"bulk", "default", "critical"}, mgr.queueList()) 32 | 33 | mgr.ProcessWeightedPriorityQueues(map[string]int{"critical": 1, "default": 100, "bulk": 1000}) 34 | assert.Equal(t, []string{"bulk", "default", "critical"}, mgr.queueList()) 35 | 36 | mgr.ProcessWeightedPriorityQueues(map[string]int{"critical": 1, "default": 1000, "bulk": 100}) 37 | assert.Equal(t, []string{"default", "bulk", "critical"}, mgr.queueList()) 38 | 39 | mgr.ProcessWeightedPriorityQueues(map[string]int{"critical": 1, "default": 1, "bulk": 1}) 40 | assert.Equal(t, []string{"critical", "bulk", "default"}, mgr.queueList()) 41 | } 42 | 43 | func TestStrictQueues(t *testing.T) { 44 | t.Parallel() 45 | mgr := NewManager() 46 | mgr.ProcessStrictPriorityQueues("critical", "default", "bulk") 47 | assert.Equal(t, []string{"critical", "default", "bulk"}, mgr.queueList()) 48 | assert.Equal(t, []string{"critical", "default", "bulk"}, mgr.queueList()) 49 | assert.Equal(t, []string{"critical", "default", "bulk"}, mgr.queueList()) 50 | assert.Equal(t, []string{"critical", "default", "bulk"}, mgr.queueList()) 51 | 52 | mgr.ProcessStrictPriorityQueues("default", "critical", "bulk") 53 | assert.Equal(t, []string{"default", "critical", "bulk"}, mgr.queueList()) 54 | assert.Equal(t, []string{"default", "critical", "bulk"}, mgr.queueList()) 55 | assert.Equal(t, []string{"default", "critical", "bulk"}, mgr.queueList()) 56 | assert.Equal(t, []string{"default", "critical", "bulk"}, mgr.queueList()) 57 | } 58 | 59 | func TestLiveServer(t *testing.T) { 60 | mgr := NewManager() 61 | mgr.ProcessStrictPriorityQueues("fwgtest") 62 | mgr.Concurrency = 1 63 | err := mgr.setUpWorkerProcess() 64 | assert.NoError(t, err) 65 | 66 | mgr.Register("aworker", func(ctx context.Context, args ...interface{}) error { 67 | //fmt.Println("doing work", args) 68 | return nil 69 | }) 70 | 71 | withServer(t, "oss", mgr, func(cl *faktory.Client) error { 72 | cl.Flush() 73 | 74 | j := faktory.NewJob("something", 1, 2) 75 | j.Queue = "fwgtest" 76 | err := cl.Push(j) 77 | assert.NoError(t, err) 78 | 79 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 80 | defer cancel() 81 | 82 | err = processOne(ctx, mgr) 83 | assert.Error(t, err) 84 | _, ok := err.(*NoHandlerError) 85 | assert.True(t, ok) 86 | assert.Equal(t, err, &NoHandlerError{JobType: "something"}) 87 | 88 | j = faktory.NewJob("aworker", 1, 2) 89 | j.Queue = "fwgtest" 90 | err = cl.Push(j) 91 | assert.NoError(t, err) 92 | 93 | err = processOne(ctx, mgr) 94 | assert.NoError(t, err) 95 | return nil 96 | }) 97 | 98 | } 99 | 100 | func TestThreadDump(t *testing.T) { 101 | t.Parallel() 102 | 103 | devnull, err := os.OpenFile("/dev/null", os.O_WRONLY, 0) 104 | assert.NoError(t, err) 105 | 106 | logg := &StdLogger{ 107 | log.New(devnull, "", 0), 108 | } 109 | dumpThreads(logg) 110 | } 111 | -------------------------------------------------------------------------------- /manager_test.go: -------------------------------------------------------------------------------- 1 | package faktory_worker 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log" 7 | "os" 8 | "syscall" 9 | "testing" 10 | "time" 11 | 12 | faktory "github.com/contribsys/faktory/client" 13 | "github.com/contribsys/faktory/util" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestManagerSetup(t *testing.T) { 18 | clx, err := faktory.Open() 19 | startsz := 0.0 20 | if err == nil { 21 | info, err := clx.Info() 22 | if err == nil { 23 | startsz = info["faktory"].(map[string]interface{})["tasks"].(map[string]interface{})["Workers"].(map[string]interface{})["size"].(float64) 24 | } 25 | } 26 | 27 | mgr := NewManager() 28 | err = mgr.setUpWorkerProcess() 29 | assert.NoError(t, err) 30 | 31 | startupCalled := false 32 | mgr.On(Startup, func(m *Manager) error { 33 | startupCalled = true 34 | assert.NotNil(t, m) 35 | return nil 36 | }) 37 | mgr.fireEvent(Startup) 38 | assert.True(t, startupCalled) 39 | 40 | withServer(t, "oss", mgr, func(cl *faktory.Client) error { 41 | info, err := cl.Info() 42 | assert.NoError(t, err) 43 | sz := info["faktory"].(map[string]interface{})["tasks"].(map[string]interface{})["Workers"].(map[string]interface{})["size"].(float64) 44 | assert.EqualValues(t, startsz+1, sz) 45 | 46 | return nil 47 | }) 48 | 49 | assert.Equal(t, "", mgr.handleEvent("quiet")) 50 | time.Sleep(1 * time.Millisecond) 51 | assert.Equal(t, "quiet", mgr.handleEvent("quiet")) 52 | 53 | devnull, err := os.OpenFile("/dev/null", os.O_WRONLY, 0) 54 | assert.NoError(t, err) 55 | 56 | logg := &StdLogger{ 57 | log.New(devnull, "", 0), 58 | } 59 | 60 | mgr.Logger = logg 61 | assert.Equal(t, "", mgr.handleEvent("dump")) 62 | 63 | terminateCalled := false 64 | mgr.On(Shutdown, func(m *Manager) error { 65 | terminateCalled = true 66 | assert.NotNil(t, m) 67 | return nil 68 | }) 69 | mgr.Terminate(false) 70 | assert.Equal(t, true, terminateCalled) 71 | // calling terminate again should be a noop 72 | terminateCalled = false 73 | mgr.Terminate(false) 74 | assert.Equal(t, false, terminateCalled) 75 | } 76 | 77 | func withServer(t *testing.T, lvl string, mgr *Manager, fn func(cl *faktory.Client) error) { 78 | err := mgr.with(func(cl *faktory.Client) error { 79 | if lvl == "oss" { 80 | return fn(cl) 81 | } 82 | 83 | hash, err := cl.Info() 84 | if err != nil { 85 | return err 86 | } 87 | desc := hash["server"].(map[string]interface{})["description"].(string) 88 | if lvl == "ent" && desc == "Faktory Enterprise" { 89 | return fn(cl) 90 | } else if lvl == "pro" && desc != "Faktory" { 91 | return fn(cl) 92 | } 93 | return nil 94 | }) 95 | 96 | if errors.Is(err, syscall.ECONNREFUSED) { 97 | util.Debug("Server not running, skipping...") 98 | return 99 | } else { 100 | assert.NoError(t, err) 101 | } 102 | } 103 | 104 | func TestInlineDispatchArgsSerialization(t *testing.T) { 105 | mgr := NewManager() 106 | 107 | var receivedArgs []interface{} 108 | mgr.Register("test_job", func(ctx context.Context, args ...interface{}) error { 109 | receivedArgs = args 110 | return nil 111 | }) 112 | 113 | // Create a temporary struct that will become a map after JSON serialization 114 | type tempStruct struct { 115 | Name string `json:"name"` 116 | Age int `json:"age"` 117 | } 118 | 119 | job := faktory.NewJob("test_job", tempStruct{Name: "John", Age: 30}) 120 | 121 | err := mgr.InlineDispatch(job) 122 | assert.NoError(t, err) 123 | 124 | // Verify that the struct was converted to a map during serialization 125 | assert.Len(t, receivedArgs, 1) 126 | argMap, ok := receivedArgs[0].(map[string]interface{}) 127 | assert.True(t, ok, "Expected argument to be converted to map[string]interface{}") 128 | assert.Equal(t, "John", argMap["name"]) 129 | assert.Equal(t, float64(30), argMap["age"]) // JSON converts numbers to float64 130 | } 131 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package faktory_worker 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | faktory "github.com/contribsys/faktory/client" 10 | ) 11 | 12 | // internal keys for context value storage 13 | type valueKey int 14 | 15 | const ( 16 | poolKey valueKey = 2 17 | jobKey valueKey = 3 18 | ) 19 | 20 | var ( 21 | NoAssociatedBatchError = fmt.Errorf("No batch associated with this job") 22 | ) 23 | 24 | // The Helper provides access to valuable data and APIs 25 | // within an executing job. 26 | // 27 | // We're pretty strict about what's exposed in the Helper 28 | // because execution should be orthogonal to 29 | // most of the Job payload contents. 30 | // 31 | // func myJob(ctx context.Context, args ...interface{}) error { 32 | // helper := worker.HelperFor(ctx) 33 | // jid := helper.Jid() 34 | // 35 | // helper.With(func(cl *faktory.Client) error { 36 | // cl.Push("anotherJob", 4, "arg") 37 | // }) 38 | type Helper interface { 39 | Jid() string 40 | JobType() string 41 | 42 | // Custom provides access to the job custom hash. 43 | // Returns the value and `ok=true` if the key was found. 44 | // If not, returns `nil` and `ok=false`. 45 | // 46 | // No type checking is performed, please use with caution. 47 | Custom(key string) (value interface{}, ok bool) 48 | 49 | // Faktory Enterprise: 50 | // the BID of the Batch associated with this job 51 | Bid() string 52 | 53 | // Faktory Enterprise: 54 | // the BID of the Batch associated with this callback (complete or success) job 55 | CallbackBid() string 56 | 57 | // Faktory Enterprise: 58 | // open the batch associated with this job so we can add more jobs to it. 59 | // 60 | // func myJob(ctx context.Context, args ...interface{}) error { 61 | // helper := worker.HelperFor(ctx) 62 | // helper.Batch(func(b *faktory.Batch) error { 63 | // return b.Push(faktory.NewJob("sometype", 1, 2, 3)) 64 | // }) 65 | Batch(func(*faktory.Batch) error) error 66 | 67 | // allows direct access to the Faktory server from the job 68 | With(func(*faktory.Client) error) error 69 | 70 | // Faktory Enterprise: 71 | // this method integrates with Faktory Enterprise's Job Tracking feature. 72 | // `reserveUntil` is optional, only needed for long jobs which have more dynamic 73 | // lifetimes. 74 | // 75 | // helper.TrackProgress(10, "Updating code...", nil) 76 | // helper.TrackProgress(20, "Cleaning caches...", &time.Now().Add(1 * time.Hour))) 77 | // 78 | TrackProgress(percent int, desc string, reserveUntil *time.Time) error 79 | } 80 | 81 | type jobHelper struct { 82 | job *faktory.Job 83 | pool *faktory.Pool 84 | } 85 | 86 | // ensure type compatibility 87 | var _ Helper = &jobHelper{} 88 | 89 | func (h *jobHelper) Jid() string { 90 | return h.job.Jid 91 | } 92 | func (h *jobHelper) Bid() string { 93 | if b, ok := h.job.GetCustom("bid"); ok { 94 | return b.(string) 95 | } 96 | return "" 97 | } 98 | func (h *jobHelper) CallbackBid() string { 99 | if b, ok := h.job.GetCustom("_bid"); ok { 100 | return b.(string) 101 | } 102 | return "" 103 | } 104 | func (h *jobHelper) JobType() string { 105 | return h.job.Type 106 | } 107 | func (h *jobHelper) Custom(key string) (value interface{}, ok bool) { 108 | return h.job.GetCustom(key) 109 | } 110 | 111 | // Caution: this method must only be called within the 112 | // context of an executing job. It will panic if it cannot 113 | // create a Helper due to missing context values. 114 | func HelperFor(ctx context.Context) Helper { 115 | if j := ctx.Value(jobKey); j != nil { 116 | job := j.(*faktory.Job) 117 | if p := ctx.Value(poolKey); p != nil { 118 | pool := p.(*faktory.Pool) 119 | return &jobHelper{ 120 | job: job, 121 | pool: pool, 122 | } 123 | } 124 | } 125 | log.Panic("Invalid job context, cannot create faktory_worker_go job helper") 126 | return nil 127 | } 128 | 129 | func jobContext(ctx context.Context, pool *faktory.Pool, job *faktory.Job) context.Context { 130 | ctx = context.WithValue(ctx, poolKey, pool) 131 | ctx = context.WithValue(ctx, jobKey, job) 132 | return ctx 133 | } 134 | 135 | // requires Faktory Enterprise 136 | func (h *jobHelper) TrackProgress(percent int, desc string, reserveUntil *time.Time) error { 137 | return h.With(func(cl *faktory.Client) error { 138 | return cl.TrackSet(h.Jid(), percent, desc, reserveUntil) 139 | }) 140 | } 141 | 142 | // requires Faktory Enterprise 143 | // Open the current batch so we can add more jobs to it. 144 | func (h *jobHelper) Batch(fn func(*faktory.Batch) error) error { 145 | bid := h.Bid() 146 | if bid == "" { 147 | return NoAssociatedBatchError 148 | } 149 | 150 | var b *faktory.Batch 151 | var err error 152 | 153 | err = h.pool.With(func(cl *faktory.Client) error { 154 | b, err = cl.BatchOpen(bid) 155 | if err != nil { 156 | return err 157 | } 158 | return fn(b) 159 | }) 160 | if err != nil { 161 | return err 162 | } 163 | 164 | return nil 165 | } 166 | 167 | func (h *jobHelper) With(fn func(*faktory.Client) error) error { 168 | return h.pool.With(fn) 169 | } 170 | -------------------------------------------------------------------------------- /test/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "strings" 8 | "time" 9 | 10 | faktory "github.com/contribsys/faktory/client" 11 | worker "github.com/contribsys/faktory_worker_go" 12 | ) 13 | 14 | func someFunc(ctx context.Context, args ...interface{}) error { 15 | help := worker.HelperFor(ctx) 16 | log.Printf("Working on job %s\n", help.Jid()) 17 | // log.Printf("Context %v\n", ctx) 18 | // log.Printf("Args %v\n", args) 19 | time.Sleep(1 * time.Second) 20 | return nil 21 | } 22 | 23 | func batchFunc(ctx context.Context, args ...interface{}) error { 24 | help := worker.HelperFor(ctx) 25 | 26 | log.Printf("Working on job %s\n", help.Jid()) 27 | if help.Bid() != "" { 28 | log.Printf("within %s...\n", help.Bid()) 29 | } 30 | // log.Printf("Context %v\n", ctx) 31 | // log.Printf("Args %v\n", args) 32 | return nil 33 | } 34 | func fastFunc(ctx context.Context, args ...interface{}) error { 35 | return nil 36 | } 37 | func longFunc(ctx context.Context, args ...interface{}) error { 38 | help := worker.HelperFor(ctx) 39 | log.Printf("Working on job %s\n", help.Jid()) 40 | select { 41 | case <-ctx.Done(): 42 | fmt.Printf("Context closed, SUCCESS") 43 | case <-time.After(30 * time.Second): 44 | fmt.Printf("30 sec timeout, FAIL") 45 | } 46 | return nil 47 | } 48 | 49 | func main() { 50 | flags := log.Ldate | log.Ltime | log.Lmicroseconds | log.LUTC 51 | log.SetFlags(flags) 52 | 53 | mgr := worker.NewManager() 54 | mgr.Use(func(ctx context.Context, job *faktory.Job, next func(ctx context.Context) error) error { 55 | log.Printf("Starting work on job %s of type %s with custom %v\n", job.Jid, job.Type, job.Custom) 56 | err := next(ctx) 57 | log.Printf("Finished work on job %s with error %v\n", job.Jid, err) 58 | return err 59 | }) 60 | 61 | // register job types and the function to execute them 62 | mgr.Register("SomeJob", someFunc) 63 | mgr.Register("SomeWorker", someFunc) 64 | mgr.Register("ImportImageJob", batchFunc) 65 | mgr.Register("ImportImageSuccess", batchFunc) 66 | mgr.Register("Long", longFunc) 67 | mgr.Register("fast", fastFunc) 68 | //mgr.Register("AnotherJob", anotherFunc) 69 | 70 | // use up to N goroutines to execute jobs 71 | mgr.Concurrency = 20 72 | 73 | // pull jobs from these queues, in this order of precedence 74 | mgr.ProcessStrictPriorityQueues("critical", "default", "bulk") 75 | 76 | var quit bool 77 | mgr.On(worker.Shutdown, func(*worker.Manager) error { 78 | quit = true 79 | return nil 80 | }) 81 | go func() { 82 | batch() 83 | unique() 84 | 85 | for { 86 | if quit { 87 | return 88 | } 89 | produce(mgr) 90 | time.Sleep(10 * time.Second) 91 | } 92 | }() 93 | 94 | // Start processing jobs, this method does not return 95 | _ = mgr.Run() 96 | } 97 | 98 | func unique() { 99 | pool, err := faktory.NewPool(5) 100 | if err != nil { 101 | panic(err) 102 | } 103 | 104 | _ = pool.With(func(cl *faktory.Client) error { 105 | if err != nil { 106 | panic(err) 107 | } 108 | 109 | if !isEnt(cl) { 110 | return nil 111 | } 112 | 113 | job := faktory.NewJob("Long", 1, 2, 3) 114 | job.SetCustom("unique_for", 0.5) 115 | err := cl.Push(job) 116 | if err != nil { 117 | panic(err) 118 | } 119 | 120 | err = cl.Push(job) 121 | if err != nil { 122 | if e, ok := err.(*faktory.ProtocolError); ok { 123 | fmt.Printf("%+v\n", *e) 124 | return e 125 | } 126 | } 127 | panic(fmt.Sprintf("Expected: %+v", err)) 128 | }) 129 | } 130 | 131 | func isEnt(cl *faktory.Client) bool { 132 | hash, err := cl.Info() 133 | if err != nil { 134 | panic(err) 135 | } 136 | desc := hash["server"].(map[string]interface{})["description"].(string) 137 | return strings.Contains(desc, "Enterprise") 138 | } 139 | 140 | func batch() { 141 | cl, err := faktory.Open() 142 | if err != nil { 143 | return 144 | } 145 | 146 | if !isEnt(cl) { 147 | return 148 | } 149 | 150 | // Batch example 151 | // We want to import all images associated with user 1234. 152 | // Once we've imported those two images, we want to fire 153 | // a success callback so we can notify user 1234. 154 | b := faktory.NewBatch(cl) 155 | b.Description = "Import images for user 1234" 156 | b.Success = faktory.NewJob("ImportImageSuccess", "parent", "1234") 157 | // Once we call Jobs(), the batch is off and running 158 | err = b.Jobs(func() error { 159 | err := b.Push(faktory.NewJob("ImportImageJob", "1")) 160 | if err != nil { 161 | return err 162 | } 163 | 164 | fmt.Println("Creating jobs") 165 | for i := 1; i <= 10000; i++ { 166 | err = b.Push(faktory.NewJob("fast", []interface{}{})) 167 | if err != nil { 168 | return err 169 | } 170 | } 171 | // a child batch represents a set of jobs which can be monitored 172 | // separately from the parent batch's jobs. parent success won't 173 | // fire until child success runs without error. 174 | child := faktory.NewBatch(cl) 175 | child.ParentBid = b.Bid 176 | child.Description = "Child of " + b.Bid 177 | child.Success = faktory.NewJob("ImportImageSuccess", "child", "1234") 178 | err = child.Jobs(func() error { 179 | return child.Push(faktory.NewJob("ImportImageJob", "2")) 180 | }) 181 | if err != nil { 182 | return err 183 | } 184 | return b.Push(faktory.NewJob("ImportImageJob", "3")) 185 | }) 186 | if err != nil { 187 | panic(err) 188 | } 189 | 190 | st, err := cl.BatchStatus(b.Bid) 191 | if err != nil { 192 | panic(err) 193 | } 194 | fmt.Printf("%+v", st) 195 | } 196 | 197 | // Push something for us to work on. 198 | func produce(mgr *worker.Manager) { 199 | j1 := faktory.NewJob("SomeJob", 1, 2, "hello") 200 | j1.Custom = map[string]interface{}{ 201 | "hello": "world", 202 | } 203 | j2 := faktory.NewJob("Long", 3, 2, 1) 204 | 205 | err := mgr.Pool.With(func(cl *faktory.Client) error { 206 | _, err := cl.PushBulk([]*faktory.Job{j1, j2}) 207 | return err 208 | }) 209 | if err != nil { 210 | fmt.Printf("produce: %v\n", err) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /runner.go: -------------------------------------------------------------------------------- 1 | package faktory_worker 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "math" 8 | "math/rand" 9 | "os" 10 | "runtime" 11 | "sort" 12 | "strings" 13 | "syscall" 14 | "time" 15 | 16 | faktory "github.com/contribsys/faktory/client" 17 | ) 18 | 19 | type lifecycleEventType int 20 | 21 | const ( 22 | Startup lifecycleEventType = 1 23 | Quiet lifecycleEventType = 2 24 | Shutdown lifecycleEventType = 3 25 | ) 26 | 27 | type NoHandlerError struct { 28 | JobType string 29 | } 30 | 31 | func (s *NoHandlerError) Error() string { 32 | return fmt.Sprintf("No handler registered for job type %s", s.JobType) 33 | } 34 | 35 | func heartbeat(mgr *Manager) { 36 | mgr.shutdownWaiter.Add(1) 37 | defer mgr.shutdownWaiter.Done() 38 | 39 | timer := time.NewTicker(15 * time.Second) 40 | for { 41 | select { 42 | case <-timer.C: 43 | // we don't care about errors, assume any network 44 | // errors will heal eventually 45 | err := mgr.with(func(c *faktory.Client) error { 46 | data, err := c.Beat(mgr.state) 47 | if err != nil && strings.Contains(err.Error(), "Unknown worker") { 48 | // If our heartbeat expires, we must restart and re-authenticate. 49 | // Use a signal so we can unwind and shutdown cleanly. 50 | mgr.Logger.Warn("Faktory heartbeat has expired, shutting down...") 51 | if process, err := os.FindProcess(os.Getpid()); err != nil { 52 | mgr.Logger.Errorf("Could not find worker process %d: %v", os.Getpid(), err) 53 | } else { 54 | _ = process.Signal(syscall.SIGTERM) 55 | } 56 | } 57 | if err != nil || data == "" { 58 | return err 59 | } 60 | var hash map[string]string 61 | err = json.Unmarshal([]byte(data), &hash) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | if state, ok := hash["state"]; ok && state != "" { 67 | mgr.handleEvent(state) 68 | } 69 | return nil 70 | }) 71 | if err != nil { 72 | mgr.Logger.Error(fmt.Sprintf("heartbeat error: %v", err)) 73 | } 74 | case <-mgr.done: 75 | timer.Stop() 76 | return 77 | } 78 | } 79 | } 80 | 81 | func process(ctx context.Context, mgr *Manager, idx int) { 82 | mgr.shutdownWaiter.Add(1) 83 | defer mgr.shutdownWaiter.Done() 84 | 85 | // delay initial fetch randomly to prevent thundering herd. 86 | // this will pause between 0 and 2B nanoseconds, i.e. 0-2 seconds 87 | time.Sleep(time.Duration(rand.Int31())) 88 | sleep := 1.0 89 | for { 90 | if mgr.state != "" { 91 | return 92 | } 93 | 94 | // check for shutdown 95 | select { 96 | case <-mgr.done: 97 | return 98 | default: 99 | } 100 | 101 | err := processOne(ctx, mgr) 102 | if err != nil { 103 | mgr.Logger.Debug(err) 104 | if _, ok := err.(*NoHandlerError); !ok { 105 | // if we don't know how to process this jobtype, 106 | // we Fail it and sleep for a bit so we don't get 107 | // caught in an infinite loop "processing" a queue full 108 | // of jobs we don't understand. 109 | time.Sleep(50 * time.Millisecond) 110 | } else { 111 | // if we have an unknown error processing a job, use 112 | // exponential backoff so we don't constantly slam the 113 | // log with "connection refused" errors or similar. 114 | select { 115 | case <-mgr.done: 116 | case <-time.After(time.Duration(sleep) * time.Second): 117 | sleep = math.Max(sleep*2, 30) 118 | } 119 | } 120 | } else { 121 | // success, reset sleep timer 122 | sleep = 1.0 123 | } 124 | } 125 | } 126 | 127 | func processOne(ctx context.Context, mgr *Manager) error { 128 | var job *faktory.Job 129 | 130 | // explicit scopes to limit variable visibility 131 | { 132 | var e error 133 | err := mgr.with(func(c *faktory.Client) error { 134 | job, e = c.Fetch(mgr.queueList()...) 135 | if e != nil { 136 | return e 137 | } 138 | return nil 139 | }) 140 | if err != nil { 141 | return err 142 | } 143 | if job == nil { 144 | return nil 145 | } 146 | } 147 | 148 | if !mgr.isRegistered(job.Type) { 149 | je := &NoHandlerError{JobType: job.Type} 150 | err := mgr.with(func(c *faktory.Client) error { 151 | return c.Fail(job.Jid, je, nil) 152 | }) 153 | if err != nil { 154 | return err 155 | } 156 | return je 157 | } 158 | 159 | joberr := mgr.dispatch(ctx, job) 160 | if joberr != nil { 161 | // job errors are normal and expected, we don't return early from them 162 | mgr.Logger.Errorf("Error running %s job %s: %v", job.Type, job.Jid, joberr) 163 | } 164 | 165 | until := time.After(30 * time.Second) 166 | sleep := 1.0 167 | for { 168 | // we want to report the result back to Faktory. 169 | // we stay in this loop until we successfully report. 170 | err := mgr.with(func(c *faktory.Client) error { 171 | if joberr != nil { 172 | return c.Fail(job.Jid, joberr, nil) 173 | } else { 174 | return c.Ack(job.Jid) 175 | } 176 | }) 177 | if err == nil { 178 | return nil 179 | } 180 | select { 181 | case <-until: 182 | mgr.Logger.Error(fmt.Errorf("Failed to report JID %v result to Faktory: %w", job.Jid, err)) 183 | return nil 184 | case <-mgr.done: 185 | mgr.Logger.Error(fmt.Errorf("Unable to report JID %v result to Faktory: %w", job.Jid, err)) 186 | return nil 187 | case <-time.After(time.Duration(sleep) * time.Second): 188 | sleep = math.Max(sleep*2, 30) 189 | mgr.Logger.Debug(fmt.Errorf("Unable to report JID %v result to Faktory: %w", job.Jid, err)) 190 | } 191 | } 192 | } 193 | 194 | // expandWeightedQueues builds a slice of queues represented the number of times equal to their weights. 195 | func expandWeightedQueues(queueWeights map[string]int) []string { 196 | weightsTotal := 0 197 | for _, queueWeight := range queueWeights { 198 | weightsTotal += queueWeight 199 | } 200 | 201 | weightedQueues := make([]string, weightsTotal) 202 | fillIndex := 0 203 | 204 | for queue, nTimes := range queueWeights { 205 | // Fill weightedQueues with queue n times 206 | for idx := 0; idx < nTimes; idx++ { 207 | weightedQueues[fillIndex] = queue 208 | fillIndex++ 209 | } 210 | } 211 | 212 | // weightedQueues has to be stable so we can write tests 213 | sort.Strings(weightedQueues) 214 | return weightedQueues 215 | } 216 | 217 | func queueKeys(queues map[string]int) []string { 218 | keys := make([]string, len(queues)) 219 | i := 0 220 | for k := range queues { 221 | keys[i] = k 222 | i++ 223 | } 224 | // queues has to be stable so we can write tests 225 | sort.Strings(keys) 226 | return keys 227 | } 228 | 229 | // shuffleQueues returns a copy of the slice with the elements shuffled. 230 | func shuffleQueues(queues []string) []string { 231 | wq := make([]string, len(queues)) 232 | copy(wq, queues) 233 | 234 | rand.Shuffle(len(wq), func(i, j int) { 235 | wq[i], wq[j] = wq[j], wq[i] 236 | }) 237 | 238 | return wq 239 | } 240 | 241 | // uniqQueues returns a slice of length len, of the unique elements while maintaining order. 242 | // The underlying array is modified to avoid allocating another one. 243 | func uniqQueues(length int, queues []string) []string { 244 | // Record the unique values and position. 245 | pos := 0 246 | uniqMap := make(map[string]int) 247 | for idx := range queues { 248 | if _, ok := uniqMap[queues[idx]]; !ok { 249 | uniqMap[queues[idx]] = pos 250 | pos++ 251 | } 252 | } 253 | 254 | // Reuse the copied array, by updating the values. 255 | for queue, position := range uniqMap { 256 | queues[position] = queue 257 | } 258 | 259 | // Slice only what we need. 260 | return queues[:length] 261 | } 262 | 263 | func dumpThreads(logg Logger) { 264 | buf := make([]byte, 64*1024) 265 | _ = runtime.Stack(buf, true) 266 | logg.Info("FULL PROCESS THREAD DUMP:") 267 | logg.Info(string(buf)) 268 | } 269 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # faktory_worker_go 2 | 3 | ![travis](https://travis-ci.org/contribsys/faktory_worker_go.svg?branch=master) 4 | 5 | This repository provides a Faktory worker process for Go apps. 6 | This worker process fetches background jobs from the Faktory server and processes them. 7 | 8 | How is this different from all the other Go background worker libraries? 9 | They all use Redis or another "dumb" datastore. 10 | This library is far simpler because the Faktory server implements most of the data storage, retry logic, Web UI, etc. 11 | 12 | # Installation 13 | 14 | You must install [Faktory](https://github.com/contribsys/faktory) first. 15 | Then: 16 | 17 | ``` 18 | go get -u github.com/contribsys/faktory_worker_go 19 | ``` 20 | 21 | # Usage 22 | 23 | To process background jobs, follow these steps: 24 | 25 | 1. Register your job types and their associated funcs 26 | 2. Set a few optional parameters 27 | 3. Start the processing 28 | 29 | There are a couple ways to stop the process. 30 | In this example, send the TERM or INT signal. 31 | 32 | ```go 33 | package main 34 | 35 | import ( 36 | "context" 37 | "log" 38 | 39 | worker "github.com/contribsys/faktory_worker_go" 40 | ) 41 | 42 | func someFunc(ctx context.Context, args ...interface{}) error { 43 | help := worker.HelperFor(ctx) 44 | log.Printf("Working on job %s\n", help.Jid()) 45 | return nil 46 | } 47 | 48 | func main() { 49 | mgr := worker.NewManager() 50 | 51 | // register job types and the function to execute them 52 | mgr.Register("SomeJob", someFunc) 53 | //mgr.Register("AnotherJob", anotherFunc) 54 | 55 | // use up to N goroutines to execute jobs 56 | mgr.Concurrency = 20 57 | // wait up to 25 seconds to let jobs in progress finish 58 | mgr.ShutdownTimeout = 25 * time.Second 59 | 60 | // pull jobs from these queues, in this order of precedence 61 | mgr.ProcessStrictPriorityQueues("critical", "default", "bulk") 62 | 63 | // alternatively you can use weights to avoid starvation 64 | //mgr.ProcessWeightedPriorityQueues(map[string]int{"critical":3, "default":2, "bulk":1}) 65 | 66 | // Start processing jobs, this method does not return. 67 | mgr.Run() 68 | } 69 | ``` 70 | 71 | Alternatively you can control the stopping of the Manager using 72 | `RunWithContext`. **You must process any signals yourself.** 73 | 74 | ```go 75 | package main 76 | 77 | import ( 78 | "context" 79 | "log" 80 | "os" 81 | "os/signal" 82 | "syscall" 83 | 84 | worker "github.com/contribsys/faktory_worker_go" 85 | ) 86 | 87 | func someFunc(ctx context.Context, args ...interface{}) error { 88 | help := worker.HelperFor(ctx) 89 | log.Printf("Working on job %s\n", help.Jid()) 90 | return nil 91 | } 92 | 93 | func main() { 94 | ctx, cancel := context.WithCancel(context.Background()) 95 | mgr := worker.NewManager() 96 | 97 | // register job types and the function to execute them 98 | mgr.Register("SomeJob", someFunc) 99 | //mgr.Register("AnotherJob", anotherFunc) 100 | 101 | // use up to N goroutines to execute jobs 102 | mgr.Concurrency = 20 103 | // wait up to 25 seconds to let jobs in progress finish 104 | mgr.ShutdownTimeout = 25 * time.Second 105 | 106 | // pull jobs from these queues, in this order of precedence 107 | mgr.ProcessStrictPriorityQueues("critical", "default", "bulk") 108 | 109 | // alternatively you can use weights to avoid starvation 110 | //mgr.ProcessWeightedPriorityQueues(map[string]int{"critical":3, "default":2, "bulk":1}) 111 | 112 | go func(){ 113 | // Start processing jobs in background routine, this method does not return 114 | // unless an error is returned or cancel() is called 115 | mgr.RunWithContext(ctx) 116 | }() 117 | 118 | go func() { 119 | stopSignals := []os.Signal{ 120 | syscall.SIGTERM, 121 | syscall.SIGINT, 122 | // TODO Implement the TSTP signal to call mgr.Quiet() 123 | } 124 | stop := make(chan os.Signal, len(stopSignals)) 125 | for _, s := range stopSignals { 126 | signal.Notify(stop, s) 127 | } 128 | 129 | for { 130 | select { 131 | case <-ctx.Done(): 132 | return 133 | case <-stop: 134 | break 135 | } 136 | } 137 | 138 | _ = time.AfterFunc(mgr.ShutdownTimeout, cancel) 139 | _ = mgr.Terminate(true) // never returns 140 | }() 141 | 142 | <-ctx.Done() 143 | } 144 | ``` 145 | 146 | # Middleware 147 | 148 | Attach middleware if you want to run code around every job execution with full access to the Job object. Returning an error will FAIL the job, returning nil will ACK it. 149 | 150 | ```go 151 | mgr := worker.NewManager() 152 | mgr.Use(func(ctx context.Context, job *faktory.Job, next func(context.Context) error) error { 153 | modctx := context.WithValue(ctx, EXAMPLE, 4.0) 154 | return next(modctx) 155 | }) 156 | ``` 157 | 158 | # Testing 159 | 160 | `faktory_worker_go` provides helpers that allow you to configure tests to execute jobs inline if you prefer. In this example, the application has defined its own wrapper function for `client.Push`. 161 | 162 | ```go 163 | import ( 164 | faktory "github.com/contribsys/faktory/client" 165 | worker "github.com/contribsys/faktory_worker_go" 166 | ) 167 | 168 | func Push(mgr worker.Manager, job *faktory.Job) error { 169 | if viper.GetBool("faktory_inline") { 170 | return syntheticPush(mgr worker.Manager, job) 171 | } 172 | return realPush(job) 173 | } 174 | 175 | func syntheticPush(mgr worker.Manager, job *faktory.Job) error { 176 | err := mgr.InlineDispatch(job) 177 | if err != nil { 178 | return errors.Wrap(err, "syntheticPush failed") 179 | } 180 | 181 | return nil 182 | } 183 | 184 | func realPush(job *faktory.Job) error { 185 | client, err := faktory.Open() 186 | if err != nil { 187 | return errors.Wrap(err, "failed to open Faktory client connection") 188 | } 189 | 190 | err = client.Push(job) 191 | if err != nil { 192 | return errors.Wrap(err, "failed to enqueue Faktory job") 193 | } 194 | 195 | return nil 196 | } 197 | ``` 198 | 199 | # FAQ 200 | 201 | * How do I specify the Faktory server location? 202 | 203 | By default, it will use localhost:7419 which is sufficient for local development. 204 | Use FAKTORY\_URL to specify the URL, e.g. `tcp://faktory.example.com:12345` or 205 | use FAKTORY\_PROVIDER to specify the environment variable which does 206 | contain the URL: FAKTORY\_PROVIDER=FAKTORYTOGO\_URL. This level of 207 | indirection is useful for SaaSes, Heroku Addons, etc. 208 | 209 | * How do I push new jobs to Faktory? 210 | 211 | 1. Inside a job, you can check out a connection from the Pool of Faktory 212 | connections using the job helper's `With` method: 213 | ```go 214 | func someFunc(ctx context.Context, args ...interface{}) error { 215 | help := worker.HelperFor(ctx) 216 | return help.With(func(cl *faktory.Client) error { 217 | job := faktory.NewJob("SomeJob", 1, 2, 3) 218 | return cl.Push(job) 219 | }) 220 | } 221 | ``` 222 | 2. You can always open a client connection to Faktory directly but this 223 | won't perform as well: 224 | ```go 225 | import ( 226 | faktory "github.com/contribsys/faktory/client" 227 | ) 228 | 229 | client, err := faktory.Open() 230 | job := faktory.NewJob("SomeJob", 1, 2, 3) 231 | err = client.Push(job) 232 | ``` 233 | 234 | **NB:** Client instances are **not safe to share**, you can use a Pool of Clients 235 | which is thread-safe. 236 | 237 | See the Faktory Client API for 238 | [Go](https://github.com/contribsys/faktory/blob/main/client) or 239 | [Ruby](https://github.com/contribsys/faktory_worker_ruby/blob/main/lib/faktory/client.rb). 240 | You can implement a Faktory client in any programming language. 241 | See [the wiki](https://github.com/contribsys/faktory/wiki) for details. 242 | 243 | # Author 244 | 245 | Mike Perham, https://ruby.social/@getajobmike 246 | 247 | # License 248 | 249 | This codebase is licensed via the Mozilla Public License, v2.0. https://choosealicense.com/licenses/mpl-2.0/ 250 | -------------------------------------------------------------------------------- /manager.go: -------------------------------------------------------------------------------- 1 | package faktory_worker 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "math/rand" 8 | "os" 9 | "strconv" 10 | "sync" 11 | "time" 12 | 13 | faktory "github.com/contribsys/faktory/client" 14 | ) 15 | 16 | // Manager coordinates the processes for the worker. It is responsible for 17 | // starting and stopping goroutines to perform work at the desired concurrency level 18 | type Manager struct { 19 | mut sync.Mutex 20 | 21 | Concurrency int 22 | Logger Logger 23 | ProcessWID string 24 | Labels []string 25 | Pool *faktory.Pool 26 | ShutdownTimeout time.Duration 27 | 28 | queues []string 29 | middleware []MiddlewareFunc 30 | state string // "", "quiet" or "terminate" 31 | // The done channel will always block unless 32 | // the system is shutting down. 33 | done chan interface{} 34 | shutdownWaiter *sync.WaitGroup 35 | jobHandlers map[string]Handler 36 | eventHandlers map[lifecycleEventType][]LifecycleEventHandler 37 | cancelFunc context.CancelFunc 38 | 39 | // This only needs to be computed once. Store it here to keep things fast. 40 | weightedPriorityQueuesEnabled bool 41 | weightedQueues []string 42 | } 43 | 44 | // Register a handler for the given jobtype. It is expected that all jobtypes 45 | // are registered upon process startup. 46 | // 47 | // mgr.Register("ImportantJob", ImportantFunc) 48 | func (mgr *Manager) Register(name string, fn Perform) { 49 | mgr.jobHandlers[name] = func(ctx context.Context, job *faktory.Job) error { 50 | return fn(ctx, job.Args...) 51 | } 52 | } 53 | 54 | // isRegistered checks if a given job name is registered with the manager. 55 | // 56 | // mgr.isRegistered("SomeJob") 57 | func (mgr *Manager) isRegistered(name string) bool { 58 | _, ok := mgr.jobHandlers[name] 59 | 60 | return ok 61 | } 62 | 63 | // dispatch immediately executes a job, including all middleware. 64 | func (mgr *Manager) dispatch(ctx context.Context, job *faktory.Job) error { 65 | perform := mgr.jobHandlers[job.Type] 66 | 67 | return dispatch(jobContext(ctx, mgr.Pool, job), mgr.middleware, job, perform) 68 | } 69 | 70 | // serializeArgs performs a JSON round trip on job arguments to match Faktory behavior 71 | func serializeArgs(args []interface{}) ([]interface{}, error) { 72 | data, err := json.Marshal(args) 73 | if err != nil { 74 | return nil, err 75 | } 76 | var result []interface{} 77 | err = json.Unmarshal(data, &result) 78 | if err != nil { 79 | return nil, err 80 | } 81 | return result, nil 82 | } 83 | 84 | // InlineDispatch is designed for testing. It immediate executes a job, including all middleware, 85 | // as well as performs manager setup if needed. 86 | func (mgr *Manager) InlineDispatch(job *faktory.Job) error { 87 | if !mgr.isRegistered(job.Type) { 88 | return fmt.Errorf("failed to dispatch inline for job type %s; job not registered", job.Type) 89 | } 90 | 91 | err := mgr.setUpWorkerProcess() 92 | if err != nil { 93 | return fmt.Errorf("couldn't set up worker process for inline dispatch - %w", err) 94 | } 95 | 96 | serializedArgs, err := serializeArgs(job.Args) 97 | if err != nil { 98 | return fmt.Errorf("failed to serialize job arguments - %w", err) 99 | } 100 | job.Args = serializedArgs 101 | 102 | err = mgr.dispatch(context.Background(), job) 103 | if err != nil { 104 | return fmt.Errorf("job was dispatched inline but failed. Job type %s, with args %+v - %w", job.Type, job.Args, err) 105 | } 106 | 107 | return nil 108 | } 109 | 110 | // Register a callback to be fired when a process lifecycle event occurs. 111 | // These are useful for hooking into process startup or shutdown. 112 | func (mgr *Manager) On(event lifecycleEventType, fn LifecycleEventHandler) { 113 | mgr.eventHandlers[event] = append(mgr.eventHandlers[event], fn) 114 | } 115 | 116 | // After calling Quiet(), no more jobs will be pulled 117 | // from Faktory by this process. 118 | func (mgr *Manager) Quiet() { 119 | mgr.mut.Lock() 120 | defer mgr.mut.Unlock() 121 | 122 | if mgr.state == "quiet" { 123 | return 124 | } 125 | 126 | mgr.Logger.Info("Quieting...") 127 | mgr.state = "quiet" 128 | mgr.fireEvent(Quiet) 129 | } 130 | 131 | // Terminate signals that the various components should shutdown. 132 | // Blocks on the shutdownWaiter until all components have finished. 133 | func (mgr *Manager) Terminate(reallydie bool) { 134 | mgr.mut.Lock() 135 | defer mgr.mut.Unlock() 136 | 137 | if mgr.state == "terminate" { 138 | return 139 | } 140 | 141 | mgr.Logger.Info("Shutting down...") 142 | mgr.state = "terminate" 143 | close(mgr.done) 144 | 145 | if mgr.cancelFunc != nil { 146 | // cancel any jobs which are lingering 147 | time.AfterFunc(mgr.ShutdownTimeout, mgr.cancelFunc) 148 | } 149 | mgr.fireEvent(Shutdown) 150 | mgr.shutdownWaiter.Wait() // can't pass this point until all jobs are done 151 | 152 | mgr.Pool.Close() 153 | mgr.Logger.Info("Goodbye") 154 | if reallydie { 155 | os.Exit(0) // nolint:gocritic 156 | } 157 | } 158 | 159 | // NewManager returns a new manager with default values. 160 | func NewManager() *Manager { 161 | return &Manager{ 162 | Concurrency: 20, 163 | Logger: NewStdLogger(), 164 | Labels: []string{"golang-" + Version}, 165 | Pool: nil, 166 | 167 | // best practice is to give jobs 25 seconds to finish their work 168 | // and then use the last 5 seconds to force any lingering jobs to 169 | // stop by closing their Context. Many cloud services default to a 170 | // hard 30 second timeout beforing KILLing the process. 171 | ShutdownTimeout: 25 * time.Second, 172 | 173 | state: "", 174 | queues: []string{"default"}, 175 | done: make(chan interface{}), 176 | shutdownWaiter: &sync.WaitGroup{}, 177 | jobHandlers: map[string]Handler{}, 178 | eventHandlers: map[lifecycleEventType][]LifecycleEventHandler{ 179 | Startup: {}, 180 | Quiet: {}, 181 | Shutdown: {}, 182 | }, 183 | weightedPriorityQueuesEnabled: false, 184 | weightedQueues: []string{}, 185 | } 186 | } 187 | 188 | func (mgr *Manager) setUpWorkerProcess() error { 189 | mgr.mut.Lock() 190 | defer mgr.mut.Unlock() 191 | 192 | if mgr.state != "" { 193 | return fmt.Errorf("cannot start worker process for the mananger in %v state", mgr.state) 194 | } 195 | 196 | // This will signal to Faktory that all connections from this process 197 | // are worker connections. 198 | if len(mgr.ProcessWID) == 0 { 199 | faktory.RandomProcessWid = strconv.FormatInt(rand.Int63(), 32) 200 | } else { 201 | faktory.RandomProcessWid = mgr.ProcessWID 202 | } 203 | // Set labels to be displayed in the UI 204 | faktory.Labels = mgr.Labels 205 | 206 | if mgr.Pool == nil { 207 | pool, err := faktory.NewPool(mgr.Concurrency + 2) 208 | if err != nil { 209 | return fmt.Errorf("couldn't create Faktory connection pool: %w", err) 210 | } 211 | mgr.Pool = pool 212 | } 213 | 214 | return nil 215 | } 216 | 217 | // RunWithContext starts processing jobs. The method will return if an error is encountered while starting. 218 | // If the context is present then os signals will be ignored, the context must be canceled for the method to return 219 | // after running. 220 | func (mgr *Manager) RunWithContext(ctx context.Context) error { 221 | err := mgr.boot(ctx) 222 | if err != nil { 223 | return err 224 | } 225 | <-ctx.Done() 226 | mgr.Terminate(false) 227 | return nil 228 | } 229 | 230 | func (mgr *Manager) boot(ctx context.Context) error { 231 | err := mgr.setUpWorkerProcess() 232 | if err != nil { 233 | return err 234 | } 235 | 236 | mgr.fireEvent(Startup) 237 | go heartbeat(mgr) 238 | 239 | mgr.Logger.Infof("faktory_worker_go %s PID %d now ready to process jobs", Version, os.Getpid()) 240 | mgr.Logger.Debugf("Using Faktory Client API %s", faktory.Version) 241 | for i := 0; i < mgr.Concurrency; i++ { 242 | go process(ctx, mgr, i) 243 | } 244 | 245 | return nil 246 | } 247 | 248 | // Run starts processing jobs. 249 | // This method does not return unless an error is encountered while starting. 250 | func (mgr *Manager) Run() error { 251 | ctx := context.Background() 252 | ctx, cancel := context.WithCancel(ctx) 253 | mgr.cancelFunc = cancel 254 | err := mgr.boot(ctx) 255 | if err != nil { 256 | return err 257 | } 258 | for { 259 | sig := <-hookSignals() 260 | mgr.handleEvent(signalMap[sig]) 261 | } 262 | } 263 | 264 | // One of the Process*Queues methods should be called once before Run() 265 | func (mgr *Manager) ProcessStrictPriorityQueues(queues ...string) { 266 | mgr.queues = queues 267 | mgr.weightedPriorityQueuesEnabled = false 268 | } 269 | 270 | func (mgr *Manager) ProcessWeightedPriorityQueues(queues map[string]int) { 271 | uniqueQueues := queueKeys(queues) 272 | weightedQueues := expandWeightedQueues(queues) 273 | 274 | mgr.queues = uniqueQueues 275 | mgr.weightedQueues = weightedQueues 276 | mgr.weightedPriorityQueuesEnabled = true 277 | } 278 | 279 | func (mgr *Manager) queueList() []string { 280 | if mgr.weightedPriorityQueuesEnabled { 281 | sq := shuffleQueues(mgr.weightedQueues) 282 | return uniqQueues(len(mgr.queues), sq) 283 | } 284 | return mgr.queues 285 | } 286 | 287 | func (mgr *Manager) fireEvent(event lifecycleEventType) { 288 | for _, fn := range mgr.eventHandlers[event] { 289 | err := fn(mgr) 290 | if err != nil { 291 | mgr.Logger.Errorf("Error running lifecycle event handler: %v", err) 292 | } 293 | } 294 | } 295 | 296 | func (mgr *Manager) with(fn func(cl *faktory.Client) error) error { 297 | if mgr.Pool == nil { 298 | panic("No Pool set on Manager, have you called manager.Run() yet?") 299 | } 300 | return mgr.Pool.With(fn) 301 | } 302 | 303 | func (mgr *Manager) handleEvent(sig string) string { 304 | if sig == mgr.state { 305 | return mgr.state 306 | } 307 | if sig == "quiet" && mgr.state == "terminate" { 308 | // this is a no-op, a terminating process is quiet already 309 | return mgr.state 310 | } 311 | 312 | switch sig { 313 | case "terminate": 314 | go func() { 315 | mgr.Terminate(true) 316 | }() 317 | case "quiet": 318 | go func() { 319 | mgr.Quiet() 320 | }() 321 | case "dump": 322 | dumpThreads(mgr.Logger) 323 | } 324 | 325 | return "" 326 | } 327 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | --------------------------------------------------------------------------------