gush 0.0.1 → 0.1

Sign up to get free protection for your applications and to get access to all the features.
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