blue_colr 0.0.6

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