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.
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
- workflow = name.constantize.new(id)
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
- persist_workflow(workflow)
28
- workflow
25
+ flow
29
26
  end
30
27
 
31
- def start_workflow(id, jobs = [])
32
- workflow = find_workflow(id)
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 jobs.empty?
37
- workflow.next_jobs
32
+ jobs = if job_names.empty?
33
+ workflow.initial_jobs
38
34
  else
39
- jobs.map {|name| workflow.find_job(name) }
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.stop!
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 = Yajl::Parser.parse(data, symbolize_keys: true)
69
+ hash = Gush::JSON.decode(data, symbolize_keys: true)
66
70
  keys = redis.keys("gush.jobs.#{id}.*")
67
- nodes = redis.mget(*keys).map { |json| Yajl::Parser.parse(json, symbolize_keys: true) }
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.nodes.each {|job| persist_job(workflow.id, job) }
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.nodes.each {|job| destroy_job(workflow.id, job) }
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(hash[:id], configure: false)
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.nodes << Gush::Job.from_hash(node)
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, Yajl::Encoder.new.encode(message))
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
@@ -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(Yajl::Parser.parse(json, symbolize_keys: true))
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
- Yajl::Encoder.new.encode(to_hash)
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
- class WorkflowNotFound < StandardError; end
2
- class DependencyLevelTooDeep < StandardError; end
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
- DEFAULTS = {
10
- finished: false,
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
- attr_writer :logger
22
-
23
- def initialize(opts = {})
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: @finished,
33
- enqueued: @enqueued,
34
- failed: @failed,
19
+ finished: finished?,
20
+ enqueued: enqueued?,
21
+ failed: failed?,
35
22
  incoming: @incoming,
36
23
  outgoing: @outgoing,
37
- finished_at: @finished_at,
38
- started_at: @started_at,
39
- failed_at: @failed_at,
40
- running: @running
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
- Yajl::Encoder.new.encode(as_json)
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
- @enqueued = false
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
- @enqueued = true
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
- @running = false
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
- @finished = true
97
- @running = false
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
- !!enqueued
64
+ !!enqueued_at
106
65
  end
107
66
 
108
67
  def finished?
109
- !!finished
68
+ !!finished_at
110
69
  end
111
70
 
112
71
  def failed?
113
- !!failed
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
- !!running
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 dependencies(flow, level = 0)
133
- fail DependencyLevelTooDeep if level > RECURSION_LIMIT
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 logger
138
- fail "You cannot log when the job is not running" unless running?
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
- @running = options[:running]
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
@@ -0,0 +1,12 @@
1
+ module Gush
2
+ class JSON
3
+
4
+ def self.encode(data)
5
+ Yajl::Encoder.new.encode(data)
6
+ end
7
+
8
+ def self.decode(data, options = {})
9
+ Yajl::Parser.parse(data, options)
10
+ end
11
+ end
12
+ end
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
- continue_workflow(workflow)
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 continue_workflow(workflow)
85
- # refetch is important to get correct workflow status
86
- unless client.find_workflow(workflow.id).stopped?
87
- client.start_workflow(workflow.id)
88
- end
89
- end
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