gush 0.0.1 → 0.1

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