bricolage 5.30.0 → 6.0.0beta1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd34b3e85926b0f85f76d05415341066025a5117c77d1b3b644ac9ce3358f0d2
4
- data.tar.gz: 067eed98758a6de824a0ab9133830a637f3cc3790e26974a946d39eaa54032fc
3
+ metadata.gz: 111b4bb62234f5a428ccd1da5a5ee2d6d4b1a051c8fda9ddd6fac3e590356797
4
+ data.tar.gz: 77c3857f92a721503bd07f6e235c81f454d61e3a55e111e7a2c0ce72efa8f6ed
5
5
  SHA512:
6
- metadata.gz: d4601475e0c9cbd70e2f54a68ddb06e417288fc52230b3ac553ff8a3f1ef79dafa6d83a8e543470d9c5cfd9d040a98df66217e2cd7a39aec6e1cd4b342640b0f
7
- data.tar.gz: aa8d9bf658e8f36a84eacd18c3d680a18178d7612c995fbdbb8dfd695720331f953607614c1ad2d467c8a211e6fd6bf29f853257455b72700fe93172846375e3
6
+ metadata.gz: ca02be7fe28f3323f8dcfa1f9c88a0dd70050f7af75109ee077d0347a5f7a4420bb7f7a6b80d9ad207bd447b640089b979e319fa9ed3cf3465e11039327bc2f3
7
+ data.tar.gz: 7d9d77d27f31d5368a694b9514a1276532335e1f980b2dd45fa77b7a4f30cc500b4dc1fb6a99a0a2f5dd35b42c9e155201764ba3d1d9a253b52790acf1f92857
data/.gitignore CHANGED
@@ -17,6 +17,4 @@ tmp
17
17
  _yardoc
18
18
  doc/
19
19
 
20
- /dev
21
- /test/home/config
22
- Gemfile.lock
20
+ /Gemfile.lock
@@ -23,4 +23,5 @@ Gem::Specification.new do |s|
23
23
  s.add_development_dependency 'test-unit'
24
24
  s.add_development_dependency 'rake'
25
25
  s.add_development_dependency 'mocha'
26
+ s.add_development_dependency 'pry-byebug'
26
27
  end
@@ -212,7 +212,7 @@ class StreamingLoadJobClass < RubyJobClass
212
212
  end
213
213
  @logger.info "creating manifest: #{manifest_name}"
214
214
  json = make_manifest_json(objects)
215
- @logger.info "manifest:\n" + json
215
+ @logger.debug "manifest:\n" + json
216
216
  url = @src.put_control_file(manifest_name, json, noop: @noop)
217
217
  yield url
218
218
  @src.remove_control_file(File.basename(url), noop: @noop) unless @keep_ctl
@@ -241,7 +241,7 @@ class StreamingLoadJobClass < RubyJobClass
241
241
  log_name = "load_log-#{@job_process_id}.csv"
242
242
  @logger.info "creating tmp load log: #{log_name}"
243
243
  csv = make_load_log_csv(objects)
244
- @logger.info "load_log:\n" + csv
244
+ @logger.debug "load_log:\n" + csv
245
245
  url = @src.put_control_file(log_name, csv, noop: @noop)
246
246
  begin
247
247
  yield url
@@ -39,7 +39,7 @@ module Bricolage
39
39
  @hooks.run_before_option_parsing_hooks(opts)
40
40
  opts.parse!(ARGV)
41
41
 
42
- @ctx = Context.for_application(opts.home, opts.job_file, environment: opts.environment, global_variables: opts.global_variables)
42
+ @ctx = Context.for_application(opts.home, opts.job_file, environment: opts.environment, option_variables: opts.option_variables)
43
43
  opts.merge_saved_options(@ctx.load_system_options)
44
44
 
45
45
  if opts.dump_options?
@@ -294,7 +294,7 @@ module Bricolage
294
294
  @job_file = nil
295
295
  @environment = nil
296
296
  @home = nil
297
- @global_variables = Variables.new
297
+ @option_variables = Variables.new
298
298
  @dry_run = false
299
299
  @explain = false
300
300
  @list_global_variables = false
@@ -351,9 +351,9 @@ Global Options:
351
351
  parser.on('-r', '--require=FEATURE', 'Requires ruby library.') {|feature|
352
352
  require feature
353
353
  }
354
- parser.on('-v', '--variable=NAME=VALUE', 'Set global variable (is different from job-level -v !!).') {|name_value|
354
+ parser.on('-v', '--variable=NAME=VALUE', 'Set option variable.') {|name_value|
355
355
  name, value = name_value.split('=', 2)
356
- @global_variables[name] = value
356
+ @option_variables[name] = value
357
357
  }
358
358
  parser.on('--dump-options', 'Shows option parsing result and quit.') {
359
359
  @dump_options = true
@@ -401,7 +401,7 @@ Global Options:
401
401
  @dump_options
402
402
  end
403
403
 
404
- attr_reader :global_variables
404
+ attr_reader :option_variables
405
405
 
406
406
  def list_global_variables?
407
407
  @list_global_variables
@@ -19,7 +19,7 @@ module Bricolage
19
19
  FileSystem.home_path(opt_path)
20
20
  end
21
21
 
22
- def Context.for_application(home_path = nil, job_path_0 = nil, job_path: nil, environment: nil, global_variables: nil, logger: nil)
22
+ def Context.for_application(home_path = nil, job_path_0 = nil, job_path: nil, environment: nil, option_variables: nil, logger: nil)
23
23
  env = environment(environment)
24
24
  if (job_path ||= job_path_0)
25
25
  fs = FileSystem.for_job_path(job_path, env)
@@ -29,24 +29,28 @@ module Bricolage
29
29
  else
30
30
  fs = FileSystem.for_options(home_path, env)
31
31
  end
32
- load(fs, env, global_variables: global_variables, logger: logger)
32
+ load(fs, env, option_variables: option_variables, logger: logger)
33
33
  end
34
34
 
35
- def Context.load(fs, env, global_variables: nil, data_sources: nil, logger: nil)
36
- new(fs, env, global_variables: global_variables, logger: logger).tap {|ctx|
35
+ def Context.load(fs, env, option_variables: nil, data_sources: nil, logger: nil)
36
+ new(fs, env, option_variables: option_variables, logger: logger).tap {|ctx|
37
37
  ctx.load_configurations
38
38
  }
39
39
  end
40
40
  private_class_method :load
41
41
 
42
- def initialize(fs, env, global_variables: nil, data_sources: nil, logger: nil)
43
- @logger = logger || Logger.default
42
+ def initialize(fs, env, option_variables: nil, data_sources: nil, logger: nil)
43
+ @logger = logger || default_logger(env)
44
44
  @filesystem = fs
45
45
  @environment = env
46
- @opt_global_variables = global_variables || Variables.new
46
+ @option_variables = option_variables || Variables.new
47
47
  @data_sources = data_sources
48
48
  end
49
49
 
50
+ private def default_logger(env)
51
+ Logger.new(device: $stderr, level: Logger::INFO)
52
+ end
53
+
50
54
  def load_configurations
51
55
  @filesystem.config_pathes('prelude.rb').each do |path|
52
56
  EmbeddedCodeAPI.module_eval(File.read(path), path.to_s, 1) if path.exist?
@@ -56,6 +60,7 @@ module Bricolage
56
60
 
57
61
  attr_reader :environment
58
62
  attr_reader :logger
63
+ attr_reader :option_variables
59
64
 
60
65
  def get_data_source(type, name)
61
66
  @data_sources.get(type, name)
@@ -63,7 +68,7 @@ module Bricolage
63
68
 
64
69
  def subsystem(id)
65
70
  self.class.new(@filesystem.subsystem(id), @environment,
66
- global_variables: @opt_global_variables,
71
+ option_variables: @option_variables,
67
72
  data_sources: @data_sources,
68
73
  logger: @logger)
69
74
  end
@@ -102,7 +107,6 @@ module Bricolage
102
107
  Variables.union(
103
108
  builtin_variables,
104
109
  load_global_variables,
105
- @opt_global_variables
106
110
  )
107
111
  end
108
112
 
@@ -130,8 +134,11 @@ module Bricolage
130
134
 
131
135
  def load_variables(path)
132
136
  Variables.define {|vars|
133
- @filesystem.config_file_loader.load_yaml(path).each do |name, value|
134
- vars[name] = value
137
+ kvs = @filesystem.config_file_loader.load_yaml(path)
138
+ if kvs
139
+ kvs.each do |name, value|
140
+ vars[name] = value
141
+ end
135
142
  end
136
143
  }
137
144
  end
@@ -0,0 +1,184 @@
1
+ require 'bricolage/exception'
2
+
3
+ module Bricolage
4
+ module DAO
5
+
6
+ class Job
7
+ include SQLUtils
8
+
9
+ Attributes = Struct.new(:id, :subsystem, :job_name, :jobnet_id, :executor_id, keyword_init: true)
10
+
11
+ def Job.for_record(r)
12
+ Attributes.new(
13
+ id: r['job_id']&.to_i,
14
+ subsystem: r['subsystem'],
15
+ job_name: r['job_name'],
16
+ jobnet_id: r['jobnet_id']&.to_i,
17
+ executor_id: r['executor_id']
18
+ )
19
+ end
20
+
21
+ def initialize(datasource)
22
+ @datasource = datasource
23
+ end
24
+
25
+ private def connect(&block)
26
+ @datasource.open_shared_connection(&block)
27
+ end
28
+
29
+ def find_or_create(jobnet_id, job_ref)
30
+ connect {|conn|
31
+ job = find(conn, jobnet_id, job_ref) # optimize the most frequent case
32
+ if job
33
+ job
34
+ else
35
+ begin
36
+ create(conn, jobnet_id, job_ref)
37
+ rescue UniqueViolationException
38
+ find(conn, jobnet_id, job_ref) or raise "[BUG] Could not find/create job record: jobnet_id=#{jobnet_id}, ref=#{job_ref}"
39
+ end
40
+ end
41
+ }
42
+ end
43
+
44
+ private def find(conn, jobnet_id, job_ref)
45
+ record = conn.query_row(<<~EndSQL)
46
+ select
47
+ "job_id"
48
+ , "subsystem"
49
+ , "job_name"
50
+ , "executor_id"
51
+ , jobnet_id
52
+ from
53
+ jobs
54
+ where
55
+ jobnet_id = #{jobnet_id}
56
+ and "subsystem" = #{s job_ref.subsystem}
57
+ and "job_name" = #{s job_ref.name}
58
+ ;
59
+ EndSQL
60
+
61
+ if record
62
+ Job.for_record(record)
63
+ else
64
+ nil
65
+ end
66
+ end
67
+
68
+ private def create(conn, jobnet_id, job_ref)
69
+ record = conn.query_row(<<~EndSQL)
70
+ insert into jobs
71
+ ( "subsystem"
72
+ , "job_name"
73
+ , jobnet_id
74
+ )
75
+ values
76
+ ( #{s job_ref.subsystem}
77
+ , #{s job_ref.name}
78
+ , #{jobnet_id}
79
+ )
80
+ returning "job_id", "subsystem", "job_name", jobnet_id
81
+ ;
82
+ EndSQL
83
+
84
+ Job.for_record(record)
85
+ end
86
+
87
+ def locked?(job_ids)
88
+ count = connect {|conn|
89
+ conn.query_value(<<~EndSQL)
90
+ select
91
+ count(job_id)
92
+ from
93
+ jobs
94
+ where
95
+ job_id in (#{job_ids.join(',')})
96
+ and executor_id is not null
97
+ ;
98
+ EndSQL
99
+ }
100
+
101
+ count.to_i > 0
102
+ end
103
+
104
+ def locked_jobs(jobnet_id)
105
+ records = connect {|conn|
106
+ conn.query_rows(<<~EndSQL)
107
+ select
108
+ "job_id"
109
+ , "subsystem"
110
+ , "job_name"
111
+ , jobnet_id
112
+ , "executor_id"
113
+ from
114
+ jobs
115
+ where
116
+ jobnet_id = #{jobnet_id}
117
+ and executor_id is not null
118
+ ;
119
+ EndSQL
120
+ }
121
+
122
+ if records.empty?
123
+ []
124
+ else
125
+ record.map {|r| Job.for_record(r) }
126
+ end
127
+ end
128
+
129
+ def lock(job_id, executor_id)
130
+ records = connect {|conn|
131
+ conn.execute_update(<<~EndSQL)
132
+ update jobs
133
+ set
134
+ executor_id = #{s executor_id}
135
+ where
136
+ job_id = #{job_id}
137
+ and executor_id is null
138
+ returning job_id
139
+ ;
140
+ EndSQL
141
+ }
142
+
143
+ if records.empty?
144
+ raise DoubleLockError, "Could not lock job: job_id=#{job_id}"
145
+ end
146
+ end
147
+
148
+ # Unlock the job.
149
+ # Returns true if successfully unlocked, otherwise false.
150
+ # FIXME: raise an exception on failing unlock?
151
+ def unlock(job_id, executor_id)
152
+ records = connect {|conn|
153
+ conn.execute_update(<<~EndSQL)
154
+ update jobs
155
+ set
156
+ executor_id = null
157
+ where
158
+ job_id = #{job_id}
159
+ and executor_id = #{s executor_id}
160
+ returning job_id
161
+ ;
162
+ EndSQL
163
+ }
164
+
165
+ not records.empty?
166
+ end
167
+
168
+ def clear_lock_all(jobnet_id)
169
+ connect {|conn|
170
+ conn.execute_update(<<~EndSQL)
171
+ update jobs
172
+ set
173
+ executor_id = null
174
+ where
175
+ jobnet_id = #{jobnet_id}
176
+ ;
177
+ EndSQL
178
+ }
179
+ end
180
+
181
+ end # class Job
182
+
183
+ end
184
+ end
@@ -0,0 +1,253 @@
1
+ module Bricolage
2
+ module DAO
3
+
4
+ class JobExecution
5
+ include SQLUtils
6
+
7
+ STATUS_WAITING = 'waiting'.freeze
8
+ STATUS_SUCCEEDED = 'succeeded'.freeze
9
+ STATUS_RUNNING = 'running'.freeze
10
+ STATUS_FAILED = 'failed'.freeze
11
+ STATUS_CANCELED = 'canceled'.freeze
12
+
13
+ Attributes = Struct.new(:job_id, :job_execution_id, :subsystem, :job_name, keyword_init: true)
14
+
15
+ def JobExecution.for_record(r)
16
+ Attributes.new(
17
+ job_id: r['job_id']&.to_i,
18
+ job_execution_id: r['job_execution_id']&.to_i,
19
+ subsystem: r['subsystem'],
20
+ job_name: r['job_name']
21
+ )
22
+ end
23
+
24
+ def JobExecution.for_connection(conn)
25
+ new(nil, connection: conn)
26
+ end
27
+
28
+ def initialize(datasource, connection: nil)
29
+ @datasource = datasource
30
+ @connection = connection
31
+ end
32
+
33
+ private def connect
34
+ if @connection
35
+ yield @connection
36
+ else
37
+ @datasource.open_shared_connection {|conn|
38
+ yield conn
39
+ }
40
+ end
41
+ end
42
+
43
+ def enqueued_jobs(jobnet_ref)
44
+ records = connect {|conn|
45
+ conn.query_rows(<<~EndSQL)
46
+ select
47
+ e.job_execution_id
48
+ , e.job_id
49
+ , j.subsystem
50
+ , j.job_name
51
+ from
52
+ job_executions e
53
+ inner join jobs j using (job_id)
54
+ inner join jobnets n using (jobnet_id)
55
+ where
56
+ n.subsystem = #{s jobnet_ref.subsystem}
57
+ and n.jobnet_name = #{s jobnet_ref.name}
58
+ and e.status in (#{s STATUS_WAITING}, #{s STATUS_RUNNING}, #{s STATUS_FAILED})
59
+ order by
60
+ e.execution_sequence
61
+ ;
62
+ EndSQL
63
+ }
64
+ records.map {|r| JobExecution.for_record(r) }
65
+ end
66
+
67
+ def enqueue_job(job, execution_sequence)
68
+ record = nil
69
+ connect {|conn|
70
+ records = conn.execute_update(<<~EndSQL)
71
+ insert into job_executions
72
+ ( job_id
73
+ , execution_sequence
74
+ , status
75
+ , message
76
+ , submitted_at
77
+ )
78
+ values
79
+ ( #{job.id}
80
+ , #{execution_sequence}
81
+ , #{s STATUS_WAITING}
82
+ , ''
83
+ , now()
84
+ )
85
+ returning job_execution_id, job_id
86
+ ;
87
+ EndSQL
88
+
89
+ record = records.first
90
+ save_state_transition(conn, record['job_execution_id'], 'submitted_at')
91
+ }
92
+
93
+ exec = JobExecution.for_record(record)
94
+ exec.subsystem = job.subsystem
95
+ exec.job_name = job.job_name
96
+ exec
97
+ end
98
+
99
+ def cancel_jobnet(jobnet_ref, message)
100
+ connect {|conn|
101
+ records = conn.execute_update(<<~EndSQL)
102
+ update job_executions
103
+ set
104
+ status = #{s STATUS_CANCELED}
105
+ , message = #{s message}
106
+ , finished_at = now()
107
+ where
108
+ job_id in (
109
+ select
110
+ j.job_id
111
+ from
112
+ jobs j inner join jobnets n using (jobnet_id)
113
+ where
114
+ n.subsystem = #{s jobnet_ref.subsystem}
115
+ and n.jobnet_name = #{s jobnet_ref.name}
116
+ )
117
+ and status in (#{s STATUS_WAITING}, #{s STATUS_RUNNING}, #{s STATUS_FAILED})
118
+ returning job_execution_id
119
+ ;
120
+ EndSQL
121
+
122
+ job_execution_ids = records.map {|r| r['job_execution_id'].to_i }
123
+ unless job_execution_ids.empty?
124
+ conn.execute_update(<<~EndSQL)
125
+ insert into job_execution_states
126
+ ( job_execution_id
127
+ , job_id
128
+ , created_at
129
+ , status
130
+ , message
131
+ )
132
+ select
133
+ job_execution_id
134
+ , job_id
135
+ , finished_at
136
+ , status
137
+ , message
138
+ from
139
+ job_executions
140
+ where
141
+ job_execution_id in (#{job_execution_ids.join(', ')})
142
+ ;
143
+ EndSQL
144
+ end
145
+ }
146
+ end
147
+
148
+ def transition_to_running(job_execution_id)
149
+ connect {|conn|
150
+ records = conn.execute_update(<<~EndSQL)
151
+ update job_executions
152
+ set
153
+ status = #{s STATUS_RUNNING}
154
+ , message = ''
155
+ , started_at = now()
156
+ , finished_at = null
157
+ where
158
+ job_execution_id = #{job_execution_id}
159
+ and status in (#{s STATUS_WAITING}, #{s STATUS_FAILED})
160
+ returning job_execution_id
161
+ ;
162
+ EndSQL
163
+ if records.empty?
164
+ raise IllegalJobStateException, "Could not run already running job: job_execution_id=#{job_execution_id}"
165
+ end
166
+
167
+ save_state_transition(conn, job_execution_id, 'started_at')
168
+ }
169
+ end
170
+
171
+ def transition_to_succeeded(job_execution_id)
172
+ connect {|conn|
173
+ records = conn.execute_update(<<~EndSQL)
174
+ update job_executions
175
+ set
176
+ finished_at = now()
177
+ , status = #{s STATUS_SUCCEEDED}
178
+ , message = ''
179
+ where
180
+ job_execution_id = #{job_execution_id}
181
+ and status = #{s STATUS_RUNNING}
182
+ returning job_execution_id
183
+ ;
184
+ EndSQL
185
+ if records.empty?
186
+ raise IllegalJobStateException, "could not transition to succeeded state: job_execution_id=#{job_execution_id}"
187
+ end
188
+
189
+ save_state_transition(conn, job_execution_id, 'finished_at')
190
+ }
191
+ end
192
+
193
+ def transition_to_failed(job_execution_id, message)
194
+ connect {|conn|
195
+ records = conn.execute_update(<<~EndSQL)
196
+ update job_executions
197
+ set
198
+ finished_at = now()
199
+ , status = #{s STATUS_FAILED}
200
+ , message = #{s message}
201
+ where
202
+ job_execution_id = #{job_execution_id}
203
+ and status = #{s STATUS_RUNNING}
204
+ returning job_execution_id
205
+ ;
206
+ EndSQL
207
+ if records.empty?
208
+ raise IllegalJobStateException, "could not transition to failed state: job_execution_id=#{job_execution_id}"
209
+ end
210
+
211
+ save_state_transition(conn, job_execution_id, 'finished_at')
212
+ }
213
+ end
214
+
215
+ private def save_state_transition(conn, job_execution_id, time_expr)
216
+ conn.execute_update(<<~EndSQL)
217
+ insert into job_execution_states
218
+ ( job_execution_id
219
+ , job_id
220
+ , created_at
221
+ , status
222
+ , message
223
+ )
224
+ select
225
+ job_execution_id
226
+ , job_id
227
+ , #{time_expr}
228
+ , status
229
+ , message
230
+ from
231
+ job_executions
232
+ where
233
+ job_execution_id = #{job_execution_id}
234
+ ;
235
+ EndSQL
236
+ end
237
+
238
+ # For tests only
239
+ def delete_all
240
+ connect {|conn|
241
+ conn.execute_update(<<~EndSQL)
242
+ delete from job_execution_states;
243
+ delete from job_executions;
244
+ delete from jobs;
245
+ delete from jobnets;
246
+ EndSQL
247
+ }
248
+ end
249
+
250
+ end # class JobExecution
251
+
252
+ end
253
+ end