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 +4 -4
- data/.travis.yml +13 -0
- data/Gemfile +1 -1
- data/README.md +51 -29
- data/bin/gush +7 -1
- data/gush.gemspec +10 -11
- data/lib/gush/cli/overview.rb +138 -0
- data/lib/gush/cli.rb +23 -125
- data/lib/gush/client.rb +47 -36
- data/lib/gush/configuration.rb +3 -5
- data/lib/gush/errors.rb +4 -3
- data/lib/gush/graph.rb +88 -0
- data/lib/gush/job.rb +34 -88
- data/lib/gush/json.rb +12 -0
- data/lib/gush/worker.rb +7 -20
- data/lib/gush/workflow.rb +66 -41
- data/lib/gush.rb +3 -3
- data/spec/features/workflows_spec.rb +31 -0
- data/spec/lib/gush/client_spec.rb +29 -32
- data/spec/lib/gush/job_spec.rb +38 -42
- data/spec/lib/gush/worker_spec.rb +30 -59
- data/spec/lib/gush/workflow_spec.rb +71 -123
- data/spec/lib/gush_spec.rb +1 -1
- data/spec/spec_helper.rb +26 -29
- metadata +39 -43
- data/lib/gush/logger_builder.rb +0 -15
- data/lib/gush/metadata.rb +0 -24
- data/lib/gush/null_logger.rb +0 -6
- data/lib/gush/version.rb +0 -3
- data/spec/lib/gush/logger_builder_spec.rb +0 -25
- data/spec/lib/gush/null_logger_spec.rb +0 -15
- data/spec/redis.conf +0 -2
data/lib/gush/client.rb
CHANGED
@@ -6,7 +6,6 @@ module Gush
|
|
6
6
|
@configuration = config
|
7
7
|
@sidekiq = build_sidekiq
|
8
8
|
@redis = build_redis
|
9
|
-
load_gushfile
|
10
9
|
end
|
11
10
|
|
12
11
|
def configure
|
@@ -16,42 +15,47 @@ module Gush
|
|
16
15
|
end
|
17
16
|
|
18
17
|
def create_workflow(name)
|
19
|
-
id = SecureRandom.uuid.split("-").first
|
20
|
-
|
21
18
|
begin
|
22
|
-
|
19
|
+
flow = name.constantize.new
|
20
|
+
flow.save
|
23
21
|
rescue NameError
|
24
22
|
raise WorkflowNotFound.new("Workflow with given name doesn't exist")
|
25
23
|
end
|
26
24
|
|
27
|
-
|
28
|
-
workflow
|
25
|
+
flow
|
29
26
|
end
|
30
27
|
|
31
|
-
def start_workflow(
|
32
|
-
workflow
|
33
|
-
workflow.start!
|
28
|
+
def start_workflow(workflow, job_names = [])
|
29
|
+
workflow.mark_as_started
|
34
30
|
persist_workflow(workflow)
|
35
31
|
|
36
|
-
jobs = if
|
37
|
-
workflow.
|
32
|
+
jobs = if job_names.empty?
|
33
|
+
workflow.initial_jobs
|
38
34
|
else
|
39
|
-
|
35
|
+
job_names.map {|name| workflow.find_job(name) }
|
40
36
|
end
|
41
37
|
|
42
38
|
jobs.each do |job|
|
43
|
-
job.enqueue!
|
44
|
-
persist_job(workflow.id, job)
|
45
39
|
enqueue_job(workflow.id, job)
|
46
40
|
end
|
47
41
|
end
|
48
42
|
|
49
43
|
def stop_workflow(id)
|
50
44
|
workflow = find_workflow(id)
|
51
|
-
workflow.
|
45
|
+
workflow.mark_as_stopped
|
52
46
|
persist_workflow(workflow)
|
53
47
|
end
|
54
48
|
|
49
|
+
def next_free_id
|
50
|
+
id = nil
|
51
|
+
loop do
|
52
|
+
id = SecureRandom.uuid
|
53
|
+
break if !redis.exists("gush.workflow.#{id}")
|
54
|
+
end
|
55
|
+
|
56
|
+
id
|
57
|
+
end
|
58
|
+
|
55
59
|
def all_workflows
|
56
60
|
redis.keys("gush.workflows.*").map do |key|
|
57
61
|
id = key.sub("gush.workflows.", "")
|
@@ -62,9 +66,9 @@ module Gush
|
|
62
66
|
def find_workflow(id)
|
63
67
|
data = redis.get("gush.workflows.#{id}")
|
64
68
|
unless data.nil?
|
65
|
-
hash =
|
69
|
+
hash = Gush::JSON.decode(data, symbolize_keys: true)
|
66
70
|
keys = redis.keys("gush.jobs.#{id}.*")
|
67
|
-
nodes = redis.mget(*keys).map { |json|
|
71
|
+
nodes = redis.mget(*keys).map { |json| Gush::JSON.decode(json, symbolize_keys: true) }
|
68
72
|
workflow_from_hash(hash, nodes)
|
69
73
|
else
|
70
74
|
raise WorkflowNotFound.new("Workflow with given id doesn't exist")
|
@@ -73,16 +77,25 @@ module Gush
|
|
73
77
|
|
74
78
|
def persist_workflow(workflow)
|
75
79
|
redis.set("gush.workflows.#{workflow.id}", workflow.to_json)
|
76
|
-
workflow.
|
80
|
+
workflow.jobs.each {|job| persist_job(workflow.id, job) }
|
81
|
+
workflow.mark_as_persisted
|
82
|
+
true
|
77
83
|
end
|
78
84
|
|
79
85
|
def persist_job(workflow_id, job)
|
80
86
|
redis.set("gush.jobs.#{workflow_id}.#{job.class.to_s}", job.to_json)
|
81
87
|
end
|
82
88
|
|
89
|
+
def load_job(workflow_id, job_id)
|
90
|
+
data = redis.get("gush.jobs.#{workflow_id}.#{job_id}")
|
91
|
+
return nil if data.nil?
|
92
|
+
data = Gush::JSON.decode(data, symbolize_keys: true)
|
93
|
+
Gush::Job.from_hash(nil, data)
|
94
|
+
end
|
95
|
+
|
83
96
|
def destroy_workflow(workflow)
|
84
97
|
redis.del("gush.workflows.#{workflow.id}")
|
85
|
-
workflow.
|
98
|
+
workflow.jobs.each {|job| destroy_job(workflow.id, job) }
|
86
99
|
end
|
87
100
|
|
88
101
|
def destroy_job(workflow_id, job)
|
@@ -97,33 +110,37 @@ module Gush
|
|
97
110
|
report("gush.workflows.status", message)
|
98
111
|
end
|
99
112
|
|
113
|
+
def enqueue_job(workflow_id, job)
|
114
|
+
job.enqueue!
|
115
|
+
persist_job(workflow_id, job)
|
116
|
+
|
117
|
+
sidekiq.push(
|
118
|
+
'class' => Gush::Worker,
|
119
|
+
'queue' => configuration.namespace,
|
120
|
+
'args' => [workflow_id, job.class.to_s, configuration.to_json]
|
121
|
+
)
|
122
|
+
end
|
123
|
+
|
100
124
|
private
|
101
125
|
|
102
126
|
attr_reader :sidekiq, :redis
|
103
127
|
|
104
128
|
def workflow_from_hash(hash, nodes = nil)
|
105
|
-
flow = hash[:klass].constantize.new(
|
106
|
-
flow.logger_builder(hash.fetch(:logger_builder, 'Gush::LoggerBuilder').constantize)
|
129
|
+
flow = hash[:klass].constantize.new(false)
|
107
130
|
flow.stopped = hash.fetch(:stopped, false)
|
131
|
+
flow.id = hash[:id]
|
108
132
|
|
109
133
|
(nodes || hash[:nodes]).each do |node|
|
110
|
-
flow.
|
134
|
+
flow.jobs << Gush::Job.from_hash(flow, node)
|
111
135
|
end
|
112
136
|
|
113
137
|
flow
|
114
138
|
end
|
115
139
|
|
116
140
|
def report(key, message)
|
117
|
-
redis.publish(key,
|
141
|
+
redis.publish(key, Gush::JSON.encode(message))
|
118
142
|
end
|
119
143
|
|
120
|
-
def enqueue_job(workflow_id, job)
|
121
|
-
sidekiq.push(
|
122
|
-
'class' => Gush::Worker,
|
123
|
-
'queue' => configuration.namespace,
|
124
|
-
'args' => [workflow_id, job.class.to_s, configuration.to_json]
|
125
|
-
)
|
126
|
-
end
|
127
144
|
|
128
145
|
def build_sidekiq
|
129
146
|
Sidekiq::Client.new(connection_pool)
|
@@ -136,11 +153,5 @@ module Gush
|
|
136
153
|
def connection_pool
|
137
154
|
ConnectionPool.new(size: configuration.concurrency, timeout: 1) { build_redis }
|
138
155
|
end
|
139
|
-
|
140
|
-
def load_gushfile
|
141
|
-
require configuration.gushfile
|
142
|
-
rescue LoadError
|
143
|
-
raise Thor::Error, "failed to load #{configuration.gushfile.basename}".colorize(:red)
|
144
|
-
end
|
145
156
|
end
|
146
157
|
end
|
data/lib/gush/configuration.rb
CHANGED
@@ -5,7 +5,7 @@ module Gush
|
|
5
5
|
attr_accessor :concurrency, :namespace, :redis_url, :environment
|
6
6
|
|
7
7
|
def self.from_json(json)
|
8
|
-
new(
|
8
|
+
new(Gush::JSON.decode(json, symbolize_keys: true))
|
9
9
|
end
|
10
10
|
|
11
11
|
def initialize(hash = {})
|
@@ -21,7 +21,6 @@ module Gush
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def gushfile
|
24
|
-
raise Thor::Error, "#{@gushfile} not found, please add it to your project".colorize(:red) unless @gushfile.exist?
|
25
24
|
@gushfile.realpath
|
26
25
|
end
|
27
26
|
|
@@ -30,13 +29,12 @@ module Gush
|
|
30
29
|
concurrency: concurrency,
|
31
30
|
namespace: namespace,
|
32
31
|
redis_url: redis_url,
|
33
|
-
environment: environment
|
34
|
-
gushfile: gushfile.to_path
|
32
|
+
environment: environment
|
35
33
|
}
|
36
34
|
end
|
37
35
|
|
38
36
|
def to_json
|
39
|
-
|
37
|
+
Gush::JSON.encode(to_hash)
|
40
38
|
end
|
41
39
|
end
|
42
40
|
end
|
data/lib/gush/errors.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
-
|
2
|
-
class
|
3
|
-
|
1
|
+
module Gush
|
2
|
+
class WorkflowNotFound < StandardError; end
|
3
|
+
class DependencyLevelTooDeep < StandardError; end
|
4
|
+
end
|
data/lib/gush/graph.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
module Gush
|
2
|
+
class Graph
|
3
|
+
attr_reader :workflow, :filename, :path, :start, :end_node
|
4
|
+
|
5
|
+
def initialize(workflow, options: {})
|
6
|
+
@workflow = workflow
|
7
|
+
@filename = options.fetch(:filename, "graph.png")
|
8
|
+
@path = options.fetch(:path, Pathname.new(Dir.tmpdir).join(filename))
|
9
|
+
end
|
10
|
+
|
11
|
+
def viz
|
12
|
+
GraphViz.new(:G, graph_options) do |graph|
|
13
|
+
set_node_options!(graph)
|
14
|
+
set_edge_options!(graph)
|
15
|
+
|
16
|
+
@start = graph.start(shape: 'diamond', fillcolor: '#CFF09E')
|
17
|
+
@end_node = graph.end(shape: 'diamond', fillcolor: '#F56991')
|
18
|
+
|
19
|
+
workflow.jobs.each do |job|
|
20
|
+
add_job(graph, job)
|
21
|
+
end
|
22
|
+
|
23
|
+
graph.output(png: path)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def path
|
28
|
+
@path.to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def add_job(graph, job)
|
33
|
+
name = job.class.to_s
|
34
|
+
graph.add_nodes(name)
|
35
|
+
|
36
|
+
if job.incoming.empty?
|
37
|
+
graph.add_edges(start, name)
|
38
|
+
end
|
39
|
+
|
40
|
+
if job.outgoing.empty?
|
41
|
+
graph.add_edges(name, end_node)
|
42
|
+
else
|
43
|
+
job.outgoing.each do |out|
|
44
|
+
graph.add_edges(name, out)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def set_node_options!(graph)
|
50
|
+
node_options.each do |key, value|
|
51
|
+
graph.node[key] = value
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def set_edge_options!(graph)
|
56
|
+
edge_options.each do |key, value|
|
57
|
+
graph.edge[key] = value
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def graph_options
|
62
|
+
{
|
63
|
+
type: :digraph,
|
64
|
+
dpi: 200,
|
65
|
+
compound: true,
|
66
|
+
rankdir: "LR",
|
67
|
+
center: true
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
def node_options
|
72
|
+
{
|
73
|
+
shape: "ellipse",
|
74
|
+
style: "filled",
|
75
|
+
color: "#555555",
|
76
|
+
fillcolor: "white"
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
def edge_options
|
81
|
+
{
|
82
|
+
dir: "forward",
|
83
|
+
penwidth: 1,
|
84
|
+
color: "#555555"
|
85
|
+
}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/lib/gush/job.rb
CHANGED
@@ -1,27 +1,14 @@
|
|
1
|
-
require 'gush/metadata'
|
2
|
-
|
3
1
|
module Gush
|
4
2
|
class Job
|
5
|
-
include Gush::Metadata
|
6
|
-
|
7
|
-
RECURSION_LIMIT = 1000
|
8
3
|
|
9
|
-
|
10
|
-
|
11
|
-
enqueued: false,
|
12
|
-
failed: false,
|
13
|
-
running: false
|
14
|
-
}
|
15
|
-
|
16
|
-
attr_accessor :finished, :enqueued, :failed, :workflow_id, :incoming, :outgoing,
|
17
|
-
:finished_at, :failed_at, :started_at, :jid, :running
|
4
|
+
attr_accessor :workflow_id, :incoming, :outgoing,
|
5
|
+
:finished_at, :failed_at, :started_at, :enqueued_at
|
18
6
|
|
19
7
|
attr_reader :name
|
20
8
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
options = DEFAULTS.dup.merge(opts)
|
9
|
+
def initialize(workflow, opts = {})
|
10
|
+
@workflow = workflow
|
11
|
+
options = opts.dup
|
25
12
|
assign_variables(options)
|
26
13
|
end
|
27
14
|
|
@@ -29,88 +16,60 @@ module Gush
|
|
29
16
|
{
|
30
17
|
name: @name,
|
31
18
|
klass: self.class.to_s,
|
32
|
-
finished:
|
33
|
-
enqueued:
|
34
|
-
failed:
|
19
|
+
finished: finished?,
|
20
|
+
enqueued: enqueued?,
|
21
|
+
failed: failed?,
|
35
22
|
incoming: @incoming,
|
36
23
|
outgoing: @outgoing,
|
37
|
-
finished_at:
|
38
|
-
|
39
|
-
|
40
|
-
|
24
|
+
finished_at: finished_at,
|
25
|
+
enqueued_at: enqueued_at,
|
26
|
+
started_at: started_at,
|
27
|
+
failed_at: failed_at,
|
28
|
+
running: running?
|
41
29
|
}
|
42
30
|
end
|
43
31
|
|
44
32
|
def to_json(options = {})
|
45
|
-
|
33
|
+
Gush::JSON.encode(as_json)
|
46
34
|
end
|
47
35
|
|
48
|
-
def self.from_hash(hash)
|
49
|
-
hash[:klass].constantize.new(
|
50
|
-
name: hash[:name],
|
51
|
-
finished: hash[:finished],
|
52
|
-
enqueued: hash[:enqueued],
|
53
|
-
failed: hash[:failed],
|
54
|
-
incoming: hash[:incoming],
|
55
|
-
outgoing: hash[:outgoing],
|
56
|
-
failed_at: hash[:failed_at],
|
57
|
-
finished_at: hash[:finished_at],
|
58
|
-
started_at: hash[:started_at],
|
59
|
-
running: hash[:running]
|
60
|
-
)
|
61
|
-
end
|
62
|
-
|
63
|
-
def before_work
|
36
|
+
def self.from_hash(flow, hash)
|
37
|
+
hash[:klass].constantize.new(flow, hash)
|
64
38
|
end
|
65
39
|
|
66
40
|
def work
|
67
41
|
end
|
68
42
|
|
69
|
-
def after_work
|
70
|
-
end
|
71
|
-
|
72
43
|
def start!
|
73
|
-
@
|
74
|
-
@running = true
|
75
|
-
@started_at = Time.now.to_i
|
44
|
+
@started_at = current_timestamp
|
76
45
|
end
|
77
46
|
|
78
47
|
def enqueue!
|
79
|
-
@
|
80
|
-
@running = false
|
81
|
-
@failed = false
|
48
|
+
@enqueued_at = current_timestamp
|
82
49
|
@started_at = nil
|
83
50
|
@finished_at = nil
|
84
51
|
@failed_at = nil
|
85
52
|
end
|
86
53
|
|
87
54
|
def finish!
|
88
|
-
@
|
89
|
-
@finished = true
|
90
|
-
@enqueued = false
|
91
|
-
@failed = false
|
92
|
-
@finished_at = Time.now.to_i
|
55
|
+
@finished_at = current_timestamp
|
93
56
|
end
|
94
57
|
|
95
58
|
def fail!
|
96
|
-
@
|
97
|
-
@
|
98
|
-
@failed = true
|
99
|
-
@enqueued = false
|
100
|
-
@finished_at = Time.now.to_i
|
101
|
-
@failed_at = Time.now.to_i
|
59
|
+
@finished_at = current_timestamp
|
60
|
+
@failed_at = current_timestamp
|
102
61
|
end
|
103
62
|
|
104
63
|
def enqueued?
|
105
|
-
!!
|
64
|
+
!!enqueued_at
|
106
65
|
end
|
107
66
|
|
108
67
|
def finished?
|
109
|
-
!!
|
68
|
+
!!finished_at
|
110
69
|
end
|
111
70
|
|
112
71
|
def failed?
|
113
|
-
!!
|
72
|
+
!!failed_at
|
114
73
|
end
|
115
74
|
|
116
75
|
def succeeded?
|
@@ -118,44 +77,31 @@ module Gush
|
|
118
77
|
end
|
119
78
|
|
120
79
|
def running?
|
121
|
-
!!
|
122
|
-
end
|
123
|
-
|
124
|
-
def can_be_started?(flow)
|
125
|
-
!running? &&
|
126
|
-
!enqueued? &&
|
127
|
-
!finished? &&
|
128
|
-
!failed? &&
|
129
|
-
dependencies_satisfied?(flow)
|
80
|
+
!!started_at && !finished?
|
130
81
|
end
|
131
82
|
|
132
|
-
def
|
133
|
-
|
134
|
-
(incoming.map {|name| flow.find_job(name) } + incoming.flat_map{ |name| flow.find_job(name).dependencies(flow, level + 1) }).uniq
|
83
|
+
def ready_to_start?
|
84
|
+
!running? && !enqueued? && !finished? && !failed?
|
135
85
|
end
|
136
86
|
|
137
|
-
def
|
138
|
-
|
139
|
-
@logger
|
87
|
+
def has_no_dependencies?
|
88
|
+
incoming.empty?
|
140
89
|
end
|
141
90
|
|
142
91
|
private
|
143
92
|
|
93
|
+
def current_timestamp
|
94
|
+
Time.now.to_i
|
95
|
+
end
|
96
|
+
|
144
97
|
def assign_variables(options)
|
145
98
|
@name = options[:name]
|
146
|
-
@finished = options[:finished]
|
147
|
-
@enqueued = options[:enqueued]
|
148
|
-
@failed = options[:failed]
|
149
99
|
@incoming = options[:incoming] || []
|
150
100
|
@outgoing = options[:outgoing] || []
|
151
101
|
@failed_at = options[:failed_at]
|
152
102
|
@finished_at = options[:finished_at]
|
153
103
|
@started_at = options[:started_at]
|
154
|
-
@
|
155
|
-
end
|
156
|
-
|
157
|
-
def dependencies_satisfied?(flow)
|
158
|
-
dependencies(flow).all? { |dep| !dep.enqueued? && dep.finished? && !dep.failed? }
|
104
|
+
@enqueued_at = options[:enqueued_at]
|
159
105
|
end
|
160
106
|
end
|
161
107
|
end
|
data/lib/gush/json.rb
ADDED
data/lib/gush/worker.rb
CHANGED
@@ -15,17 +15,12 @@ module Gush
|
|
15
15
|
start = Time.now
|
16
16
|
report(workflow, job, :started, start)
|
17
17
|
|
18
|
-
job.logger = workflow.build_logger_for_job(job, job_id)
|
19
|
-
job.jid = jid
|
20
|
-
|
21
18
|
failed = false
|
22
19
|
error = nil
|
23
20
|
|
24
21
|
mark_as_started(workflow, job)
|
25
22
|
begin
|
26
|
-
job.before_work
|
27
23
|
job.work
|
28
|
-
job.after_work
|
29
24
|
rescue Exception => e
|
30
25
|
failed = true
|
31
26
|
error = e
|
@@ -35,9 +30,8 @@ module Gush
|
|
35
30
|
report(workflow, job, :finished, start)
|
36
31
|
mark_as_finished(workflow, job)
|
37
32
|
|
38
|
-
|
33
|
+
enqueue_outgoing_jobs(workflow.id, job)
|
39
34
|
else
|
40
|
-
log_exception(job.logger, error)
|
41
35
|
mark_as_failed(workflow, job)
|
42
36
|
report(workflow, job, :failed, start, error.message)
|
43
37
|
end
|
@@ -81,19 +75,12 @@ module Gush
|
|
81
75
|
(Time.now - start).to_f.round(3)
|
82
76
|
end
|
83
77
|
|
84
|
-
def
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
def log_exception(logger, exception)
|
92
|
-
first, *rest = exception.backtrace
|
93
|
-
|
94
|
-
logger << "#{first}: #{exception.message} (#{exception.class})\n"
|
95
|
-
rest.each do |line|
|
96
|
-
logger << " from #{line}\n"
|
78
|
+
def enqueue_outgoing_jobs(workflow_id, job)
|
79
|
+
job.outgoing.each do |job_name|
|
80
|
+
out = client.load_job(workflow_id, job_name)
|
81
|
+
if out.ready_to_start?
|
82
|
+
client.enqueue_job(workflow_id, out)
|
83
|
+
end
|
97
84
|
end
|
98
85
|
end
|
99
86
|
end
|