bricolage 5.30.0 → 6.0.0beta1

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.
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