├── LICENSE ├── README.md ├── basic_test.go ├── build.go ├── go.mod ├── go.sum ├── help.go └── task.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 omeid 4 | 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kargar [![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/omeid/kargar) 2 |

3 | 4 |

5 | 6 | Kagrar is a concurrency-aware task harness and dependency management with first-class support for Deadlines, Cancelation, and task-labeled logging. 7 | 8 | 9 | Kargar allows you to cancel tasks. 10 | 11 | ```go 12 | ctx, cancel := context.WithCancel(context.Background()) 13 | 14 | b := kargar.NewBuild(ctx) 15 | 16 | b.Add(kargar.Task{ 17 | 18 | Name: "say-hello", 19 | Usage: "This tasks is self-documented, it says hello for every second.", 20 | 21 | Action: func(ctx context.Context) error { 22 | 23 | second := time.NewTicker(time.Second) 24 | 25 | for { 26 | select { 27 | 28 | case <-second.C: 29 | ctx.Info("Hello!") 30 | case <-ctx.Done(): 31 | return ctx.Err() 32 | } 33 | } 34 | }, 35 | }) 36 | 37 | go func() { 38 | time.Sleep(4 * time.Second) 39 | cancel() 40 | }() 41 | 42 | err := b.Run("say-hello") 43 | if err != nil { 44 | log.Println(err) 45 | } 46 | ``` 47 | 48 | ```sh 49 | INFO[0001] Hello! task=say-hello 50 | INFO[0002] Hello! task=say-hello 51 | INFO[0003] Hello! task=say-hello 52 | INFO[0004] Hello! task=say-hello 53 | ERRO[0004] context canceled 54 | ``` 55 | 56 | To avoid race-conditions, there will be one and only one instance of any given task running at any given time. 57 | 58 | 59 | ```go 60 | b := kargar.New() 61 | 62 | b.Add( 63 | kargar.Task{ 64 | 65 | Name: "slow-dependency", 66 | Usage: "This is a slow task, takes 3 seconds before it prints time.", 67 | 68 | Action: func(ctx context.Context) error { 69 | 70 | ctx.Warn("I am pretty slow.") 71 | for { 72 | select { 73 | 74 | case now := <-time.After(3 * time.Second): 75 | ctx.Infof("Time is %s", now.Format(time.Kitchen)) 76 | return nil 77 | case <-ctx.Done(): 78 | return ctx.Err() 79 | } 80 | } 81 | }, 82 | }, 83 | 84 | kargar.Task{ 85 | Name: "a", 86 | Usage: "This task depends on 'slow-dependency.'", 87 | Deps: []string{"slow-dependency"}, 88 | Action: kargar.Noop(), 89 | }, 90 | 91 | kargar.Task{ 92 | Name: "b", 93 | Usage: "This task depends on 'slow-dependency.'", 94 | Deps: []string{"slow-dependency"}, 95 | Action: kargar.Noop(), 96 | }, 97 | 98 | kargar.Task{ 99 | Name: "c", 100 | Usage: "This task depends on 'slow-dependency.'", 101 | Deps: []string{"slow-dependency"}, 102 | Action: kargar.Noop(), 103 | }, 104 | ) 105 | 106 | ctx := b.Context() 107 | var wg sync.WaitGroup 108 | 109 | for _, t := range []string{"a", "b", "c"} { 110 | wg.Add(1) 111 | go func(t string) { 112 | defer wg.Done() 113 | err := b.Run(t) 114 | if err != nil { 115 | ctx.Error(err) 116 | } 117 | }(t) 118 | } 119 | 120 | wg.Wait() 121 | ``` 122 | 123 | ``` 124 | WARN[0000] I am pretty slow. parent=c task=slow-dependency 125 | INFO[0003] Time is 2:14PM parent=c task=slow-dependency 126 | WARN[0003] I am pretty slow. parent=b task=slow-dependency 127 | INFO[0006] Time is 2:14PM parent=b task=slow-dependency 128 | WARN[0006] I am pretty slow. parent=a task=slow-dependency 129 | INFO[0009] Time is 2:14PM parent=a task=slow-dependency 130 | ``` 131 | 132 | # Kar 133 | 134 | To build and run Kargar tasks from CLI, see [kar](https://github.com/omeid/kar). 135 | 136 | ### LICENSE 137 | [MIT](LICENSE). 138 | -------------------------------------------------------------------------------- /basic_test.go: -------------------------------------------------------------------------------- 1 | package kargar_test 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | "time" 7 | 8 | "github.com/omeid/gonzo/context" 9 | "github.com/omeid/kargar" 10 | ) 11 | 12 | func TestCancel(t *testing.T) { 13 | 14 | ctx, cancel := context.WithCancel(context.Background()) 15 | 16 | b := kargar.NewBuild(ctx) 17 | 18 | b.Add(kargar.Task{ 19 | 20 | Name: "say-hello", 21 | Usage: "This tasks is self-documented, it says hello for every second.", 22 | 23 | Action: func(ctx context.Context) error { 24 | 25 | second := time.NewTicker(time.Second) 26 | 27 | for { 28 | select { 29 | 30 | case <-second.C: 31 | ctx.Info("Hello!") 32 | case <-ctx.Done(): 33 | return ctx.Err() 34 | } 35 | } 36 | }, 37 | }) 38 | 39 | go func() { 40 | time.Sleep(4 * time.Second) 41 | cancel() 42 | }() 43 | 44 | err := b.Run("say-hello") 45 | if err != nil { 46 | b.Context().Error(err) 47 | } 48 | } 49 | 50 | func TestConcurrency(t *testing.T) { 51 | 52 | b := kargar.New() 53 | 54 | b.Add( 55 | kargar.Task{ 56 | 57 | Name: "slow-dependency", 58 | Usage: "This is a slow task, takes 3 seconds before it prints time.", 59 | 60 | Action: func(ctx context.Context) error { 61 | 62 | ctx.Warn("I am pretty slow.") 63 | for { 64 | select { 65 | 66 | case now := <-time.After(3 * time.Second): 67 | ctx.Infof("Time is %s", now.Format(time.Kitchen)) 68 | return nil 69 | case <-ctx.Done(): 70 | return ctx.Err() 71 | } 72 | } 73 | }, 74 | }, 75 | 76 | kargar.Task{ 77 | Name: "a", 78 | Usage: "This task depends on 'slow-dependency.'", 79 | Deps: []string{"slow-dependency"}, 80 | Action: kargar.Noop(), 81 | }, 82 | 83 | kargar.Task{ 84 | Name: "b", 85 | Usage: "This task depends on 'slow-dependency.'", 86 | Deps: []string{"slow-dependency"}, 87 | Action: kargar.Noop(), 88 | }, 89 | 90 | kargar.Task{ 91 | Name: "c", 92 | Usage: "This task depends on 'slow-dependency.'", 93 | Deps: []string{"slow-dependency"}, 94 | Action: kargar.Noop(), 95 | }, 96 | ) 97 | 98 | ctx := b.Context() 99 | var wg sync.WaitGroup 100 | 101 | for _, t := range []string{"a", "b", "c"} { 102 | wg.Add(1) 103 | go func(t string) { 104 | defer wg.Done() 105 | err := b.Run(t) 106 | if err != nil { 107 | ctx.Error(err) 108 | } 109 | }(t) 110 | } 111 | 112 | wg.Wait() 113 | } 114 | -------------------------------------------------------------------------------- /build.go: -------------------------------------------------------------------------------- 1 | package kargar 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "sync" 9 | 10 | "github.com/omeid/gonzo/context" 11 | ) 12 | 13 | var ctx = context.Background() 14 | 15 | //Meta holds information about the build. 16 | type Meta struct { 17 | // The name of the program. Defaults to os.Args[0] 18 | Name string 19 | // Description of the program. 20 | Usage string 21 | // Version of the program 22 | Version string 23 | // Author 24 | Author string 25 | // Author e-mail 26 | Email string 27 | 28 | // License 29 | License string 30 | } 31 | 32 | // Build is a simple build harness that you can register tasks and their 33 | // dependencies and then run them. 34 | type Build struct { 35 | Meta Meta 36 | ctx context.Context 37 | 38 | tasks taskstack 39 | 40 | //cleanups []func() 41 | //runcleanups bool 42 | 43 | lock sync.Mutex 44 | } 45 | 46 | // New returns a Build with a contex with no deadline or values and is never canceled. 47 | func New() *Build { 48 | return NewBuild( 49 | context.Background(), 50 | ) 51 | } 52 | 53 | // NewBuild returns a Build using the provided Context. 54 | func NewBuild(ctx context.Context) *Build { 55 | //done := make(chan struct{}) 56 | return &Build{ 57 | ctx: ctx, 58 | tasks: make(taskstack), 59 | lock: sync.Mutex{}, 60 | } 61 | } 62 | 63 | // Context returns the current builds context. 64 | // Useful for loging. 65 | func (b *Build) Context() context.Context { 66 | return b.ctx 67 | } 68 | 69 | // Add registers the provided tasks to the build. 70 | // Circular Dependencies are not allowed. 71 | func (b *Build) Add(tasks ...Task) error { 72 | 73 | b.lock.Lock() 74 | defer b.lock.Unlock() 75 | for i, T := range tasks { 76 | if T.Name == "" { 77 | return fmt.Errorf("Task %d Missing Name", i) 78 | } 79 | 80 | if T.Action == nil { 81 | return fmt.Errorf("Task %s Missing Action", T.Name) 82 | } 83 | 84 | if T.Usage == "" { 85 | return fmt.Errorf("Task %s Missing Usage", T.Name) 86 | } 87 | 88 | if _, ok := b.tasks[T.Name]; ok { 89 | return fmt.Errorf("Duplicate task: %s", T.Name) 90 | } 91 | t := &task{Task: T, deps: make(taskstack), running: false} 92 | 93 | for _, dep := range t.Deps { 94 | d, ok := b.tasks[dep] 95 | if !ok { 96 | return fmt.Errorf("Missing Task %s. Required by Task %s", dep, t.Name) 97 | } 98 | _, ok = d.deps[t.Name] 99 | if ok { 100 | return fmt.Errorf("Circular dependency %s requies %s and around", d.Name, t.Name) 101 | } 102 | t.deps[dep] = d 103 | } 104 | 105 | b.tasks[t.Name] = t 106 | } 107 | return nil 108 | } 109 | 110 | // ErrorNoSuchTask is returned when any of the given tasks does not exist. 111 | var ErrorNoSuchTask = fmt.Errorf("No Such Task") 112 | 113 | //RunFor runs a task using an alternative context. 114 | //This this is typically useful when you want to dynamically 115 | //invoked tasks from another task but still maintain proper 116 | //context hireachy. 117 | func (b *Build) RunFor(ctx context.Context, tasks ...string) error { 118 | 119 | if !kargar() { 120 | return errors.New("KARGAR=false, escaping run") 121 | } 122 | 123 | for _, name := range tasks { 124 | select { 125 | case <-ctx.Done(): 126 | b.ctx.Warn("Build Canacled.") 127 | 128 | default: 129 | t, ok := b.tasks[name] 130 | if !ok { 131 | b.ctx.Errorf("Missing Task %s", name) 132 | return ErrorNoSuchTask 133 | } 134 | err := t.run(ctx) 135 | if err != nil { 136 | return err 137 | } 138 | } 139 | } 140 | return nil 141 | } 142 | 143 | //Run runs the provided lists of tasks. 144 | func (b *Build) Run(tasks ...string) error { 145 | return b.RunFor(b.ctx, tasks...) 146 | } 147 | 148 | func kargar() bool { 149 | kargar := os.Getenv("KARGAR") 150 | if kargar == "" { 151 | return true 152 | } 153 | k, err := strconv.ParseBool(kargar) 154 | if err != nil { 155 | ctx.Fatal(err) 156 | } 157 | return k 158 | } 159 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/omeid/kargar 2 | 3 | go 1.12 4 | 5 | require github.com/omeid/gonzo v0.0.0-20190807042340-9a491fec4a09 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 3 | github.com/omeid/gonzo v0.0.0-20190807042340-9a491fec4a09 h1:n+ZCpn/XgVDdyjhugHJB5+ERRW27Rr949LQuXZPjeWY= 4 | github.com/omeid/gonzo v0.0.0-20190807042340-9a491fec4a09/go.mod h1:1u/PwI9FfoPSWWR2U/hox5upe8/QGKnxISZwbYKuZQA= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 7 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 8 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 10 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 11 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 12 | -------------------------------------------------------------------------------- /help.go: -------------------------------------------------------------------------------- 1 | package kargar 2 | 3 | import ( 4 | "text/template" 5 | ) 6 | 7 | // BuildHelpTemplate holds the template used for generating 8 | // the help message for a build 9 | // TODO: Move this to /kar? 10 | var BuildHelpTemplate = `KARGAR: 11 | {{.Name}} - {{.Usage}} 12 | 13 | USAGE: 14 | {{.Name}} [global options] {task [flags]...} 15 | 16 | VERSION: 17 | {{.Version}}{{if or .Author .Email}} 18 | 19 | AUTHOR:{{if .Author}} 20 | {{.Author}}{{if .Email}} - <{{.Email}}>{{end}}{{else}} 21 | {{.Email}}{{end}}{{end}} 22 | 23 | TASKS: 24 | {{range .Tasks }}{{printf "%-15s %s" .Name .Usage}} 25 | {{end}}{{if .Flags}} 26 | GLOBAL OPTIONS: 27 | {{range .Flags}}{{.}} 28 | {{end}}{{end}} 29 | ` 30 | 31 | // TaskTemplate holds the template used to generate 32 | // the help message for a specific task. 33 | var TaskHelpTemplate = `TASK: 34 | {{.Name}} - {{.Usage}}{{if .Description}} 35 | 36 | DESCRIPTION: 37 | {{.Description}}{{end}}{{if .Deps}} 38 | 39 | DEPENDENCIES: 40 | {{ range .Deps }}{{ . }} 41 | {{ end }}{{ end }}{{ if .Flags }} 42 | 43 | OPTIONS: 44 | {{range .Flags}}{{.}} 45 | {{end}}{{ end }} 46 | ` 47 | 48 | //Help templates hold both Build and Task help templates. 49 | var HelpTemplate *template.Template 50 | 51 | func init() { 52 | HelpTemplate = template.Must(template.New("build").Parse(BuildHelpTemplate)) 53 | HelpTemplate = template.Must(HelpTemplate.New("task").Parse(TaskHelpTemplate)) 54 | } 55 | -------------------------------------------------------------------------------- /task.go: -------------------------------------------------------------------------------- 1 | package kargar 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/omeid/gonzo/context" 7 | ) 8 | 9 | // Action is a function that is called when a task is run. 10 | type Action func(context.Context) error 11 | 12 | // Noop is an Action that does nothing and returns `nil` immediately. 13 | // For clarity and to avoid weird bugs, every task must have an Action 14 | // Noop is provided for tasks that are only used to group a collection 15 | // of tasks as dependency. 16 | func Noop() Action { 17 | return func(ctx context.Context) error { return nil } 18 | } 19 | 20 | // Task holds the meta information and an action. 21 | type Task struct { 22 | // Taks name 23 | Name string 24 | // A short description of the task. 25 | Usage string 26 | // A long explanation of how the task works. 27 | Description string 28 | // List of dependencies. 29 | // When running a task, the dependencies will be run in the order 30 | Deps []string 31 | // The function to call when the task is invoked. 32 | Action Action 33 | } 34 | 35 | type task struct { 36 | Task 37 | deps taskstack 38 | lock sync.Mutex 39 | done <-chan struct{} 40 | running bool 41 | } 42 | 43 | type taskstack map[string]*task 44 | 45 | func (t *task) run(ctx context.Context) error { 46 | t.lock.Lock() 47 | defer func() { 48 | t.running = false 49 | t.lock.Unlock() 50 | }() 51 | 52 | if task, ok := ctx.Value("task").(string); ok { 53 | ctx = context.WithValue(ctx, "parent", task) 54 | } 55 | 56 | if t.Name != "default" { 57 | ctx = context.WithValue(ctx, "task", t.Name) 58 | } 59 | 60 | ctx, cancel := context.WithCancel(ctx) 61 | 62 | var once sync.Once; 63 | 64 | ctx.Debug("start") 65 | var wg sync.WaitGroup 66 | for _, t := range t.deps { 67 | select { 68 | case <-ctx.Done(): 69 | break 70 | default: 71 | wg.Add(1) 72 | go func(t *task) { 73 | defer wg.Done() 74 | ctx.Debug("Waiting for %s", t.Name) 75 | err := t.run(ctx) 76 | if err != nil { 77 | once.Do(func() { 78 | ctx.Warnf("%s failed. Giving up!", t.Name); 79 | }); 80 | 81 | cancel() 82 | if err != context.Canceled { 83 | ctx.Error(err) 84 | } 85 | } 86 | }(t) 87 | } 88 | } 89 | wg.Wait() 90 | 91 | err := ctx.Err() 92 | if err != nil { 93 | if err == context.Canceled { 94 | return nil 95 | } 96 | return err 97 | } 98 | 99 | t.running = true 100 | err = t.Action(ctx) 101 | if err == nil { 102 | ctx.Debug("Done.") 103 | } 104 | return err 105 | } 106 | --------------------------------------------------------------------------------