gush 0.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 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