gush 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ee767bd97e343cac8bf395a65d2d19c63abd638b
4
+ data.tar.gz: d5a41126c1895e505871ae108613955466724829
5
+ SHA512:
6
+ metadata.gz: ce471a04c947ff17fc3864644223463da25ca52aa59e34cc2118bcb607526db2b394c79b6b708af23bc29a7ae4efb5647e5bd19a09e51d9df253e1b8442fac42
7
+ data.tar.gz: dda4e7862751f8c7a7cc625f7bdafaca54dc6718a96689346d010d4ae8f6b3350dff9cd07cf461ad369930eb90f75e0bf33242f685be85ed0482065e8f8f367d
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ workflows/
18
+ tmp
19
+ test.rb
20
+ /Gushfile.rb
21
+ dump.rdb
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in gush.gemspec
4
+ gemspec
5
+
6
+ gem 'pry'
7
+ gem 'yajl-ruby'
8
+ gem 'bbq-spawn', git: 'git@github.com:drugpl/bbq-spawn.git'
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Piotrek Okoński
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # Gush
2
+
3
+ Gush is a parallel workflow runner using only Redis as its message broker and Sidekiq for workers.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'gush'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install gush
18
+
19
+ ## Usage
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
+ ### Defining workflows
34
+
35
+ The DSL for defining jobs consists of a single `run` method.
36
+ Here is a complete example of a workflow you can create:
37
+
38
+ ```ruby
39
+ # workflows/sample_workflow.rb
40
+ class SampleWorkflow < Gush::Workflow
41
+ def configure
42
+ run FetchJob1
43
+ run FetchJob2
44
+
45
+ run PersistJob1, after: FetchJob1
46
+ run PersistJob2, after: FetchJob2
47
+
48
+ run Normalize,
49
+ after: [PersistJob1, PersistJob2],
50
+ before: Index
51
+
52
+ run Index
53
+ end
54
+ end
55
+ ```
56
+
57
+ **Hint:** For debugging purposes you can vizualize the graph using `viz` command:
58
+
59
+ ```
60
+ bundle exec gush viz SampleWorkflow
61
+ ```
62
+
63
+ For the Workflow above, the graph will look like this:
64
+
65
+ ![SampleWorkflow](http://i.imgur.com/SmeRRVT.png)
66
+ ### Defining jobs
67
+
68
+ Jobs are classes inheriting from `Gush::Job`:
69
+
70
+ ```ruby
71
+ #workflows/sample/fetch_job1.rb
72
+ class FetchJob1 < Gush::Job
73
+ def work
74
+ # do some fetching from remote APIs
75
+ end
76
+ end
77
+ ```
78
+
79
+ ### Running
80
+
81
+ #### 1. Register workflow
82
+
83
+ After you define your workflows and jobs, all you have to do is register them:
84
+
85
+ ```
86
+ bundle exec gush create SampleWorkflow
87
+ ```
88
+
89
+ the command will return a unique workflow id you will use in later commands.
90
+
91
+ #### 2. Run workers
92
+
93
+ This will start Sidekiq workers responsible for processing jobs
94
+
95
+ ```
96
+ bundle exec gush workers
97
+ ```
98
+
99
+ #### 3. Start the workflow
100
+
101
+ Use your workflow_id returned by `create` command.
102
+
103
+ ```
104
+ bundle gush start <workflow_id>
105
+ ```
106
+
107
+ ### 5. Check the status
108
+
109
+ - of a specific workflow:
110
+
111
+ ```
112
+ bundle gush show <workflow_id>
113
+ ```
114
+
115
+ - of all created workflows:
116
+
117
+ ```
118
+ bundle gush list
119
+ ```
120
+
121
+
122
+ ## Contributing
123
+
124
+ 1. Fork it ( http://github.com/lonelyplanet/gush/fork )
125
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
126
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
127
+ 4. Push to the branch (`git push origin my-new-feature`)
128
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/gush ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ require "pathname"
3
+ require "bundler"
4
+ Bundler.require
5
+
6
+ bin_file = Pathname.new(__FILE__).realpath
7
+ # add self to libpath
8
+ $:.unshift File.expand_path("../../lib", bin_file)
9
+
10
+ require 'gush'
11
+
12
+ Gush::CLI.start(ARGV)
data/gush.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'gush/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "gush"
8
+ spec.version = Gush::VERSION
9
+ spec.authors = ["Piotrek Okoński"]
10
+ spec.email = ["piotrek@okonski.org"]
11
+ spec.summary = "Fast and distributed workflow runner using only Sidekiq and Redis"
12
+ spec.homepage = "https://github.com/pokonski/gush"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = "gush"
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
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"
29
+ spec.add_development_dependency "bundler", "~> 1.5"
30
+ spec.add_development_dependency "rake"
31
+ spec.add_development_dependency "rspec", '~> 3.0.0'
32
+ end
data/lib/gush.rb ADDED
@@ -0,0 +1,47 @@
1
+ require "bundler/setup"
2
+
3
+ require "graphviz"
4
+ require "hiredis"
5
+ require "pathname"
6
+ require "redis"
7
+ require "securerandom"
8
+ require "sidekiq"
9
+
10
+ require "gush/cli"
11
+ require "gush/client"
12
+ require "gush/configuration"
13
+ require "gush/errors"
14
+ require "gush/job"
15
+ require "gush/logger_builder"
16
+ require "gush/metadata"
17
+ require "gush/null_logger"
18
+ require "gush/version"
19
+ require "gush/worker"
20
+ require "gush/workflow"
21
+
22
+ module Gush
23
+ def self.gushfile
24
+ configuration.gushfile
25
+ end
26
+
27
+ def self.root
28
+ Pathname.new(__FILE__).parent.parent
29
+ end
30
+
31
+ def self.configuration
32
+ @configuration ||= Configuration.new
33
+ end
34
+
35
+ def self.configure
36
+ yield configuration
37
+ reconfigure_sidekiq_server
38
+ end
39
+
40
+ def self.reconfigure_sidekiq_server
41
+ Sidekiq.configure_server do |config|
42
+ config.redis = { url: configuration.redis_url, queue: configuration.namespace}
43
+ end
44
+ end
45
+ end
46
+
47
+ Gush.reconfigure_sidekiq_server
data/lib/gush/cli.rb ADDED
@@ -0,0 +1,245 @@
1
+ require 'terminal-table'
2
+ require 'colorize'
3
+ require 'thor'
4
+ require 'launchy'
5
+ require 'sidekiq'
6
+ require 'sidekiq/api'
7
+
8
+ module Gush
9
+ class CLI < Thor
10
+ class_option :gushfile, desc: "configuration file to use", aliases: "-f"
11
+ class_option :concurrency, desc: "concurrency setting for Sidekiq", aliases: "-c"
12
+ class_option :redis, desc: "Redis URL to use", aliases: "-r"
13
+ class_option :namespace, desc: "namespace to run jobs in", aliases: "-n"
14
+ class_option :env, desc: "Sidekiq environment", aliases: "-e"
15
+
16
+ def initialize(*)
17
+ super
18
+ Gush.configure do |config|
19
+ config.gushfile = options.fetch("gushfile", config.gushfile)
20
+ config.concurrency = options.fetch("concurrency", config.concurrency)
21
+ config.redis_url = options.fetch("redis", config.redis_url)
22
+ config.namespace = options.fetch("namespace", config.namespace)
23
+ config.environment = options.fetch("environment", config.environment)
24
+ end
25
+ end
26
+
27
+ desc "create [WorkflowClass]", "Registers new workflow"
28
+ def create(name)
29
+ workflow = client.create_workflow(name)
30
+ puts "Workflow created with id: #{workflow.id}"
31
+ puts "Start it with command: gush start #{workflow.id}"
32
+ rescue
33
+ puts "Workflow not found."
34
+ end
35
+
36
+ desc "start [workflow_id]", "Starts Workflow with given ID"
37
+ def start(*args)
38
+ id = args.shift
39
+ 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
+ end
45
+
46
+ desc "create_and_start [WorkflowClass]", "Create and instantly start the new workflow"
47
+ def create_and_start(name, *args)
48
+ workflow = client.create_workflow(name)
49
+ client.start_workflow(workflow.id, args)
50
+ 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
+ end
56
+
57
+ desc "stop [workflow_id]", "Stops Workflow with given ID"
58
+ def stop(*args)
59
+ id = args.shift
60
+ client.stop_workflow(id)
61
+ rescue WorkflowNotFound
62
+ puts "Workflow not found."
63
+ end
64
+
65
+ desc "clear", "Clears all jobs from Sidekiq queue"
66
+ def clear
67
+ Sidekiq::Queue.new(client.configuration.namespace).clear
68
+ end
69
+
70
+ desc "show [workflow_id]", "Shows details about workflow with given ID"
71
+ option :skip_overview, type: :boolean
72
+ option :skip_jobs, type: :boolean
73
+ option :jobs, default: :all
74
+ def show(workflow_id)
75
+ workflow = client.find_workflow(workflow_id)
76
+
77
+ display_overview_for(workflow) unless options[:skip_overview]
78
+
79
+ display_jobs_list_for(workflow, options[:jobs]) unless options[:skip_jobs]
80
+ rescue WorkflowNotFound
81
+ puts "Workflow not found."
82
+ end
83
+
84
+ desc "rm [workflow_id]", "Delete workflow with given ID"
85
+ def rm(workflow_id)
86
+ workflow = client.find_workflow(workflow_id)
87
+ client.destroy_workflow(workflow)
88
+ rescue WorkflowNotFound
89
+ puts "Workflow not found."
90
+ end
91
+
92
+ desc "list", "Lists all workflows with their statuses"
93
+ def list
94
+ workflows = client.all_workflows
95
+ rows = workflows.map do |workflow|
96
+ [workflow.id, workflow.class, {alignment: :center, value: status_for(workflow)}]
97
+ end
98
+ headers = [
99
+ {alignment: :center, value: 'id'},
100
+ {alignment: :center, value: 'name'},
101
+ {alignment: :center, value: 'status'}
102
+ ]
103
+ puts Terminal::Table.new(headings: headers, rows: rows)
104
+ end
105
+
106
+ desc "workers", "Starts Sidekiq workers"
107
+ def workers
108
+ config = client.configuration
109
+ Kernel.exec "bundle exec sidekiq -r #{config.gushfile} -c #{config.concurrency} -q #{config.namespace} -e #{config.environment} -v"
110
+ end
111
+
112
+ desc "viz [WorkflowClass]", "Displays graph, visualising job dependencies"
113
+ def viz(name)
114
+ 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)
153
+ end
154
+
155
+ private
156
+
157
+ def client
158
+ @client ||= Client.new
159
+ end
160
+
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
179
+
180
+ puts Terminal::Table.new(rows: rows)
181
+ end
182
+
183
+ 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
199
+ end
200
+
201
+ 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
239
+ end
240
+
241
+ def gushfile
242
+ Pathname.pwd.join(options[:gushfile])
243
+ end
244
+ end
245
+ end