blue_colr 0.0.6

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.
Files changed (5) hide show
  1. data/README.rdoc +77 -0
  2. data/bin/bcrun +23 -0
  3. data/bin/bluecolrd +182 -0
  4. data/lib/blue_colr.rb +189 -0
  5. metadata +85 -0
data/README.rdoc ADDED
@@ -0,0 +1,77 @@
1
+ = blue_colr, database-based process launcher
2
+
3
+ == Overview
4
+
5
+ blue_colr allows you to easily launch processes using database as a queue. It
6
+ consists of +bluecolrd+, a deamon that executes whatever finds in a queue,
7
+ and a DSL for enqueuing processes that enables you to easily describe the order
8
+ and dependencies of processes.
9
+
10
+ == Installation
11
+
12
+ git clone git://github.com/jablan/blue_colr.git
13
+ cd blue_colr
14
+ gem build blue_colr.gemspec
15
+ gem install blue_colr-0.0.6.gem
16
+
17
+ You may want to install +log4r+ gem as well, as it provides more powerful logging
18
+ features than builtin Ruby's +Logger+.
19
+
20
+ == Example
21
+
22
+ require 'blue_colr'
23
+
24
+ BlueColr.start do
25
+ run 'echo These processes'
26
+ run 'echo will be ran sequentially.'
27
+ parallel do
28
+ run 'echo And these'
29
+ sequential do
30
+ run 'echo (but not'
31
+ run 'echo these two)'
32
+ end
33
+ run 'echo in parallel.'
34
+ end
35
+ run 'echo These will execute'
36
+ run 'echo after all above are finished.'
37
+ end
38
+
39
+ Previous code will queue processes within the database, keeping them in
40
+ dependency order. Those within +sequential+ block (and in root block, by
41
+ default) will run each after the one before finishes. Those within +parallel+
42
+ block will run in parallel. The commands after +parallel+ block will be executed
43
+ after _all_ the commands in +parallel+ block are sucessfully finished.
44
+
45
+ Note: the code above will not _start_ the processes by itself, but enqueue them
46
+ to the database, by default. A separate process called +bluecolrd+ is
47
+ used for that.
48
+
49
+ == <tt>bluecolrd</tt>
50
+
51
+ Blue_colr daemon is constantly running, checking the database for newly enqueued
52
+ processes, and executing them in a subshell, observing the order.
53
+
54
+ == <tt>bcrun</tt>
55
+
56
+ This script is used to launch arbitrary command through blue_colr. You might want
57
+ to do that if you want to keep track of the stuff you launch (as everything goes
58
+ through a database table).
59
+
60
+ bcrun -c path_to_config.yaml -x "command to execute"
61
+
62
+ == Enviroments
63
+
64
+ An environment is something like _category_ which you assign to a set of processes
65
+ when enqueuing them. Then you can have multiple daemons running, each one of them
66
+ targeting specific environment. That allows easy distribution of your tasks across
67
+ multiple machines, while keeping them synchronized, like the following scenario:
68
+
69
+ * Start tasks a and b on machine X and c on machine Y
70
+ * When all above are sucessfully done, start task d on machine Z
71
+
72
+ == ToDo
73
+
74
+ * Scripts to create necessarry tables
75
+ * Proper test code
76
+ * Examples
77
+
data/bin/bcrun ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # utility to launch bluecolr processes
4
+
5
+ require "rubygems"
6
+ require 'blue_colr'
7
+
8
+ BlueColr.log = Logger.new(STDOUT)
9
+ cmd = ''
10
+
11
+ BlueColr.custom_args do |opts|
12
+ opts.banner = "Usage: bcrun [options]"
13
+
14
+ opts.on("-x cmd", "--execute command", "Command to execute (enclose in quotes if contains parameters)") do |command|
15
+ cmd = command
16
+ end
17
+ end
18
+
19
+ # queuing processes sequentially
20
+ BlueColr.launch do
21
+ run cmd
22
+ end
23
+
data/bin/bluecolrd ADDED
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # daemon to run blue_colr processes
4
+
5
+ require "rubygems"
6
+ require 'date'
7
+ require 'yaml'
8
+ begin
9
+ require 'log4r' # try using log4r if available
10
+ require 'log4r/yamlconfigurator'
11
+ # include Log4r
12
+ rescue LoadError
13
+ require 'logger' # otherwise, use plain ruby's one
14
+ end
15
+ require "optparse"
16
+ require 'sequel'
17
+ require 'blue_colr'
18
+ require 'fileutils'
19
+
20
+ def logger(name = nil)
21
+ @logger[name || @environment] || @logger['default']
22
+ end
23
+
24
+ def init_logger
25
+ if @conf['log4r_config']
26
+ log_cfg = Log4r::YamlConfigurator # shorthand
27
+ log_cfg['ENVIRONMENT'] = @environment if @environment
28
+ log_cfg['LOGFILENAME'] = @log_file
29
+
30
+ # load the YAML file with this
31
+ log_cfg.decode_yaml(@conf['log4r_config'])
32
+
33
+ @logger = Log4r::Logger
34
+ else
35
+ @logger = {'default' => Logger.new(@log_file)}
36
+ end
37
+ logger.level = @args['debuglevel'] || Logger::WARN
38
+ end
39
+
40
+ def parse_command_line(args)
41
+ data = {}
42
+
43
+ OptionParser.new do |opts|
44
+ opts.banner = "Usage: bluecolrd [options]"
45
+
46
+ opts.on("-c CONFIG", "--conf CONFIG", "YAML config file") do |config|
47
+ data["config"] = config
48
+ end
49
+
50
+ opts.on("-e NAME", "--environment NAME", "Environment name (e.g. test, production etc.) to work on (default none)") do |env|
51
+ data["environment"] = env
52
+ end
53
+
54
+ opts.on("-m COUNT", "--max-count COUNT", "Max number of simultaneous processes to start.") do |count|
55
+ data["max"] = count.to_i
56
+ end
57
+
58
+ opts.on("-l LOGFILE", "--logfile LOGFILE", "File to log to.") do |logfile|
59
+ data["logfile"] = logfile
60
+ end
61
+
62
+ opts.on("-d LEVEL", "--debuglevel LEVEL", "Debug level to use (0 - DEBUG, 1 - INFO etc).") do |level|
63
+ data['debuglevel'] = level.to_i
64
+ end
65
+
66
+ opts.on_tail('-h', '--help', 'display this help and exit') do
67
+ puts opts
68
+ return nil
69
+ end
70
+
71
+ opts.parse(args)
72
+ end
73
+
74
+ return data
75
+ end
76
+
77
+ # check whether it's ok to spawn another process
78
+ def ok_to_run?
79
+ # check the limit of max processes, if given TODO: @pids is not used anymore, this is not working.
80
+ @max_processes == 0 || @pids.size < @max_processes
81
+ # !@args['max'] || @pids.size < @args['max']
82
+ end
83
+
84
+ def run process
85
+ logger.debug "Running #{process[:module_name]}"
86
+ script = process[:cmd]
87
+ logger.debug script
88
+ id = process[:id]
89
+
90
+ # update process item in the db
91
+ # set status of process_item to "running"
92
+ @db[:process_items].filter(:id => id).update(:status => BlueColr::STATUS_RUNNING, :started_at => Time.now)
93
+
94
+ log_path = @conf['log_path'] || '.'
95
+ log_path = (process[:process_from] || Time.now).strftime(log_path) # interpolate date
96
+
97
+ FileUtils.mkdir_p log_path
98
+ log_file = File.join(log_path, "#{id}.out")
99
+ # run actual command
100
+ Thread.new do
101
+ begin
102
+ Dir.chdir(process[:chdir]) if process[:chdir]
103
+ Kernel.system("#{script} >> #{log_file} 2>&1")
104
+ ok = $?.success?
105
+ exitstatus = $?.exitstatus
106
+ rescue
107
+ # do nothing, just exit with error
108
+ # this usually means that exec tried to execute a file that doesn't exist
109
+ ok = false
110
+ exitstatus = 99
111
+ end
112
+
113
+ # find corresponding process_item
114
+ # change its status in the DB and update ended_at timestamp
115
+ @db[:process_items].filter(:id => process[:id]).update(
116
+ :status => ok ? BlueColr::STATUS_OK : BlueColr::STATUS_ERROR,
117
+ :exit_code => exitstatus,
118
+ :ended_at => Time.now
119
+ )
120
+ logger(process[:logger]).error(@error_log_msg % process.to_hash) unless ok
121
+
122
+ logger.info "Process ended: id #{process[:id]} #{$?}"
123
+ end
124
+ end
125
+
126
+ # MAIN PROGRAM STARTS HERE
127
+
128
+ # pid => process, hash of started processes
129
+ @pids = {}
130
+
131
+ @args = parse_command_line(ARGV)
132
+
133
+ raise "No configuration file defined (-c <config>)." unless @args && @args["config"]
134
+ raise "Couldn't read #{@args["config"]} file." unless @args['config'] && @conf = YAML::load(File.new(@args["config"]).read)
135
+ @max_processes = @args['max'] || @conf['max_processes'] || 0 # default unlimited
136
+ @environment = @args['environment'] || @conf['environment'] || nil
137
+ @log_file = @args['logfile'] || "process_daemon_#{@environment}"
138
+ @error_log_msg = @conf['error_log_msg'] || 'Process failed: id %{id}'
139
+
140
+ init_logger
141
+
142
+ begin
143
+ @db = Sequel.connect(@conf['db_url'], :logger => logger('sequel')) # try to use sequel logger, if defined
144
+
145
+ logger.info 'Starting daemon'
146
+
147
+ loop do
148
+ # get all pending items
149
+ query = "select i.id
150
+ from process_items i
151
+ left join process_item_dependencies d ON i.id = d.process_item_id
152
+ left join process_items i2 ON d.depends_on_id = i2.id and i2.status NOT IN ('#{BlueColr::STATUS_OK}', '#{BlueColr::STATUS_SKIPPED}')
153
+ where i.status = '#{BlueColr::STATUS_PENDING}' and i.environment = ?
154
+ group by i.id
155
+ having count(i2.id) = 0"
156
+ process_items = @db[query, @environment]
157
+
158
+ process_items.each do |id|
159
+ logger.debug "Pending item: #{id.inspect}"
160
+ if ok_to_run?
161
+ item = @db[:process_items].filter(:id => id[:id]).first
162
+ run(item)
163
+ end
164
+ end
165
+ sleep(@conf['sleep_interval'] || 10)
166
+ end
167
+
168
+ rescue Interrupt
169
+ if logger
170
+ logger.fatal("Ctrl-C received, exiting")
171
+ else
172
+ puts "Ctrl-C received, exiting"
173
+ end
174
+ exit 1
175
+ rescue Exception => ex
176
+ p ex.class
177
+ logger.fatal(ex.to_s) if logger
178
+ puts "#{ex.to_s} ==>"
179
+ puts ex.backtrace.join("\n")
180
+ exit 1
181
+ end
182
+
data/lib/blue_colr.rb ADDED
@@ -0,0 +1,189 @@
1
+ # This class provides a simple DSL for enqueuing processes to the database
2
+ # in particular order.
3
+
4
+ require 'rubygems'
5
+ require 'date'
6
+ require 'logger'
7
+ require 'ostruct'
8
+ require 'optparse'
9
+ require 'yaml'
10
+ require 'sequel'
11
+
12
+
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'
20
+
21
+ class << self
22
+ attr_accessor :log, :db, :environment, :db_uri
23
+
24
+ # default options to use when launching a process - every field maps to a
25
+ # column in process_items table
26
+ def default_options
27
+ @default_options ||= OpenStruct.new
28
+ end
29
+
30
+ # local hash used to store misc runtime options
31
+ def options
32
+ @options ||= OpenStruct.new
33
+ end
34
+
35
+ def sequential &block
36
+ self.new.sequential &block
37
+ end
38
+
39
+ def parallel &block
40
+ self.new.parallel &block
41
+ end
42
+
43
+ # set custom commandline parameters from parent script, will be called upon
44
+ # command line parameter extraction
45
+ def custom_args &block
46
+ @custom_args_block = block
47
+ end
48
+
49
+ # launch a set of tasks, provided within a given block
50
+ def launch &block
51
+ @log ||= Logger.new('process_daemon')
52
+
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
+ worker = self.new
71
+ db.transaction do
72
+ worker.instance_eval &block
73
+ end
74
+ worker
75
+ end
76
+
77
+ # run a set of tasks (launch it and wait until the last one finishes). exit with returned exitcode.
78
+ def run &block
79
+ worker = launch &block
80
+ exit worker.wait
81
+ end
82
+
83
+ def parse_command_line(args)
84
+ data = Hash.new()
85
+
86
+ OptionParser.new do |opts|
87
+ opts.banner = "Usage: process_daemon.rb [options]"
88
+
89
+ opts.on("-c CONFIG", "--conf CONFIG", "YAML config file") do |config|
90
+ data["config"] = config
91
+ end
92
+
93
+ # process custom args, if given
94
+ @custom_args_block.call(opts) if @custom_args_block
95
+
96
+ opts.on_tail('-h', '--help', 'display this help and exit') do
97
+ puts opts
98
+ exit
99
+ # return nil
100
+ end
101
+
102
+ # begin
103
+ opts.parse(args)
104
+ # rescue OptionParser::InvalidOption
105
+ # # do nothing
106
+ # end
107
+
108
+ end
109
+
110
+ return data
111
+ end
112
+ end
113
+
114
+ attr_reader :all_ids, :result
115
+
116
+ def initialize type = :sequential, waitfor = []
117
+ @type = type
118
+ @waitfor = waitfor
119
+ @result = []
120
+ @all_ids = [] # list of all ids of processes enqueued, used if waiting
121
+ end
122
+
123
+ def db
124
+ self.class.db
125
+ end
126
+
127
+ def log
128
+ self.class.log
129
+ end
130
+
131
+ def sequential &block
132
+ exec :sequential, &block
133
+ end
134
+
135
+ def parallel &block
136
+ exec :parallel, &block
137
+ end
138
+
139
+ def exec type = :sequential, &block
140
+ g = self.class.new type, @waitfor
141
+ g.instance_eval &block
142
+ ids = g.result
143
+ if @type == :sequential
144
+ @waitfor = ids
145
+ @result = ids
146
+ else
147
+ @result += ids
148
+ end
149
+ @result
150
+ end
151
+
152
+ def enqueue cmd, waitfor = [], opts = {}
153
+ id = nil
154
+ def_opts = self.class.default_options.send(:table) # convert from OpenStruct to Hash
155
+ id = db[:process_items].insert(def_opts.merge(opts).merge(:status => STATUS_PREPARING, :cmd => cmd, :queued_at => Time.now))
156
+ waitfor.each do |wid|
157
+ db[:process_item_dependencies].insert(:process_item_id => id, :depends_on_id => wid)
158
+ end
159
+ db[:process_items].filter(:id => id).update(:status => STATUS_PENDING)
160
+ # id = TaskGroup.counter
161
+ log.info "enqueueing #{id}: #{cmd}, waiting for #{waitfor.inspect}"
162
+ # remember id
163
+ @all_ids << id
164
+ id
165
+ end
166
+
167
+ def run cmd, opts = {}
168
+ id = enqueue cmd, @waitfor, opts
169
+ if @type == :sequential
170
+ @waitfor = [id]
171
+ @result = [id]
172
+ else
173
+ @result << id
174
+ end
175
+ @result
176
+ end
177
+
178
+ # wait for all enqueued processes to finish
179
+ def wait
180
+ log.info 'Waiting for all processes to finish'
181
+ loop do
182
+ failed = db[:process_items].filter(:id => @all_ids, :status => STATUS_ERROR).first
183
+ return failed[:exit_code] if failed
184
+ not_ok_count = db[:process_items].filter(:id => @all_ids).exclude(:status => STATUS_OK).count
185
+ return 0 if not_ok_count == 0 # all ok, finish
186
+ sleep 10
187
+ end
188
+ end
189
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: blue_colr
3
+ version: !ruby/object:Gem::Version
4
+ hash: 19
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 6
10
+ version: 0.0.6
11
+ platform: ruby
12
+ authors:
13
+ - Mladen Jablanovic
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-07-07 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: sequel
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
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
36
+ email:
37
+ - jablan@radioni.ca
38
+ executables:
39
+ - bluecolrd
40
+ - bcrun
41
+ extensions: []
42
+
43
+ extra_rdoc_files: []
44
+
45
+ files:
46
+ - bin/bcrun
47
+ - bin/bluecolrd
48
+ - lib/blue_colr.rb
49
+ - README.rdoc
50
+ has_rdoc: true
51
+ homepage: http://github.com/jablan/blue_colr
52
+ licenses: []
53
+
54
+ post_install_message:
55
+ rdoc_options: []
56
+
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ hash: 3
65
+ segments:
66
+ - 0
67
+ version: "0"
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ hash: 3
74
+ segments:
75
+ - 0
76
+ version: "0"
77
+ requirements: []
78
+
79
+ rubyforge_project:
80
+ rubygems_version: 1.3.7
81
+ signing_key:
82
+ specification_version: 3
83
+ summary: Database based process launcher
84
+ test_files: []
85
+