blue_colr 0.0.9 → 0.1.0

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/README.rdoc CHANGED
@@ -18,7 +18,7 @@ features than builtin Ruby's +Logger+.
18
18
 
19
19
  require 'blue_colr'
20
20
 
21
- BlueColr.start do
21
+ BlueColr.launch do
22
22
  run 'echo These processes'
23
23
  run 'echo will be ran sequentially.'
24
24
  parallel do
@@ -43,15 +43,21 @@ Note: the code above will not _start_ the processes by itself, but enqueue them
43
43
  to the database, by default. A separate process called +bluecolrd+ is
44
44
  used for that.
45
45
 
46
+ The following chart, generated by the same code above, is its execution sequence:
47
+
48
+ http://github.com/downloads/jablan/blue_colr/readme_example.png
49
+
46
50
  == Requirements and Configuration
47
51
 
52
+ In order to access the database, blue_colr requires sequel ORM library, if you
53
+ don't have it, its gem will be installed along with blue_colr.
54
+
48
55
  Blue_colr uses a relational database to simulate a process queue so you will have
49
56
  to provide one. It relies on two tables, named +process_items+ and
50
- +process_item_dependencies+ to work. +db/+ directory contains Postgresql scripts
51
- for creating these two, and this should be moved to Sequel migration later.
57
+ +process_item_dependencies+ to work. +db/+ directory contains Sequel migrations
58
+ for creating these two:
52
59
 
53
- In order to access the database, blue_colr requires sequel ORM library, if you
54
- don't have it, its gem will be installed along with blue_colr.
60
+ sequel -m db/ sqlite://examples/test.db
55
61
 
56
62
  Basic configuration is passed to blue_colr either by setting options from your
57
63
  code, or (if not set), blue_colr will parse your command line arguments and
@@ -77,12 +83,12 @@ when enqueuing them. Then you can have multiple daemons running, each one of the
77
83
  targeting specific environment. That allows easy distribution of your tasks across
78
84
  multiple machines, while keeping them synchronized, like the following scenario:
79
85
 
80
- * Start tasks a and b on machine X and c on machine Y
81
- * When all above are sucessfully done, start task d on machine Z
86
+ * Start tasks +a+ and +b+ on machine +X+ and +c+ on machine +Y+
87
+ * When all above are sucessfully done, start task +d+ on machine +Z+
82
88
 
83
89
  == ToDo
84
90
 
85
- * Move db table creation scripts to Sequel migration
86
- * Write proper tests
91
+ * More tests
92
+ * Better docs
87
93
  * Examples
88
94
 
data/bin/bluecolrd CHANGED
@@ -23,7 +23,7 @@ def logger(name = nil)
23
23
  end
24
24
 
25
25
  def init_logger
26
- if @conf['log4r_config']
26
+ if Module::const_defined?(:Log4r) && @conf['log4r_config']
27
27
  log_cfg = Log4r::YamlConfigurator # shorthand
28
28
  log_cfg['ENVIRONMENT'] = @environment if @environment
29
29
  log_cfg['LOGFILENAME'] = @log_file
@@ -33,7 +33,7 @@ def init_logger
33
33
 
34
34
  @logger = Log4r::Logger
35
35
  else
36
- @logger = {'default' => Logger.new(@log_file)}
36
+ @logger = {'default' => Logger.new(@log_file || STDOUT)}
37
37
  end
38
38
  logger.level = @args['debuglevel'] || Logger::WARN
39
39
  end
@@ -82,15 +82,15 @@ def ok_to_run?
82
82
  # !@args['max'] || @pids.size < @args['max']
83
83
  end
84
84
 
85
- def run process
86
- logger.debug "Running #{process[:module_name]}"
85
+ def run process, running_state
86
+ logger.debug "Running process ##{process[:id]}:"
87
87
  script = process[:cmd]
88
88
  logger.debug script
89
89
  id = process[:id]
90
90
 
91
91
  # update process item in the db
92
92
  # set status of process_item to "running"
93
- @db[:process_items].filter(:id => id).update(:status => BlueColr::STATUS_RUNNING, :started_at => Time.now)
93
+ @db[:process_items].filter(:id => id).update(:status => running_state, :started_at => Time.now)
94
94
 
95
95
  log_path = @conf['log_path'] || '.'
96
96
  log_path = (process[:process_from] || Time.now).strftime(log_path) # interpolate date
@@ -111,16 +111,17 @@ def run process
111
111
  exitstatus = 99
112
112
  end
113
113
 
114
+ final_state = BlueColr.state_from_running(running_state, ok)
114
115
  # find corresponding process_item
115
116
  # change its status in the DB and update ended_at timestamp
116
117
  @db[:process_items].filter(:id => process[:id]).update(
117
- :status => ok ? BlueColr::STATUS_OK : BlueColr::STATUS_ERROR,
118
+ :status => final_state,
118
119
  :exit_code => exitstatus,
119
120
  :ended_at => Time.now
120
121
  )
121
- logger(process[:logger]).error(@error_log_msg % process.to_hash) unless ok
122
+ # logger(process[:logger]).error(@error_log_msg % process.to_hash) unless ok
122
123
 
123
- logger.info "Process ended: id #{process[:id]} #{$?}"
124
+ # logger.info "Process ended: id #{process[:id]} #{$?}"
124
125
  end
125
126
  end
126
127
 
@@ -129,16 +130,17 @@ end
129
130
  # pid => process, hash of started processes
130
131
  @pids = {}
131
132
 
132
- @args = parse_command_line(ARGV)
133
+ @args = parse_command_line(ARGV)
133
134
 
134
- raise "No configuration file defined (-c <config>)." unless @args && @args["config"]
135
- raise "Couldn't read #{@args["config"]} file." unless @args['config'] && @conf = YAML::load(File.new(@args["config"]).read)
136
- @max_processes = @args['max'] || @conf['max_processes'] || 0 # default unlimited
137
- @environment = @args['environment'] || @conf['environment'] || nil
138
- @log_file = @args['logfile'] || "process_daemon_#{@environment}"
139
- @error_log_msg = @conf['error_log_msg'] || 'Process failed: id %{id}'
135
+ raise "No configuration file defined (-c <config>)." unless @args && @args["config"]
136
+ raise "Couldn't read #{@args["config"]} file." unless @args['config'] && @conf = YAML::load(File.new(@args["config"]).read)
137
+ BlueColr.conf = @conf
138
+ @max_processes = @args['max'] || @conf['max_processes'] || 0 # default unlimited
139
+ @environment = @args['environment'] || @conf['environment'] || nil
140
+ @log_file = @args['logfile'] || "process_daemon_#{@environment}.log"
141
+ @error_log_msg = @conf['error_log_msg'] || 'Process failed: id %{id}'
140
142
 
141
- init_logger
143
+ init_logger
142
144
 
143
145
  begin
144
146
  @db = Sequel.connect(@conf['db_url'], :logger => logger('sequel')) # try to use sequel logger, if defined
@@ -147,34 +149,40 @@ begin
147
149
 
148
150
  loop do
149
151
  # get all pending items
150
- query = "select i.id
151
- from process_items i
152
- left join process_item_dependencies d ON i.id = d.process_item_id
153
- left join process_items i2 ON d.depends_on_id = i2.id and i2.status NOT IN ('#{BlueColr::STATUS_OK}', '#{BlueColr::STATUS_SKIPPED}')
154
- where i.status = '#{BlueColr::STATUS_PENDING}' and i.environment = ?
155
- group by i.id
156
- having count(i2.id) = 0"
157
- process_items = @db[query, @environment]
158
-
159
- process_items.each do |id|
160
- logger.debug "Pending item: #{id.inspect}"
152
+ pending_processes = @db[:process_items].filter(:status => BlueColr.get_pending_states).all
153
+ pending_processes = pending_processes.map do |process|
154
+ # get all the parents' statuses
155
+ parent_statuses = @db[:process_items].
156
+ join(:process_item_dependencies, :depends_on_id => :id).
157
+ filter(:process_item_id => process[:id]).
158
+ select(:status).
159
+ map{|h| h[:status]}
160
+
161
+ running_status = BlueColr.state_from_pending(process[:status], parent_statuses)
162
+ [process, running_status]
163
+ end
164
+
165
+ pending_processes.select{|_, running_status| running_status}.each do |process, running_status|
166
+ logger.debug "Pending item: #{process[:id]}"
161
167
  if ok_to_run?
162
- item = @db[:process_items].filter(:id => id[:id]).first
163
- run(item)
168
+ # item = @db[:process_items].filter(:id => id[:id]).first
169
+ run(process, running_status)
164
170
  else
165
171
  logger.debug "No available thread, waiting"
166
172
  end
167
- end
168
- sleep(@conf['sleep_interval'] || 10)
169
- end
170
173
 
171
- rescue Interrupt
172
- if logger
173
- logger.fatal("Ctrl-C received, exiting")
174
- else
175
- puts "Ctrl-C received, exiting"
176
- end
177
- exit 1
174
+ end
175
+ Kernel.sleep 5
176
+ # Kernel.sleep(@conf['sleep_interval'] || 10)
177
+ end # loop
178
+
179
+ #rescue Interrupt
180
+ # if logger
181
+ # logger.fatal("Ctrl-C received, exiting")
182
+ # else
183
+ # puts "Ctrl-C received, exiting"
184
+ # end
185
+ # exit 1
178
186
  rescue Exception => ex
179
187
  p ex.class
180
188
  logger.fatal(ex.to_s) if logger
@@ -0,0 +1,74 @@
1
+ class BlueColr
2
+ # this module, when included in BlueColr, generates GraphViz graph of the invoked processes,
3
+ # instead actually enqueueing them.
4
+ module GraphOutput
5
+
6
+ def self.included target
7
+ target.instance_eval do
8
+ # graph nodes are given unique ids
9
+ def next_id
10
+ @id ||= 0
11
+ @id += 1
12
+ end
13
+
14
+ # gets different color for different environments,
15
+ # currently cycling between couple predefined colors
16
+ # TODO: enable submitting color through option
17
+ def get_color group
18
+ colors = [
19
+ '#FFFFFF',
20
+ '#DDFFDD',
21
+ '#DDDDFF',
22
+ '#FFDDDD',
23
+ '#FFFFDD',
24
+ '#FFDDFF',
25
+ '#DDFFFF',
26
+ ]
27
+ @groups ||= []
28
+ @groups << group unless @groups.member? group
29
+ colors[@groups.index(group) % colors.length]
30
+ end
31
+
32
+ # override class method launch, we are creating output file here,
33
+ # and we don't need database
34
+ def launch &block
35
+ default_options.gv_filename ||= "output.dot"
36
+ worker = self.new
37
+ File.open(default_options.gv_filename, 'w') do |f|
38
+ default_options.gv_file = f
39
+ f.puts "digraph G {"
40
+ worker.instance_eval &block
41
+ f.puts "}"
42
+ end
43
+ worker
44
+ end
45
+
46
+ # override default enqueue method, as just including won't do
47
+ define_method :enqueue, instance_method(:graph_enqueue)
48
+ end
49
+ end
50
+
51
+ # original enqueue enqueues the process to the database,
52
+ # here we should just output a graph elements to the output file
53
+ def graph_enqueue cmd, waitfor = [], opts = {}
54
+ gv_file = self.class.default_options.gv_file
55
+ id = self.class.next_id
56
+ waitfor.each do |wid|
57
+ # output graph edges
58
+ gv_file.puts " b#{wid} -> b#{id};"
59
+ end
60
+ # determine node label
61
+ label = opts[:label] || cmd
62
+ label.gsub!(/([^\\])"/, '\1""')
63
+ # determine node color
64
+ color = self.class.get_color(opts[:group] || opts[:environment])
65
+ # output node description
66
+ gv_file.puts " b#{id} [shape=box,style=filled,fillcolor=\"#{color}\",label=\"#{label}\"];"
67
+ # remember id
68
+ @all_ids << id
69
+ id
70
+ end
71
+ end
72
+
73
+ include GraphOutput
74
+ end
data/lib/blue_colr.rb CHANGED
@@ -11,15 +11,72 @@ require 'sequel'
11
11
 
12
12
 
13
13
  class BlueColr
14
- STATUS_OK = 'ok'
15
- STATUS_ERROR = 'error'
16
- STATUS_PENDING = 'pending'
17
- STATUS_RUNNING = 'running'
18
- STATUS_PREPARING = 'preparing'
19
- STATUS_SKIPPED = 'skipped'
14
+ # STATUS_OK = 'ok'
15
+ # STATUS_ERROR = 'error'
16
+ # STATUS_PENDING = 'pending'
17
+ # STATUS_RUNNING = 'running'
18
+ # STATUS_PREPARING = 'preparing'
19
+ # STATUS_SKIPPED = 'skipped'
20
+
21
+ # default state transitions with simple state setup ('PENDING => RUNNING => OK or ERROR')
22
+ DEFAULT_PENDING_STATE = 'pending'
23
+ PREPARING_STATE = 'preparing'
24
+ DEFAULT_STATEMAP = {
25
+ 'on_pending' => {
26
+ DEFAULT_PENDING_STATE => [
27
+ ['running', ['ok', 'skipped']]
28
+ ]
29
+ },
30
+ 'on_running' => {
31
+ 'running' => {
32
+ 'error' => 'error',
33
+ 'ok' => 'ok'
34
+ }
35
+ },
36
+ 'on_restart' => {
37
+ 'error' => 'pending',
38
+ 'ok' => 'pending'
39
+ }
40
+ }
20
41
 
21
42
  class << self
22
- attr_accessor :log, :db, :environment, :db_uri
43
+ attr_accessor :environment
44
+ attr_writer :statemap, :log, :db, :db_uri, :conf
45
+
46
+ def log
47
+ @log ||= Logger.new('process_daemon')
48
+ end
49
+
50
+ def conf
51
+ unless @conf
52
+ parse_command_line unless @args
53
+
54
+ raise "No configuration file defined (-c <config>)." if @args["config"].nil?
55
+ raise "Couldn't read #{@args["config"]} file." unless @args['config'] && @conf = YAML::load(File.new(@args["config"]).read)
56
+
57
+ # setting default options that should be written along with all the records to process_items
58
+ if @conf['default_options']
59
+ @conf['default_options'].each do |k,v|
60
+ default_options.send("#{k}=", v)
61
+ end
62
+ end
63
+ end
64
+ @conf
65
+ end
66
+
67
+ def db_uri
68
+ unless @db_uri # get the config from command line
69
+ @db_uri = self.conf['db_url']
70
+ end
71
+ @db_uri
72
+ end
73
+
74
+ def db
75
+ unless @db # not connected
76
+ @db = Sequel.connect(self.db_uri, :logger => self.log)
77
+ end
78
+ @db
79
+ end
23
80
 
24
81
  # default options to use when launching a process - every field maps to a
25
82
  # column in process_items table
@@ -32,6 +89,10 @@ class BlueColr
32
89
  @options ||= OpenStruct.new
33
90
  end
34
91
 
92
+ def statemap
93
+ @statemap ||= conf['statemap'] || DEFAULT_STATEMAP
94
+ end
95
+
35
96
  def sequential &block
36
97
  self.new.sequential &block
37
98
  end
@@ -48,25 +109,7 @@ class BlueColr
48
109
 
49
110
  # launch a set of tasks, provided within a given block
50
111
  def launch &block
51
- @log ||= Logger.new('process_daemon')
52
112
 
53
- unless @db # not connected
54
- unless @db_uri # get the config from command line
55
- @args = parse_command_line ARGV
56
-
57
- raise "No configuration file defined (-c <config>)." if @args["config"].nil?
58
- raise "Couldn't read #{@args["config"]} file." unless @args['config'] && @conf = YAML::load(File.new(@args["config"]).read)
59
-
60
- @db_uri = @conf['db_url']
61
- # setting default options that should be written along with all the records to process_items
62
- if @conf['default_options']
63
- @conf['default_options'].each do |k,v|
64
- default_options.send("#{k}=", v)
65
- end
66
- end
67
- end
68
- @db = Sequel.connect(@db_uri, :logger => @log)
69
- end
70
113
  worker = self.new
71
114
  db.transaction do
72
115
  worker.instance_eval &block
@@ -80,8 +123,8 @@ class BlueColr
80
123
  exit worker.wait
81
124
  end
82
125
 
83
- def parse_command_line(args)
84
- data = Hash.new()
126
+ def parse_command_line &block
127
+ data = {}
85
128
 
86
129
  OptionParser.new do |opts|
87
130
  opts.banner = "Usage: process_daemon.rb [options]"
@@ -91,7 +134,7 @@ class BlueColr
91
134
  end
92
135
 
93
136
  # process custom args, if given
94
- @custom_args_block.call(opts) if @custom_args_block
137
+ block.call(opts) if block_given?
95
138
 
96
139
  opts.on_tail('-h', '--help', 'display this help and exit') do
97
140
  puts opts
@@ -100,16 +143,53 @@ class BlueColr
100
143
  end
101
144
 
102
145
  # begin
103
- opts.parse(args)
146
+ opts.parse(ARGV)
104
147
  # rescue OptionParser::InvalidOption
105
148
  # # do nothing
106
149
  # end
107
150
 
108
151
  end
109
152
 
110
- return data
153
+ @args = data
111
154
  end
112
- end
155
+
156
+
157
+ # state related methods
158
+
159
+ # get the next state from pending, given current state and state of all "parent" processes
160
+ def state_from_pending current_state, parent_states
161
+ new_state, _ = self.statemap['on_pending'][current_state].find { |_, required_parent_states|
162
+ (parent_states - required_parent_states).empty?
163
+ }
164
+ new_state
165
+ end
166
+
167
+ # get the next state from running, given current state and whether the command has finished successfully
168
+ def state_from_running current_state, ok
169
+ self.statemap['on_running'][current_state][ok ? 'ok' : 'error']
170
+ end
171
+
172
+ # get the next state to get upon restart, given the current state
173
+ def state_on_restart current_state
174
+ self.statemap['on_restart'][current_state]
175
+ end
176
+
177
+ # get all possible pending states
178
+ def get_pending_states
179
+ self.statemap['on_pending'].map{|state, _| state}
180
+ end
181
+
182
+ # get all possible error states
183
+ def get_error_states
184
+ self.statemap['on_running'].map{|_, new_states| new_states['error']}
185
+ end
186
+
187
+ # get all possible ok states
188
+ def get_ok_states
189
+ self.statemap['on_running'].map{|_, new_states| new_states['ok']}
190
+ end
191
+
192
+ end # class methods
113
193
 
114
194
  attr_reader :all_ids, :result
115
195
 
@@ -151,14 +231,15 @@ class BlueColr
151
231
 
152
232
  def enqueue cmd, waitfor = [], opts = {}
153
233
  id = nil
234
+ opts = {status: DEFAULT_PENDING_STATE}.merge(opts)
154
235
  def_opts = self.class.default_options.send(:table) # convert from OpenStruct to Hash
155
- # rejecting fields that do not match to a column in the table:
236
+ # rejecting fields that do not have corresponding column in the table:
156
237
  fields = def_opts.merge(opts).select{|k,_| db[:process_items].columns.member? k}
157
- id = db[:process_items].insert(fields.merge(:status => STATUS_PREPARING, :cmd => cmd, :queued_at => Time.now))
238
+ id = db[:process_items].insert(fields.merge(:status => PREPARING_STATE, :cmd => cmd, :queued_at => Time.now))
158
239
  waitfor.each do |wid|
159
240
  db[:process_item_dependencies].insert(:process_item_id => id, :depends_on_id => wid)
160
241
  end
161
- db[:process_items].filter(:id => id).update(:status => STATUS_PENDING)
242
+ db[:process_items].filter(:id => id).update(:status => opts[:status])
162
243
  # id = TaskGroup.counter
163
244
  log.info "enqueueing #{id}: #{cmd}, waiting for #{waitfor.inspect}"
164
245
  # remember id
@@ -181,9 +262,9 @@ class BlueColr
181
262
  def wait
182
263
  log.info 'Waiting for all processes to finish'
183
264
  loop do
184
- failed = db[:process_items].filter(:id => @all_ids, :status => STATUS_ERROR).first
265
+ failed = db[:process_items].filter(:id => @all_ids, :status => BlueColr.get_error_states).first
185
266
  return failed[:exit_code] if failed
186
- not_ok_count = db[:process_items].filter(:id => @all_ids).exclude(:status => STATUS_OK).count
267
+ not_ok_count = db[:process_items].filter(:id => @all_ids).exclude(:status => BlueColr.get_ok_states).count
187
268
  return 0 if not_ok_count == 0 # all ok, finish
188
269
  sleep 10
189
270
  end
metadata CHANGED
@@ -1,7 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blue_colr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.9
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
5
11
  platform: ruby
6
12
  authors:
7
13
  - Mladen Jablanovic
@@ -9,19 +15,23 @@ autorequire:
9
15
  bindir: bin
10
16
  cert_chain: []
11
17
 
12
- date: 2011-10-13 00:00:00 +02:00
18
+ date: 2011-10-17 00:00:00 +02:00
13
19
  default_executable:
14
20
  dependencies:
15
21
  - !ruby/object:Gem::Dependency
16
22
  name: sequel
17
- type: :runtime
18
- version_requirement:
19
- version_requirements: !ruby/object:Gem::Requirement
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
20
26
  requirements:
21
27
  - - ">="
22
28
  - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
23
32
  version: "0"
24
- version:
33
+ type: :runtime
34
+ version_requirements: *id001
25
35
  description: Blue_colr provides simple DSL to enqueue processes in given order, using database table as a queue, and a deamon to run them
26
36
  email:
27
37
  - jablan@radioni.ca
@@ -36,6 +46,7 @@ files:
36
46
  - bin/bcrun
37
47
  - bin/bluecolrd
38
48
  - lib/blue_colr.rb
49
+ - lib/blue_colr/graph_output.rb
39
50
  - README.rdoc
40
51
  has_rdoc: true
41
52
  homepage: http://github.com/jablan/blue_colr
@@ -47,21 +58,27 @@ rdoc_options: []
47
58
  require_paths:
48
59
  - lib
49
60
  required_ruby_version: !ruby/object:Gem::Requirement
61
+ none: false
50
62
  requirements:
51
63
  - - ">="
52
64
  - !ruby/object:Gem::Version
65
+ hash: 3
66
+ segments:
67
+ - 0
53
68
  version: "0"
54
- version:
55
69
  required_rubygems_version: !ruby/object:Gem::Requirement
70
+ none: false
56
71
  requirements:
57
72
  - - ">="
58
73
  - !ruby/object:Gem::Version
74
+ hash: 3
75
+ segments:
76
+ - 0
59
77
  version: "0"
60
- version:
61
78
  requirements: []
62
79
 
63
80
  rubyforge_project:
64
- rubygems_version: 1.3.5
81
+ rubygems_version: 1.6.2
65
82
  signing_key:
66
83
  specification_version: 3
67
84
  summary: Database based process launcher