gush 0.0.1 → 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ee767bd97e343cac8bf395a65d2d19c63abd638b
4
- data.tar.gz: d5a41126c1895e505871ae108613955466724829
3
+ metadata.gz: 7fa19439bb45ef12b63b393f7956aca1edd96ebe
4
+ data.tar.gz: 0ce4dc6cdc209c66e33248da59390b017a54a4e7
5
5
  SHA512:
6
- metadata.gz: ce471a04c947ff17fc3864644223463da25ca52aa59e34cc2118bcb607526db2b394c79b6b708af23bc29a7ae4efb5647e5bd19a09e51d9df253e1b8442fac42
7
- data.tar.gz: dda4e7862751f8c7a7cc625f7bdafaca54dc6718a96689346d010d4ae8f6b3350dff9cd07cf461ad369930eb90f75e0bf33242f685be85ed0482065e8f8f367d
6
+ metadata.gz: 79fa056cf641d929d5f61985a12cc734a9fd4946c3b6ada2d31fd1bbec3b34b2bb7fd2dbc22821a0e42710a37baa1fc72955e867fcbab7a1582cea2ba82df9db
7
+ data.tar.gz: 229f3550ff30f57ff4cb27eb0cd177d962cd604be9f2d9e8b420172a4543461eef3c430f00096287d88a136e7dacd5b11160d133dab94c7831171b076368fee2
data/.travis.yml ADDED
@@ -0,0 +1,13 @@
1
+ language: ruby
2
+ script: "bundle exec rspec"
3
+ rvm:
4
+ - 2.0.0
5
+ - 2.1.6
6
+ - 2.2.2
7
+ services:
8
+ - redis-server
9
+ email:
10
+ recipients:
11
+ - piotrek@okonski.org
12
+ on_success: change
13
+ on_failure: always
data/Gemfile CHANGED
@@ -5,4 +5,4 @@ gemspec
5
5
 
6
6
  gem 'pry'
7
7
  gem 'yajl-ruby'
8
- gem 'bbq-spawn', git: 'git@github.com:drugpl/bbq-spawn.git'
8
+ gem "fakeredis", require: false
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Gush
1
+ # Gush [![Build Status](https://travis-ci.org/pokonski/gush.svg?branch=master)](https://travis-ci.org/pokonski/gush)
2
2
 
3
3
  Gush is a parallel workflow runner using only Redis as its message broker and Sidekiq for workers.
4
4
 
@@ -18,21 +18,9 @@ Or install it yourself as:
18
18
 
19
19
  ## Usage
20
20
 
21
- Your project should contain a file called `Gushfile.rb` which loads all the necessary workflows for Sidekiq to use.
22
-
23
- Example:
24
-
25
- ```ruby
26
- require_relative './lib/your_project'
27
-
28
- Dir[Rowlf.root.join("workflows/**/*.rb")].each do |file|
29
- require file
30
- end
31
- ```
32
-
33
21
  ### Defining workflows
34
22
 
35
- The DSL for defining jobs consists of a single `run` method.
23
+ The DSL for defining jobs consists of a single `run` method.
36
24
  Here is a complete example of a workflow you can create:
37
25
 
38
26
  ```ruby
@@ -63,6 +51,7 @@ bundle exec gush viz SampleWorkflow
63
51
  For the Workflow above, the graph will look like this:
64
52
 
65
53
  ![SampleWorkflow](http://i.imgur.com/SmeRRVT.png)
54
+
66
55
  ### Defining jobs
67
56
 
68
57
  Jobs are classes inheriting from `Gush::Job`:
@@ -76,35 +65,54 @@ class FetchJob1 < Gush::Job
76
65
  end
77
66
  ```
78
67
 
79
- ### Running
68
+ ### Running workflows
80
69
 
81
- #### 1. Register workflow
70
+ Now that we have defined our workflow we can use it:
82
71
 
83
- After you define your workflows and jobs, all you have to do is register them:
72
+ #### 1. Initialize and save it
84
73
 
74
+ ```ruby
75
+ flow = SampleWorkflow.new
76
+ flow.save # saves workflow and its jobs to Redis
85
77
  ```
86
- bundle exec gush create SampleWorkflow
87
- ```
88
78
 
89
- the command will return a unique workflow id you will use in later commands.
79
+ **or:** you can also use a shortcut:
80
+
81
+ ```ruby
82
+ flow = SampleWorkflow.create
83
+ ```
90
84
 
91
- #### 2. Run workers
85
+ #### 2. Start workflow
92
86
 
93
- This will start Sidekiq workers responsible for processing jobs
87
+ First you need to start Sidekiq workers:
94
88
 
95
89
  ```
96
90
  bundle exec gush workers
97
91
  ```
98
92
 
99
- #### 3. Start the workflow
100
-
101
- Use your workflow_id returned by `create` command.
93
+ and then start your workflow:
102
94
 
95
+ ```ruby
96
+ flow.start!
103
97
  ```
104
- bundle gush start <workflow_id>
98
+
99
+ Now Gush will start processing jobs in background using Sidekiq
100
+ in the order defined in `configure` method inside Workflow.
101
+
102
+
103
+ ### Checking status:
104
+
105
+ #### In Ruby:
106
+
107
+ ```ruby
108
+ flow.reload
109
+ flow.status
110
+ #=> :running|:pending|:finished|:failed
105
111
  ```
106
112
 
107
- ### 5. Check the status
113
+ `reload` is needed to see the latest status, since workflows are updated asynchronously.
114
+
115
+ #### Via CLI:
108
116
 
109
117
  - of a specific workflow:
110
118
 
@@ -113,15 +121,29 @@ bundle gush start <workflow_id>
113
121
  ```
114
122
 
115
123
  - of all created workflows:
116
-
124
+
117
125
  ```
118
126
  bundle gush list
119
127
  ```
120
128
 
129
+ ### Requiring workflows inside your projects
130
+
131
+ **Skip this step if using Gush inside Rails application, workflows will already be loaded**
132
+
133
+ When using Gush and its CLI commands you need a Gushfile.rb in root directory.
134
+ Gushfile should require all your Workflows and jobs, for example:
135
+
136
+ ```ruby
137
+ require_relative './lib/your_project'
138
+
139
+ Dir[Rails.root.join("app/workflows/**/*.rb")].each do |file|
140
+ require file
141
+ end
142
+ ```
121
143
 
122
144
  ## Contributing
123
145
 
124
- 1. Fork it ( http://github.com/lonelyplanet/gush/fork )
146
+ 1. Fork it ( http://github.com/pokonski/gush/fork )
125
147
  2. Create your feature branch (`git checkout -b my-new-feature`)
126
148
  3. Commit your changes (`git commit -am 'Add some feature'`)
127
149
  4. Push to the branch (`git push origin my-new-feature`)
data/bin/gush CHANGED
@@ -9,4 +9,10 @@ $:.unshift File.expand_path("../../lib", bin_file)
9
9
 
10
10
  require 'gush'
11
11
 
12
- Gush::CLI.start(ARGV)
12
+ begin
13
+ Gush::CLI.start(ARGV)
14
+ rescue Gush::WorkflowNotFound
15
+ puts "Workflow not found".red
16
+ rescue Gush::DependencyLevelTooDeep
17
+ puts "Dependency level too deep. Perhaps you have a dependency cycle?".red
18
+ end
data/gush.gemspec CHANGED
@@ -1,11 +1,10 @@
1
1
  # coding: utf-8
2
2
  lib = File.expand_path('../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'gush/version'
5
4
 
6
5
  Gem::Specification.new do |spec|
7
6
  spec.name = "gush"
8
- spec.version = Gush::VERSION
7
+ spec.version = "0.1"
9
8
  spec.authors = ["Piotrek Okoński"]
10
9
  spec.email = ["piotrek@okonski.org"]
11
10
  spec.summary = "Fast and distributed workflow runner using only Sidekiq and Redis"
@@ -17,15 +16,15 @@ Gem::Specification.new do |spec|
17
16
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
17
  spec.require_paths = ["lib"]
19
18
 
20
- spec.add_dependency "sidekiq", "~> 3.2.2"
21
- spec.add_dependency "yajl-ruby"
22
- spec.add_dependency "redis", "~> 3.0.0"
23
- spec.add_dependency "hiredis", "~> 0.5.2"
24
- spec.add_dependency "ruby-graphviz"
25
- spec.add_dependency "terminal-table"
26
- spec.add_dependency "colorize"
27
- spec.add_dependency "thor"
28
- spec.add_dependency "launchy"
19
+ spec.add_dependency "sidekiq", "~> 3.3.4"
20
+ spec.add_dependency "yajl-ruby", "~> 1.2.1"
21
+ spec.add_dependency "redis", "~> 3.2.1"
22
+ spec.add_dependency "hiredis", "~> 0.6.0"
23
+ spec.add_dependency "ruby-graphviz", "~> 1.2.2"
24
+ spec.add_dependency "terminal-table", "~> 1.4.5"
25
+ spec.add_dependency "colorize", "~> 0.7.7"
26
+ spec.add_dependency "thor", "~> 0.19.1"
27
+ spec.add_dependency "launchy", "~> 2.4.3"
29
28
  spec.add_development_dependency "bundler", "~> 1.5"
30
29
  spec.add_development_dependency "rake"
31
30
  spec.add_development_dependency "rspec", '~> 3.0.0'
@@ -0,0 +1,138 @@
1
+ module Gush
2
+ class CLI
3
+ class Overview
4
+ attr_reader :workflow
5
+
6
+ def initialize(workflow)
7
+ @workflow = workflow
8
+ end
9
+
10
+ def table
11
+ Terminal::Table.new(rows: rows)
12
+ end
13
+
14
+ def status
15
+ if workflow.failed?
16
+ failed_status
17
+ elsif workflow.running?
18
+ running_status
19
+ elsif workflow.finished?
20
+ "done".green
21
+ elsif workflow.stopped?
22
+ "stopped".red
23
+ else
24
+ "pending".light_white
25
+ end
26
+ end
27
+
28
+ def jobs_list(jobs)
29
+ "\nJobs list:\n".tap do |output|
30
+ jobs_by_type(jobs).each do |job|
31
+ output << job_to_list_element(job)
32
+ end
33
+ end
34
+ end
35
+
36
+ private
37
+ def rows
38
+ [].tap do |rows|
39
+ columns.each_pair do |name, value|
40
+ rows << [{alignment: :center, value: name}, value]
41
+ rows << :separator if name != "Status"
42
+ end
43
+ end
44
+ end
45
+
46
+ def columns
47
+ {
48
+ "ID" => workflow.id,
49
+ "Name" => workflow.class.to_s,
50
+ "Jobs" => workflow.jobs.count,
51
+ "Failed jobs" => failed_jobs_count.red,
52
+ "Succeeded jobs" => succeeded_jobs_count.green,
53
+ "Enqueued jobs" => enqueued_jobs_count.yellow,
54
+ "Running jobs" => running_jobs_count.blue,
55
+ "Remaining jobs" => remaining_jobs_count,
56
+ "Status" => status
57
+ }
58
+ end
59
+
60
+ def running_status
61
+ finished = succeeded_jobs_count.to_i
62
+ status = "running".yellow
63
+ status += "\n#{finished}/#{total_jobs_count} [#{(finished*100)/total_jobs_count}%]"
64
+ end
65
+
66
+ def failed_status
67
+ status = "failed".light_red
68
+ status += "\n#{failed_job} failed"
69
+ end
70
+
71
+ def job_to_list_element(job)
72
+ name = job.name
73
+ case
74
+ when job.failed?
75
+ "[✗] #{name.red} \n"
76
+ when job.finished?
77
+ "[✓] #{name.green} \n"
78
+ when job.enqueued?
79
+ "[•] #{name.yellow} \n"
80
+ when job.running?
81
+ "[•] #{name.blue} \n"
82
+ else
83
+ "[ ] #{name} \n"
84
+ end
85
+ end
86
+
87
+ def jobs_by_type(type)
88
+ return sorted_jobs if type == :all
89
+ jobs.select{|j| j.public_send("#{type}?") }
90
+ end
91
+
92
+ def sorted_jobs
93
+ workflow.jobs.sort_by do |job|
94
+ case
95
+ when job.failed?
96
+ 0
97
+ when job.finished?
98
+ 1
99
+ when job.enqueued?
100
+ 2
101
+ when job.running?
102
+ 3
103
+ else
104
+ 4
105
+ end
106
+ end
107
+ end
108
+
109
+ def failed_job
110
+ workflow.jobs.find(&:failed).name
111
+ end
112
+
113
+ def total_jobs_count
114
+ workflow.jobs.count
115
+ end
116
+
117
+ def failed_jobs_count
118
+ workflow.jobs.count(&:failed?).to_s
119
+ end
120
+
121
+ def succeeded_jobs_count
122
+ workflow.jobs.count(&:succeeded?).to_s
123
+ end
124
+
125
+ def enqueued_jobs_count
126
+ workflow.jobs.count(&:enqueued?).to_s
127
+ end
128
+
129
+ def running_jobs_count
130
+ workflow.jobs.count(&:running?).to_s
131
+ end
132
+
133
+ def remaining_jobs_count
134
+ workflow.jobs.count{|j| [j.finished, j.failed, j.enqueued].none? }.to_s
135
+ end
136
+ end
137
+ end
138
+ end
data/lib/gush/cli.rb CHANGED
@@ -22,6 +22,7 @@ module Gush
22
22
  config.namespace = options.fetch("namespace", config.namespace)
23
23
  config.environment = options.fetch("environment", config.environment)
24
24
  end
25
+ load_gushfile
25
26
  end
26
27
 
27
28
  desc "create [WorkflowClass]", "Registers new workflow"
@@ -29,18 +30,12 @@ module Gush
29
30
  workflow = client.create_workflow(name)
30
31
  puts "Workflow created with id: #{workflow.id}"
31
32
  puts "Start it with command: gush start #{workflow.id}"
32
- rescue
33
- puts "Workflow not found."
34
33
  end
35
34
 
36
35
  desc "start [workflow_id]", "Starts Workflow with given ID"
37
36
  def start(*args)
38
37
  id = args.shift
39
38
  client.start_workflow(id, args)
40
- rescue WorkflowNotFound
41
- puts "Workflow not found."
42
- rescue DependencyLevelTooDeep
43
- puts "Dependency level too deep. Perhaps you have a dependency cycle?"
44
39
  end
45
40
 
46
41
  desc "create_and_start [WorkflowClass]", "Create and instantly start the new workflow"
@@ -48,18 +43,12 @@ module Gush
48
43
  workflow = client.create_workflow(name)
49
44
  client.start_workflow(workflow.id, args)
50
45
  puts "Created and started workflow with id: #{workflow.id}"
51
- rescue WorkflowNotFound
52
- puts "Workflow not found."
53
- rescue DependencyLevelTooDeep
54
- puts "Dependency level too deep. Perhaps you have a dependency cycle?"
55
46
  end
56
47
 
57
48
  desc "stop [workflow_id]", "Stops Workflow with given ID"
58
49
  def stop(*args)
59
50
  id = args.shift
60
51
  client.stop_workflow(id)
61
- rescue WorkflowNotFound
62
- puts "Workflow not found."
63
52
  end
64
53
 
65
54
  desc "clear", "Clears all jobs from Sidekiq queue"
@@ -77,16 +66,12 @@ module Gush
77
66
  display_overview_for(workflow) unless options[:skip_overview]
78
67
 
79
68
  display_jobs_list_for(workflow, options[:jobs]) unless options[:skip_jobs]
80
- rescue WorkflowNotFound
81
- puts "Workflow not found."
82
69
  end
83
70
 
84
71
  desc "rm [workflow_id]", "Delete workflow with given ID"
85
72
  def rm(workflow_id)
86
73
  workflow = client.find_workflow(workflow_id)
87
74
  client.destroy_workflow(workflow)
88
- rescue WorkflowNotFound
89
- puts "Workflow not found."
90
75
  end
91
76
 
92
77
  desc "list", "Lists all workflows with their statuses"
@@ -112,44 +97,10 @@ module Gush
112
97
  desc "viz [WorkflowClass]", "Displays graph, visualising job dependencies"
113
98
  def viz(name)
114
99
  client
115
- workflow = name.constantize.new("start")
116
- GraphViz.new(:G, type: :digraph, dpi: 200, compound: true) do |g|
117
- g[:compound] = true
118
- g[:rankdir] = "LR"
119
- g[:center] = true
120
- g.node[:shape] = "ellipse"
121
- g.node[:style] = "filled"
122
- g.node[:color] = "#555555"
123
- g.node[:fillcolor] = "white"
124
- g.edge[:dir] = "forward"
125
- g.edge[:penwidth] = 1
126
- g.edge[:color] = "#555555"
127
- start = g.start(shape: 'diamond', fillcolor: '#CFF09E')
128
- end_node = g.end(shape: 'diamond', fillcolor: '#F56991')
129
-
130
-
131
- workflow.nodes.each do |job|
132
- name = job.class.to_s
133
- g.add_nodes(name)
134
-
135
- if job.incoming.empty?
136
- g.add_edges(start, name)
137
- end
138
-
139
-
140
- if job.outgoing.empty?
141
- g.add_edges(name, end_node)
142
- else
143
- job.outgoing.each do |out|
144
- g.add_edges(name, out)
145
- end
146
- end
147
- end
148
-
149
- g.output(png: Pathname.new(Dir.tmpdir).join("graph.png"))
150
- end
151
-
152
- Launchy.open(Pathname.new(Dir.tmpdir).join("graph.png").to_s)
100
+ workflow = name.constantize.new
101
+ graph = Graph.new(workflow)
102
+ graph.viz
103
+ Launchy.open graph.path
153
104
  end
154
105
 
155
106
  private
@@ -158,88 +109,35 @@ module Gush
158
109
  @client ||= Client.new
159
110
  end
160
111
 
161
- def display_overview_for(workflow)
162
- rows = []
163
- columns = {
164
- "id" => workflow.id,
165
- "name" => workflow.class.to_s,
166
- "jobs" => workflow.nodes.count,
167
- "failed jobs" => workflow.nodes.count(&:failed?).to_s.red,
168
- "succeeded jobs" => workflow.nodes.count(&:succeeded?).to_s.green,
169
- "enqueued jobs" => workflow.nodes.count(&:enqueued?).to_s.yellow,
170
- "running jobs" => workflow.nodes.count(&:running?).to_s.blue,
171
- "remaining jobs" => workflow.nodes.count{|j| [j.finished, j.failed, j.enqueued].all? {|b| !b} },
172
- "status" => status_for(workflow)
173
- }
174
-
175
- columns.each_pair do |name, value|
176
- rows << [{alignment: :center, value: name}, value]
177
- rows << :separator if name != "status"
178
- end
112
+ def overview(workflow)
113
+ CLI::Overview.new(workflow)
114
+ end
179
115
 
180
- puts Terminal::Table.new(rows: rows)
116
+ def display_overview_for(workflow)
117
+ puts overview(workflow).table
181
118
  end
182
119
 
183
120
  def status_for(workflow)
184
- if workflow.failed?
185
- status = "failed".light_red
186
- status += "\n#{workflow.nodes.find(&:failed).name} failed"
187
- elsif workflow.running?
188
- status = "running".yellow
189
- finished = workflow.nodes.count {|job| job.finished }
190
- total = workflow.nodes.count
191
- status += "\n#{finished}/#{total} [#{(finished*100)/total}%]"
192
- elsif workflow.finished?
193
- status = "done".green
194
- elsif workflow.stopped?
195
- status = "stopped".red
196
- else
197
- status = "pending".light_white
198
- end
121
+ overview(workflow).status
199
122
  end
200
123
 
201
124
  def display_jobs_list_for(workflow, jobs)
202
- puts "\nJobs list:\n"
203
-
204
- jobs_by_type(workflow, jobs).each do |job|
205
- name = job.name
206
- puts case
207
- when job.failed?
208
- "[✗] #{name.red}"
209
- when job.finished?
210
- "[✓] #{name.green}"
211
- when job.enqueued?
212
- "[•] #{name.yellow}"
213
- when job.running?
214
- "[•] #{name.blue}"
215
- else
216
- "[ ] #{name}"
217
- end
218
- end
219
- end
220
-
221
- def jobs_by_type(workflow, type)
222
- jobs = workflow.nodes.sort_by do |job|
223
- case
224
- when job.failed?
225
- 0
226
- when job.finished?
227
- 1
228
- when job.enqueued?
229
- 2
230
- when job.running?
231
- 3
232
- else
233
- 4
234
- end
235
- end
236
-
237
- jobs.select!{|j| j.public_send("#{type}?") } unless type == :all
238
- jobs
125
+ puts overview(workflow).jobs_list(jobs)
239
126
  end
240
127
 
241
128
  def gushfile
242
129
  Pathname.pwd.join(options[:gushfile])
243
130
  end
131
+
132
+ def load_gushfile
133
+ file = client.configuration.gushfile
134
+ if !@gushfile.exist?
135
+ raise Thor::Error, "#{file} not found, please add it to your project".colorize(:red)
136
+ end
137
+
138
+ require file
139
+ rescue LoadError
140
+ raise Thor::Error, "failed to require #{file}".colorize(:red)
141
+ end
244
142
  end
245
143
  end