patriot-workflow-scheduler 0.6.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.
Files changed (79) hide show
  1. checksums.yaml +15 -0
  2. data/bin/patriot +8 -0
  3. data/bin/patriot-init +35 -0
  4. data/lib/patriot.rb +11 -0
  5. data/lib/patriot/command.rb +71 -0
  6. data/lib/patriot/command/base.rb +199 -0
  7. data/lib/patriot/command/command_group.rb +43 -0
  8. data/lib/patriot/command/command_macro.rb +141 -0
  9. data/lib/patriot/command/composite.rb +49 -0
  10. data/lib/patriot/command/parser.rb +78 -0
  11. data/lib/patriot/command/sh_command.rb +42 -0
  12. data/lib/patriot/controller.rb +2 -0
  13. data/lib/patriot/controller/package_controller.rb +81 -0
  14. data/lib/patriot/controller/worker_admin_controller.rb +159 -0
  15. data/lib/patriot/job_store.rb +66 -0
  16. data/lib/patriot/job_store/base.rb +159 -0
  17. data/lib/patriot/job_store/factory.rb +19 -0
  18. data/lib/patriot/job_store/in_memory_store.rb +252 -0
  19. data/lib/patriot/job_store/job.rb +118 -0
  20. data/lib/patriot/job_store/job_ticket.rb +30 -0
  21. data/lib/patriot/job_store/rdb_job_store.rb +353 -0
  22. data/lib/patriot/tool.rb +2 -0
  23. data/lib/patriot/tool/batch_parser.rb +102 -0
  24. data/lib/patriot/tool/patriot_command.rb +48 -0
  25. data/lib/patriot/tool/patriot_commands/execute.rb +92 -0
  26. data/lib/patriot/tool/patriot_commands/job.rb +62 -0
  27. data/lib/patriot/tool/patriot_commands/plugin.rb +41 -0
  28. data/lib/patriot/tool/patriot_commands/register.rb +77 -0
  29. data/lib/patriot/tool/patriot_commands/upgrade.rb +24 -0
  30. data/lib/patriot/tool/patriot_commands/validate.rb +84 -0
  31. data/lib/patriot/tool/patriot_commands/worker.rb +35 -0
  32. data/lib/patriot/tool/patriot_commands/worker_admin.rb +60 -0
  33. data/lib/patriot/util.rb +14 -0
  34. data/lib/patriot/util/config.rb +58 -0
  35. data/lib/patriot/util/config/base.rb +22 -0
  36. data/lib/patriot/util/config/inifile_config.rb +63 -0
  37. data/lib/patriot/util/cron_format_parser.rb +104 -0
  38. data/lib/patriot/util/date_util.rb +200 -0
  39. data/lib/patriot/util/db_client.rb +65 -0
  40. data/lib/patriot/util/db_client/base.rb +142 -0
  41. data/lib/patriot/util/db_client/hash_record.rb +53 -0
  42. data/lib/patriot/util/db_client/record.rb +25 -0
  43. data/lib/patriot/util/logger.rb +24 -0
  44. data/lib/patriot/util/logger/facade.rb +33 -0
  45. data/lib/patriot/util/logger/factory.rb +59 -0
  46. data/lib/patriot/util/logger/log4r_factory.rb +111 -0
  47. data/lib/patriot/util/logger/webrick_log_factory.rb +47 -0
  48. data/lib/patriot/util/param.rb +73 -0
  49. data/lib/patriot/util/retry.rb +30 -0
  50. data/lib/patriot/util/script.rb +52 -0
  51. data/lib/patriot/util/system.rb +120 -0
  52. data/lib/patriot/worker.rb +35 -0
  53. data/lib/patriot/worker/base.rb +153 -0
  54. data/lib/patriot/worker/info_server.rb +90 -0
  55. data/lib/patriot/worker/job_store_server.rb +32 -0
  56. data/lib/patriot/worker/multi_node_worker.rb +157 -0
  57. data/lib/patriot/worker/servlet.rb +23 -0
  58. data/lib/patriot/worker/servlet/job_servlet.rb +128 -0
  59. data/lib/patriot/worker/servlet/worker_status_servlet.rb +44 -0
  60. data/skel/batch/sample/daily/test.pbc +4 -0
  61. data/skel/config/patriot.ini +21 -0
  62. data/skel/public/css/bootstrap.css +2495 -0
  63. data/skel/public/css/original.css +54 -0
  64. data/skel/public/js/bootstrap-alerts.js +124 -0
  65. data/skel/public/js/bootstrap-buttons.js +64 -0
  66. data/skel/public/js/bootstrap-dropdown.js +55 -0
  67. data/skel/public/js/bootstrap-modal.js +260 -0
  68. data/skel/public/js/bootstrap-popover.js +90 -0
  69. data/skel/public/js/bootstrap-scrollspy.js +107 -0
  70. data/skel/public/js/bootstrap-tabs.js +80 -0
  71. data/skel/public/js/bootstrap-twipsy.js +321 -0
  72. data/skel/public/js/jquery-1.6.4.min.js +4 -0
  73. data/skel/public/templates/_jobs.erb +97 -0
  74. data/skel/public/templates/job.erb +119 -0
  75. data/skel/public/templates/jobs.erb +21 -0
  76. data/skel/public/templates/jobs_deleted.erb +6 -0
  77. data/skel/public/templates/layout.erb +103 -0
  78. data/skel/public/templates/state_updated.erb +6 -0
  79. metadata +235 -0
@@ -0,0 +1,30 @@
1
+ module Patriot
2
+ module JobStore
3
+ # a ticket to execute a job.
4
+ class JobTicket
5
+ # default attributes
6
+ attr_accessor :job_id, :update_id, :node
7
+ # attributes for offer
8
+ attr_accessor :exec_node, :exec_host, :exec_thread, :execution_id
9
+ # attributes for completion
10
+ attr_accessor :exit_code, :description
11
+
12
+ # @param [String] job_id
13
+ # @param [Integer] update_id
14
+ # @param [String] node the name of node on which the job should be executed
15
+ def initialize(job_id, update_id, node=nil)
16
+ @job_id = job_id
17
+ @update_id = update_id
18
+ @node = node
19
+ end
20
+
21
+ # @return [String] returns string expression of this instance
22
+ def to_s
23
+ node = @node.nil? ? "any" : @node
24
+ string = "job_id: #{job_id}, update_id: #{update_id}, node: #{node}"
25
+ return string
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,353 @@
1
+ require 'json'
2
+ module Patriot
3
+ module JobStore
4
+
5
+ # a JobStore implementation on RDB
6
+ class RDBJobStore < Patriot::JobStore::Base
7
+
8
+ # default priority
9
+ DEFAULT_PRIORITY=1 # TODO move to Patriot::JobStore of core
10
+
11
+ #### Tables
12
+ # job definition table
13
+ JOB_TABLE = 'jobs'
14
+ # dependency relation table
15
+ FLOW_TABLE = 'flows'
16
+ # job and produced product table
17
+ PRODUCER_TABLE = 'producers'
18
+ # job and required product table
19
+ CONSUMER_TABLE = 'consumers'
20
+ # table for execution history
21
+ HISTORY_TABLE = 'job_histories'
22
+
23
+ # date format of execution history
24
+ HISTORY_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
25
+
26
+ # attributes included in job_ticket
27
+ TICKET_COLUMNS = ['job_id', 'update_id', 'node']
28
+ # all columns of the job table
29
+ ALL_COLUMNS = [:id,
30
+ :job_id,
31
+ :job_def_id,
32
+ :update_id,
33
+ :state,
34
+ :content,
35
+ :start_after,
36
+ :node,
37
+ :host,
38
+ :priority]
39
+ # mapping from command attributes to table columns
40
+ ATTR_TO_COLUMN = {Patriot::Command::STATE_ATTR => :state,
41
+ Patriot::Command::PRIORITY_ATTR => :priority,
42
+ Patriot::Command::START_DATETIME_ATTR => :start_after,
43
+ Patriot::Command::EXEC_NODE_ATTR => :node,
44
+ Patriot::Command::EXEC_HOST_ATTR => :host}
45
+
46
+ include Patriot::Util::DBClient
47
+ include Patriot::Util::Logger
48
+ include Patriot::Util::Retry
49
+
50
+ # @see Patriot::JobStore::Base#initialize
51
+ def initialize(store_id, config)
52
+ @config = config
53
+ prefix = [Patriot::JobStore::CONFIG_PREFIX, store_id].join(".")
54
+ @db_config = read_dbconfig(prefix, config)
55
+ @logger = create_logger(config)
56
+ @initiator_id = connect(@db_config){|c| c.select(JOB_TABLE, {:job_id => Patriot::JobStore::INITIATOR_JOB_ID})[0].to_hash[:id] }
57
+ end
58
+
59
+ # @see Patriot::JobStore::Base#register
60
+ def register(update_id, jobs)
61
+ jobs.each{|job| raise "#{job.job_id} is not acceptable" unless acceptable?(job) }
62
+ @logger.info "start to register jobs"
63
+ connect(@db_config) do |c|
64
+ jobs.each{|job| upsert_job(update_id, job, c)}
65
+ c.update(JOB_TABLE,
66
+ {:state => Patriot::JobStore::JobState::WAIT},
67
+ {:state => Patriot::JobStore::JobState::INIT, :update_id => update_id}
68
+ )
69
+ end
70
+ @logger.info "job registration finished"
71
+ end
72
+
73
+ def upsert_job(update_id, job, c)
74
+ new_vals = {:job_id => job.job_id, :update_id => update_id, :priority => DEFAULT_PRIORITY}
75
+ # extract and remove comman attributes
76
+ requisites = job.delete(Patriot::Command::REQUISITES_ATTR) || []
77
+ products = job.delete(Patriot::Command::PRODUCTS_ATTR) || []
78
+
79
+ prev_vals = c.select(JOB_TABLE, {:job_id => job.job_id})
80
+ ATTR_TO_COLUMN.each do |a,c|
81
+ val = job.delete(a)
82
+ next if val.nil? && c == :state
83
+ new_vals[c] = val
84
+ end
85
+ # serialize remaining attributes
86
+ new_vals[:content] = JSON.generate(job.attributes)
87
+
88
+ if prev_vals.empty?
89
+ new_vals[:state] ||= Patriot::JobStore::JobState::INIT # set default state
90
+ serial_id = c.insert(JOB_TABLE, new_vals)
91
+ elsif prev_vals.size == 1
92
+ serial_id = prev_vals[0].to_hash[:id]
93
+ c.update(JOB_TABLE, new_vals, {:job_id => job.job_id})
94
+ end
95
+
96
+ raise "failed to upsert a job #{j}" if serial_id.nil?
97
+
98
+ update_dependency(serial_id, requisites, CONSUMER_TABLE, c)
99
+ update_dependency(serial_id, products, PRODUCER_TABLE, c)
100
+ # set dependency for initiator jobs
101
+ c.insert(FLOW_TABLE, {:producer_id => @initiator_id, :consumer_id => serial_id}, {:ignore => true}) if requisites.empty?
102
+ end
103
+ private :upsert_job
104
+
105
+ def update_dependency(serial_id, updated_products, updated_table, conn)
106
+ raise "unknown dependency table #{updated_table}" unless [CONSUMER_TABLE, PRODUCER_TABLE].include?(updated_table)
107
+ updated_col = updated_table == CONSUMER_TABLE ? :consumer_id : :producer_id
108
+ opposite_table = updated_table == CONSUMER_TABLE ? PRODUCER_TABLE : CONSUMER_TABLE
109
+ opposite_col = updated_table == CONSUMER_TABLE ? :producer_id : :consumer_id
110
+
111
+ # deleted dependency
112
+ conn.select(updated_table, {:job_id => serial_id}).each do |u|
113
+ unless updated_products.include?(u.product)
114
+ conn.delete(updated_table, {:job_id => serial_id, :product => u.product})
115
+ conn.delete(FLOW_TABLE,{updated_col=> serial_id})
116
+ end
117
+ end
118
+
119
+ # added dependency
120
+ updated_products.each do |product|
121
+ conn.insert(updated_table, {:job_id => serial_id, :product => product}, {:ignore => true})
122
+ conn.select(opposite_table, {:product => product}).each do |producer|
123
+ conn.insert(FLOW_TABLE, {updated_col => serial_id, opposite_col => producer.job_id}, {:ignore => true})
124
+ end
125
+ end
126
+ end
127
+ private :update_dependency
128
+
129
+ # @see Patriot::JobStore::Base#acceptable?
130
+ def acceptable?(job)
131
+ begin
132
+ json = JSON.generate(job.attributes)
133
+ rescue Exception => e
134
+ @logger.warn e
135
+ return false
136
+ end
137
+ return true
138
+ end
139
+
140
+ # @see Patriot::JobStore::Base#get_job_tickets
141
+ def get_job_tickets(host, nodes, options = {})
142
+ nodes = [nodes] unless nodes.is_a?(Array)
143
+ begin
144
+ query = generate_fetching_job_sql(host, nodes,options)
145
+ @logger.debug "fetchings job by #{query}"
146
+ connect(@db_config) do |c|
147
+ return c.execute_statement(query).map{|r| Patriot::JobStore::JobTicket.new(r.job_id, r.update_id, r.node) }
148
+ end
149
+ rescue => e
150
+ @logger.error e
151
+ raise e
152
+ end
153
+ end
154
+
155
+ def generate_fetching_job_sql(host, nodes, options)
156
+ node_condition = (nodes.map{|n| "c.node = '#{n}'" } | ["c.node IS NULL"]).join(" OR ")
157
+ query = <<"END_OB_QUERY"
158
+ SELECT c.#{TICKET_COLUMNS[0]}, c.#{TICKET_COLUMNS[1]}, c.#{TICKET_COLUMNS[2]}
159
+ FROM flows f
160
+ JOIN jobs c on c.id = f.consumer_id
161
+ JOIN jobs p on f.producer_id = p.id
162
+ WHERE c.state=#{Patriot::JobStore::JobState::WAIT}
163
+ AND (#{node_condition})
164
+ AND (c.host = '#{host}' OR c.host IS NULL)
165
+ AND c.content IS NOT NULL
166
+ AND (c.start_after IS NULL OR c.start_after < current_timestamp)
167
+ GROUP BY f.consumer_id HAVING Min(p.state=#{Patriot::JobStore::JobState::SUCCEEDED})=1
168
+ ORDER BY c.priority
169
+ END_OB_QUERY
170
+ query = "#{query} LIMIT #{options[:fetch_limit]} " if options.has_key?(:fetch_limit)
171
+ return query.gsub(/(\r|\n|\s+)/, ' ')
172
+ end
173
+ private :generate_fetching_job_sql
174
+
175
+ # @see Patriot::JobStore::Base#offer_to_execute
176
+ def offer_to_execute(job_ticket)
177
+ connect(@db_config) do |c|
178
+ unless _check_and_set_state(job_ticket, Patriot::JobStore::JobState::WAIT, Patriot::JobStore::JobState::RUNNING, c)
179
+ @logger.debug("execution of job: #{job_ticket.job_id} is skipped")
180
+ return nil
181
+ end
182
+ execution_id = c.insert(HISTORY_TABLE,
183
+ {:job_id => job_ticket.job_id,
184
+ :node => job_ticket.exec_node,
185
+ :host => job_ticket.exec_host,
186
+ :thread => job_ticket.exec_thread,
187
+ :begin_at => Time.now.strftime(HISTORY_DATE_FORMAT)})
188
+ record = c.select(JOB_TABLE, {:job_id => job_ticket.job_id})
189
+ raise "duplicated entry found for #{job_ticket}" if record.size > 1
190
+ raise "no entry found for #{job_ticket}" if record.empty?
191
+ job = record_to_job(record[0])
192
+ begin
193
+ return {:execution_id => execution_id, :command => job.to_command(@config)}
194
+ rescue Exception => e
195
+ marked = _check_and_set_state(job_ticket, Patriot::JobStore::JobState::RUNNING, Patriot::JobStore::JobState::FAILED, c)
196
+ @logger.error "failed to create a command for #{job_ticket.job_id} (set to error? #{marked})"
197
+ raise e
198
+ end
199
+ end
200
+ end
201
+
202
+ # @see Patriot::JobStore::Base#report_completion_status
203
+ def report_completion_status(job_ticket)
204
+ exit_code = job_ticket.exit_code
205
+ post_state = Patriot::JobStore::EXIT_CODE_TO_STATE[exit_code]
206
+ raise "illegal exit_code #{exit_code}" if post_state.nil?
207
+ connect(@db_config) do |c|
208
+ if c.update(HISTORY_TABLE, {:end_at => Time.now.strftime(HISTORY_DATE_FORMAT), :exit_code => exit_code, :description => job_ticket.description}, {:id => job_ticket.execution_id}) != 1
209
+ @logger.warn "illegal state of history for #{job_ticket.job_id}"
210
+ end
211
+ return _check_and_set_state(job_ticket, Patriot::JobStore::JobState::RUNNING, post_state, c)
212
+ end
213
+ end
214
+
215
+ def _check_and_set_state(job_ticket, prev_state, post_state, conn)
216
+ @logger.debug("changing state of #{job_ticket.job_id} from #{prev_state} to #{post_state}")
217
+ condition = {:job_id => job_ticket.job_id, :state => prev_state, :update_id => job_ticket.update_id}
218
+ num_updated = conn.update(JOB_TABLE, {:state => post_state}, condition)
219
+ if num_updated == 0 # in case of job is redfined
220
+ @logger.info("definition or state of job: #{job_ticket.job_id} is changed and its state is not changed")
221
+ return false
222
+ elsif num_updated != 1
223
+ raise "illegal state: #{job_ticket.job_id} has more than #{num_updated} records"
224
+ end
225
+ return true
226
+ end
227
+ private :_check_and_set_state
228
+
229
+ # @see Patriot::JobStore::Base#set_state
230
+ def set_state(update_id, job_ids, new_state)
231
+ raise "jobs are not selected" if job_ids.nil? || job_ids.empty?
232
+ stmt = "UPDATE jobs SET state = #{new_state} WHERE #{job_ids.map{|jid| "job_id = '#{jid}'"}.join(" OR ")}"
233
+ connect(@db_config){|c| c.execute_statement(stmt, :update)}
234
+ end
235
+
236
+ # @see Patriot::JobStore::Base#get_job
237
+ def get_job(job_id)
238
+ connect(@db_config) do |c|
239
+ records = c.select(JOB_TABLE, {:job_id => job_id})
240
+ return nil if records.empty?
241
+ raise "duplicate job_ticket for #{job_id}" unless records.size == 1
242
+ record = records[0]
243
+ serial_id = record.to_hash[:id]
244
+ job = record_to_job(record)
245
+ job[Patriot::Command::PRODUCTS_ATTR] = c.select(PRODUCER_TABLE, {:job_id => serial_id}).map{|r| r.product}
246
+ job[Patriot::Command::REQUISITES_ATTR] = c.select(CONSUMER_TABLE, {:job_id => serial_id}).map{|r| r.product}
247
+ return job
248
+ end
249
+ end
250
+
251
+ # @see Patriot::JobStore::Base#get_producers
252
+ def get_producers(products, opts = {:include_attrs => [Patriot::Command::STATE_ATTR]})
253
+ return _get_jobs_for_products(PRODUCER_TABLE, products, opts)
254
+ end
255
+
256
+ # @see Patriot::JobStore::Base#get_producers
257
+ def get_consumers(products, opts = {:include_attrs => [Patriot::Command::STATE_ATTR]})
258
+ return _get_jobs_for_products(CONSUMER_TABLE, products, opts)
259
+ end
260
+
261
+ def _get_jobs_for_products(table, products, opts = {:include_attrs => [Patriot::Command::STATE_ATTR]})
262
+ result = {}
263
+ return result if products.nil?
264
+ products = [products] unless products.is_a? Array
265
+ included_cols = (opts[:include_attrs] || []).map{|a| ATTR_TO_COLUMN[a]}
266
+ connect(@db_config) do |c|
267
+ products.each do |product|
268
+ jids = c.select(table, {:product => product}).map{|r| r.job_id}
269
+ next if jids.empty?
270
+ included_cols = (['job_id'] | (included_cols || [])).uniq
271
+ query = "SELECT job_id, #{included_cols.join(', ')} FROM jobs WHERE #{jids.map{|jid| "id = #{jid}" }.join(' OR ')}"
272
+ c.execute_statement(query, :select).each do |r|
273
+ hashval = r.to_hash
274
+ result[hashval.delete(:job_id)] = hashval
275
+ end
276
+ end
277
+ end
278
+ return result
279
+ end
280
+ private :_get_jobs_for_products
281
+
282
+ # @see Patriot::JobStore::JobState
283
+ def get_execution_history(job_id, opts = {})
284
+ opts = {:limit => 1, :order => :DESC}.merge(opts)
285
+ query = "SELECT * FROM #{HISTORY_TABLE} WHERE job_id = '#{job_id}' ORDER BY id #{opts[:order]} LIMIT #{opts[:limit]}"
286
+ connect(@db_config) do |c|
287
+ return c.execute_statement(query, :select).map(&:to_hash)
288
+ end
289
+ end
290
+
291
+ # @see Patriot::JobStore::Base#find_jobs_by_state
292
+ def find_jobs_by_state(state, opts = {})
293
+ raise "OFFSET is set WITHOUT LIMIT" if opts.has_key?(:offset) && !opts.has_key?(:limit)
294
+ condition = ["state = #{state}", "id != #{@initiator_id}"]
295
+ condition |= ["job_id LIKE '#{opts[:filter_exp]}'"] if opts.has_key?(:filter_exp)
296
+ query = "SELECT job_id FROM jobs WHERE #{condition.join(' AND ')}"
297
+ query = "#{query} ORDER BY job_id DESC"
298
+ if opts.has_key?(:limit)
299
+ query = "#{query} LIMIT #{opts[:limit]}"
300
+ query = "#{query} OFFSET #{opts[:offset]}" if opts.has_key?(:offset)
301
+ end
302
+ connect(@db_config) do |c|
303
+ return c.execute_statement(query, :select).map{|r| r.job_id }
304
+ end
305
+ end
306
+
307
+ # @see Patriot::JobStore::Base#get_job_size
308
+ def get_job_size(opts = {})
309
+ opts = {:ignore_states => []}.merge(opts)
310
+ if opts[:ignore_states].empty?
311
+ query = "SELECT state, count(1) size FROM jobs GROUP BY state"
312
+ else
313
+ query = "SELECT state, count(1) size FROM jobs WHERE #{opts[:ignore_states].map{|s| "state != #{s}" }.join(" AND ")} GROUP BY state"
314
+ end
315
+ sizes = {}
316
+ connect(@db_config) do |c|
317
+ c.execute_statement(query).each do |r|
318
+ sizes[r.state] = r.size
319
+ sizes[r.state] = sizes[r.state] - 1 if r.state == Patriot::JobStore::JobState::SUCCEEDED # ignore initiator
320
+ end
321
+ end
322
+ return sizes
323
+ end
324
+
325
+ # @see Patriot::JobStore::Base#delete_job
326
+ def delete_job(job_id)
327
+ connect(@db_config) do |c|
328
+ record = c.select(JOB_TABLE, {:job_id => job_id})
329
+ return if record.nil? || record.empty?
330
+ raise "illegal state: more than one records for #{job_id}" if record.size > 1
331
+ serial_id = record[0].to_hash[:id]
332
+ c.delete(CONSUMER_TABLE, {:job_id => serial_id})
333
+ c.delete(PRODUCER_TABLE, {:job_id => serial_id})
334
+ c.delete(FLOW_TABLE, {:consumer_id => serial_id})
335
+ c.delete(FLOW_TABLE, {:producer_id => serial_id})
336
+ c.delete(JOB_TABLE, {:job_id => job_id})
337
+ end
338
+ end
339
+
340
+ def record_to_job(record)
341
+ job = Patriot::JobStore::Job.new(record.job_id)
342
+ job.update_id = record.update_id
343
+ ATTR_TO_COLUMN.each{|attr, col| job[attr] = record.send(col) }
344
+ unless record.content.nil?
345
+ content = JSON.parse(record.content)
346
+ content.each{|k,v| job[k] = v}
347
+ end
348
+ return job
349
+ end
350
+ private :record_to_job
351
+ end
352
+ end
353
+ end
@@ -0,0 +1,2 @@
1
+ require 'patriot/tool/batch_parser'
2
+ require 'patriot/tool/patriot_command'
@@ -0,0 +1,102 @@
1
+ module Patriot
2
+ module Tool
3
+ # PBC parser
4
+ class BatchParser
5
+ include Patriot::Util::Logger
6
+ include Patriot::Util::DateUtil
7
+ include Patriot::Util::Script
8
+ include Patriot::Util::CronFormatParser
9
+
10
+ # default interval is daily
11
+ DEFAULT_INTERVAL = '0 0 * * *'
12
+
13
+ # @param config [Patriot::Util::Config::Base] configuration
14
+ def initialize(config)
15
+ @config = config
16
+ @logger = create_logger(config)
17
+ end
18
+
19
+ # parse PBC files and process commands specified in the PBC files
20
+ # @param date [String] a date (yyyy-MM-dd) or range of dates (yyyy-MM-dd,yyyy-MM-dd)
21
+ # @param paths [String|Array] paths to PBC files to be parsed
22
+ # @param options parse options
23
+ # @option options [String] :filter a regular expression to extract target commands
24
+ # @yield block to process each command
25
+ # @yieldparam cmd [Patriot::Command::Base] parsed command
26
+ # @yieldparam source [Hash] location of the file has the command. (:path => <path to the file>)
27
+ # @return an array of commands
28
+ def process(date, paths, options = {}, &blk)
29
+ dates = validate_and_parse_dates(date)
30
+ commands = []
31
+ dates.each do |d|
32
+ files = paths.map {|path| get_batch_files(path, date)}.flatten
33
+ if files.nil? || files.size == 0
34
+ @logger.warn "ERROR: no pbc exists #{paths}"
35
+ next
36
+ end
37
+ commands |= parse(d, files, options){|cmd, source| yield(cmd, source) if block_given?}
38
+ end
39
+ return commands
40
+ end
41
+
42
+ # parse PBC files and return a set of commands specified in the PBC files
43
+ # @param date [String] date (in yyyy-MM-dd) of which jobs are built by this parser
44
+ # @param files [String|Array] PBC files to be parsed
45
+ # @param options parse options
46
+ # @option options [String] :filter a regular expression to extract target commands
47
+ # @param blk block to process each command
48
+ # @return an array of commands
49
+ def parse(date, files, options = {}, &blk)
50
+ return if files.empty?
51
+ datetime = DateTime.parse(date)
52
+ # for backward compatibility to be removed
53
+ $dt = date
54
+ $month = date.split('-').values_at(0,1).join('-')
55
+ commands = []
56
+ filter = (options[:filter]) ? Regexp.new(options[:filter]) : nil
57
+
58
+ files = [files] unless files.is_a?(Array)
59
+ files.each do |file|
60
+ @logger.info "parsing #{file}"
61
+ open(file) do |f|
62
+ exp = ""
63
+ preprocess = []
64
+ while((line = f.gets) != nil) do
65
+ if(exp.empty? && line.start_with?('#'))
66
+ preprocess << line
67
+ end
68
+ exp << line
69
+ end
70
+ context = {:interval => DEFAULT_INTERVAL}.merge(parse_preprocess(preprocess))
71
+ expand_on_date(datetime, context[:interval]).each do |dt|
72
+ dsl_parser.parse(dt, exp).flatten.each do |cmd|
73
+ next unless filter.nil? || cmd.job_id =~ filter
74
+ yield(cmd, {:path => file}) if block_given?
75
+ commands << cmd
76
+ end
77
+ end
78
+ end
79
+ end
80
+ return commands
81
+ end
82
+
83
+ # parse pre processors
84
+ # @param pre_process [Array<String>] a list of pre processors
85
+ def parse_preprocess(pre_process)
86
+ context = {}
87
+ pre_process.each do |op|
88
+ if op.start_with?('#interval ')
89
+ context[:interval] = op.split(' ', 2)[1].strip
90
+ end
91
+ end
92
+ return context
93
+ end
94
+
95
+ # return parser instance
96
+ def dsl_parser
97
+ # CommandGroup includes the Patriot::Parser module
98
+ return Patriot::Command::CommandGroup.new(@config)
99
+ end
100
+ end
101
+ end
102
+ end