canvas_sync 0.16.2 → 0.17.0.beta3
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 +4 -4
- data/README.md +49 -137
- data/app/models/canvas_sync/sync_batch.rb +5 -0
- data/db/migrate/20170915210836_create_canvas_sync_job_log.rb +12 -31
- data/db/migrate/20180725155729_add_job_id_to_canvas_sync_job_logs.rb +4 -13
- data/db/migrate/20190916154829_add_fork_count_to_canvas_sync_job_logs.rb +3 -11
- data/db/migrate/20201018210836_create_canvas_sync_sync_batches.rb +11 -0
- data/lib/canvas_sync.rb +36 -118
- data/lib/canvas_sync/concerns/api_syncable.rb +27 -0
- data/lib/canvas_sync/job.rb +5 -5
- data/lib/canvas_sync/job_batches/batch.rb +399 -0
- data/lib/canvas_sync/job_batches/batch_aware_job.rb +62 -0
- data/lib/canvas_sync/job_batches/callback.rb +153 -0
- data/lib/canvas_sync/job_batches/chain_builder.rb +210 -0
- data/lib/canvas_sync/job_batches/context_hash.rb +147 -0
- data/lib/canvas_sync/job_batches/jobs/base_job.rb +7 -0
- data/lib/canvas_sync/job_batches/jobs/concurrent_batch_job.rb +18 -0
- data/lib/canvas_sync/job_batches/jobs/serial_batch_job.rb +73 -0
- data/lib/canvas_sync/job_batches/sidekiq.rb +93 -0
- data/lib/canvas_sync/job_batches/status.rb +63 -0
- data/lib/canvas_sync/jobs/begin_sync_chain_job.rb +34 -0
- data/lib/canvas_sync/jobs/report_checker.rb +3 -6
- data/lib/canvas_sync/jobs/report_processor_job.rb +2 -5
- data/lib/canvas_sync/jobs/report_starter.rb +27 -19
- data/lib/canvas_sync/jobs/sync_accounts_job.rb +3 -5
- data/lib/canvas_sync/jobs/sync_admins_job.rb +2 -4
- data/lib/canvas_sync/jobs/sync_assignment_groups_job.rb +2 -4
- data/lib/canvas_sync/jobs/sync_assignments_job.rb +2 -4
- data/lib/canvas_sync/jobs/sync_context_module_items_job.rb +2 -4
- data/lib/canvas_sync/jobs/sync_context_modules_job.rb +2 -4
- data/lib/canvas_sync/jobs/sync_provisioning_report_job.rb +5 -35
- data/lib/canvas_sync/jobs/sync_roles_job.rb +2 -5
- data/lib/canvas_sync/jobs/sync_simple_table_job.rb +11 -32
- data/lib/canvas_sync/jobs/sync_submissions_job.rb +2 -4
- data/lib/canvas_sync/jobs/sync_terms_job.rb +25 -8
- data/lib/canvas_sync/misc_helper.rb +15 -0
- data/lib/canvas_sync/version.rb +1 -1
- data/spec/canvas_sync/canvas_sync_spec.rb +136 -153
- data/spec/canvas_sync/jobs/job_spec.rb +9 -17
- data/spec/canvas_sync/jobs/report_checker_spec.rb +1 -3
- data/spec/canvas_sync/jobs/report_processor_job_spec.rb +0 -3
- data/spec/canvas_sync/jobs/report_starter_spec.rb +19 -28
- data/spec/canvas_sync/jobs/sync_admins_job_spec.rb +1 -4
- data/spec/canvas_sync/jobs/sync_assignment_groups_job_spec.rb +2 -1
- data/spec/canvas_sync/jobs/sync_assignments_job_spec.rb +3 -2
- data/spec/canvas_sync/jobs/sync_context_module_items_job_spec.rb +3 -2
- data/spec/canvas_sync/jobs/sync_context_modules_job_spec.rb +3 -2
- data/spec/canvas_sync/jobs/sync_provisioning_report_job_spec.rb +3 -35
- data/spec/canvas_sync/jobs/sync_roles_job_spec.rb +1 -4
- data/spec/canvas_sync/jobs/sync_simple_table_job_spec.rb +5 -12
- data/spec/canvas_sync/jobs/sync_submissions_job_spec.rb +2 -1
- data/spec/canvas_sync/jobs/sync_terms_job_spec.rb +1 -4
- data/spec/dummy/app/models/account.rb +3 -0
- data/spec/dummy/app/models/pseudonym.rb +14 -0
- data/spec/dummy/app/models/submission.rb +1 -0
- data/spec/dummy/app/models/user.rb +1 -0
- data/spec/dummy/config/environments/test.rb +2 -0
- data/spec/dummy/db/migrate/20201016181346_create_pseudonyms.rb +24 -0
- data/spec/dummy/db/schema.rb +24 -4
- data/spec/job_batching/batch_aware_job_spec.rb +100 -0
- data/spec/job_batching/batch_spec.rb +363 -0
- data/spec/job_batching/callback_spec.rb +38 -0
- data/spec/job_batching/flow_spec.rb +91 -0
- data/spec/job_batching/integration/integration.rb +57 -0
- data/spec/job_batching/integration/nested.rb +88 -0
- data/spec/job_batching/integration/simple.rb +47 -0
- data/spec/job_batching/integration/workflow.rb +134 -0
- data/spec/job_batching/integration_helper.rb +48 -0
- data/spec/job_batching/sidekiq_spec.rb +124 -0
- data/spec/job_batching/status_spec.rb +92 -0
- data/spec/job_batching/support/base_job.rb +14 -0
- data/spec/job_batching/support/sample_callback.rb +2 -0
- data/spec/spec_helper.rb +17 -0
- metadata +90 -8
- data/lib/canvas_sync/job_chain.rb +0 -57
- data/lib/canvas_sync/jobs/fork_gather.rb +0 -59
- data/spec/canvas_sync/jobs/fork_gather_spec.rb +0 -73
@@ -0,0 +1,62 @@
|
|
1
|
+
module CanvasSync
|
2
|
+
module JobBatches
|
3
|
+
module BatchAwareJob
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
around_perform do |job, block|
|
8
|
+
if (@bid) # This _must_ be @bid - not just bid
|
9
|
+
prev_batch = Thread.current[:batch]
|
10
|
+
begin
|
11
|
+
Thread.current[:batch] = Batch.new(@bid)
|
12
|
+
block.call
|
13
|
+
batch&.save_context_changes
|
14
|
+
Batch.process_successful_job(@bid, job_id)
|
15
|
+
rescue
|
16
|
+
Batch.process_failed_job(@bid, job_id)
|
17
|
+
raise
|
18
|
+
ensure
|
19
|
+
Thread.current[:batch] = prev_batch
|
20
|
+
end
|
21
|
+
else
|
22
|
+
block.call
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
around_enqueue do |job, block|
|
27
|
+
if (batch = Thread.current[:batch])
|
28
|
+
batch.increment_job_queue(job_id) if (@bid = batch.bid)
|
29
|
+
end
|
30
|
+
block.call
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def bid
|
35
|
+
@bid || Thread.current[:batch]&.bid
|
36
|
+
end
|
37
|
+
|
38
|
+
def batch
|
39
|
+
Thread.current[:batch]
|
40
|
+
end
|
41
|
+
|
42
|
+
def batch_context
|
43
|
+
batch&.context || {}
|
44
|
+
end
|
45
|
+
|
46
|
+
def valid_within_batch?
|
47
|
+
batch.valid?
|
48
|
+
end
|
49
|
+
|
50
|
+
def serialize
|
51
|
+
super.tap do |data|
|
52
|
+
data['batch_id'] = @bid # This _must_ be @bid - not just bid
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def deserialize(data)
|
57
|
+
super
|
58
|
+
@bid = data['batch_id']
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
module CanvasSync
|
2
|
+
module JobBatches
|
3
|
+
class Batch
|
4
|
+
module Callback
|
5
|
+
|
6
|
+
VALID_CALLBACKS = %w[success complete dead].freeze
|
7
|
+
|
8
|
+
module CallbackWorkerCommon
|
9
|
+
def perform(definition, event, opts, bid, parent_bid)
|
10
|
+
return unless VALID_CALLBACKS.include?(event)
|
11
|
+
|
12
|
+
method = nil
|
13
|
+
target = :instance
|
14
|
+
clazz = definition
|
15
|
+
if clazz.is_a?(String)
|
16
|
+
if clazz.include?('#')
|
17
|
+
clazz, method = clazz.split("#")
|
18
|
+
elsif clazz.include?('.')
|
19
|
+
clazz, method = clazz.split(".")
|
20
|
+
target = :class
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
method ||= "on_#{event}"
|
25
|
+
status = Batch::Status.new(bid)
|
26
|
+
|
27
|
+
if clazz && object = Object.const_get(clazz)
|
28
|
+
target = target == :instance ? object.new : object
|
29
|
+
if target.respond_to?(method)
|
30
|
+
target.send(method, status, opts)
|
31
|
+
else
|
32
|
+
Batch.logger.warn("Invalid callback method #{definition} - #{target.to_s} does not respond to #{method}")
|
33
|
+
end
|
34
|
+
else
|
35
|
+
Batch.logger.warn("Invalid callback method #{definition} - Class #{clazz} not found")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class ActiveJobCallbackWorker < ActiveJob::Base
|
41
|
+
include CallbackWorkerCommon
|
42
|
+
|
43
|
+
def self.enqueue_all(args, queue)
|
44
|
+
args.each do |arg_set|
|
45
|
+
set(queue: queue).perform_later(*arg_set)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
if defined?(::Sidekiq)
|
51
|
+
class SidekiqCallbackWorker
|
52
|
+
include ::Sidekiq::Worker
|
53
|
+
include CallbackWorkerCommon
|
54
|
+
|
55
|
+
def self.enqueue_all(args, queue)
|
56
|
+
return if args.empty?
|
57
|
+
|
58
|
+
::Sidekiq::Client.push_bulk(
|
59
|
+
'class' => self,
|
60
|
+
'args' => args,
|
61
|
+
'queue' => queue
|
62
|
+
)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
Worker = SidekiqCallbackWorker
|
66
|
+
else
|
67
|
+
Worker = ActiveJobCallbackWorker
|
68
|
+
end
|
69
|
+
|
70
|
+
class Finalize
|
71
|
+
def dispatch status, opts
|
72
|
+
bid = opts["bid"]
|
73
|
+
callback_bid = status.bid
|
74
|
+
event = opts["event"].to_sym
|
75
|
+
callback_batch = bid != callback_bid
|
76
|
+
|
77
|
+
Batch.logger.debug {"Finalize #{event} batch id: #{opts["bid"]}, callback batch id: #{callback_bid} callback_batch #{callback_batch}"}
|
78
|
+
|
79
|
+
batch_status = Status.new bid
|
80
|
+
send(event, bid, batch_status, batch_status.parent_bid)
|
81
|
+
|
82
|
+
# Different events are run in different callback batches
|
83
|
+
Batch.cleanup_redis callback_bid if callback_batch
|
84
|
+
Batch.cleanup_redis bid if event == :success
|
85
|
+
end
|
86
|
+
|
87
|
+
def success(bid, status, parent_bid)
|
88
|
+
return unless parent_bid
|
89
|
+
|
90
|
+
_, _, success, _, complete, pending, children, failure = Batch.redis do |r|
|
91
|
+
r.multi do
|
92
|
+
r.sadd("BID-#{parent_bid}-success", bid)
|
93
|
+
r.expire("BID-#{parent_bid}-success", Batch::BID_EXPIRE_TTL)
|
94
|
+
r.scard("BID-#{parent_bid}-success")
|
95
|
+
r.sadd("BID-#{parent_bid}-complete", bid)
|
96
|
+
r.scard("BID-#{parent_bid}-complete")
|
97
|
+
r.hincrby("BID-#{parent_bid}", "pending", 0)
|
98
|
+
r.hincrby("BID-#{parent_bid}", "children", 0)
|
99
|
+
r.scard("BID-#{parent_bid}-failed")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
# if job finished successfully and parent batch completed call parent complete callback
|
103
|
+
# Success callback is called after complete callback
|
104
|
+
if complete == children && pending == failure
|
105
|
+
Batch.logger.debug {"Finalize parent complete bid: #{parent_bid}"}
|
106
|
+
Batch.enqueue_callbacks(:complete, parent_bid)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def complete(bid, status, parent_bid)
|
111
|
+
pending, children, success = Batch.redis do |r|
|
112
|
+
r.multi do
|
113
|
+
r.hincrby("BID-#{bid}", "pending", 0)
|
114
|
+
r.hincrby("BID-#{bid}", "children", 0)
|
115
|
+
r.scard("BID-#{bid}-success")
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# if we batch was successful run success callback
|
120
|
+
if pending.to_i.zero? && children == success
|
121
|
+
Batch.enqueue_callbacks(:success, bid)
|
122
|
+
|
123
|
+
elsif parent_bid
|
124
|
+
# if batch was not successfull check and see if its parent is complete
|
125
|
+
# if the parent is complete we trigger the complete callback
|
126
|
+
# We don't want to run this if the batch was successfull because the success
|
127
|
+
# callback may add more jobs to the parent batch
|
128
|
+
|
129
|
+
Batch.logger.debug {"Finalize parent complete bid: #{parent_bid}"}
|
130
|
+
_, complete, pending, children, failure = Batch.redis do |r|
|
131
|
+
r.multi do
|
132
|
+
r.sadd("BID-#{parent_bid}-complete", bid)
|
133
|
+
r.scard("BID-#{parent_bid}-complete")
|
134
|
+
r.hincrby("BID-#{parent_bid}", "pending", 0)
|
135
|
+
r.hincrby("BID-#{parent_bid}", "children", 0)
|
136
|
+
r.scard("BID-#{parent_bid}-failed")
|
137
|
+
end
|
138
|
+
end
|
139
|
+
if complete == children && pending == failure
|
140
|
+
Batch.enqueue_callbacks(:complete, parent_bid)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def cleanup_redis bid, callback_bid=nil
|
146
|
+
Batch.cleanup_redis bid
|
147
|
+
Batch.cleanup_redis callback_bid if callback_bid
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
module CanvasSync
|
2
|
+
module JobBatches
|
3
|
+
class ChainBuilder
|
4
|
+
VALID_PLACEMENT_PARAMETERS = %i[before after with].freeze
|
5
|
+
|
6
|
+
attr_reader :base_job
|
7
|
+
|
8
|
+
def initialize(base_type = SerialBatchJob)
|
9
|
+
if base_type.is_a?(Hash)
|
10
|
+
@base_job = base_type
|
11
|
+
else
|
12
|
+
@base_job = {
|
13
|
+
job: base_type,
|
14
|
+
parameters: [],
|
15
|
+
}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def process!
|
20
|
+
normalize!
|
21
|
+
self.class.enqueue_job(base_job)
|
22
|
+
end
|
23
|
+
|
24
|
+
def [](key)
|
25
|
+
if key.is_a?(Class)
|
26
|
+
get_sub_chain(key)
|
27
|
+
else
|
28
|
+
@base_job[key]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def params
|
33
|
+
ParamsMapper.new(self[:parameters])
|
34
|
+
end
|
35
|
+
|
36
|
+
def <<(new_job)
|
37
|
+
insert_at(-1, new_job)
|
38
|
+
end
|
39
|
+
|
40
|
+
def insert_at(position, new_jobs)
|
41
|
+
chain = self.class.get_chain_parameter(base_job)
|
42
|
+
new_jobs = [new_jobs] unless new_jobs.is_a?(Array)
|
43
|
+
chain.insert(-1, *new_jobs)
|
44
|
+
end
|
45
|
+
|
46
|
+
def insert(new_jobs, **kwargs)
|
47
|
+
invalid_params = kwargs.keys - VALID_PLACEMENT_PARAMETERS
|
48
|
+
raise "Invalid placement parameters: #{invalid_params.map(&:to_s).join(', ')}" if invalid_params.present?
|
49
|
+
raise "At most one placement parameter may be provided" if kwargs.values.compact.length > 1
|
50
|
+
|
51
|
+
new_jobs = [new_jobs] unless new_jobs.is_a?(Array)
|
52
|
+
|
53
|
+
if !kwargs.present?
|
54
|
+
insert_at(-1, new_jobs)
|
55
|
+
else
|
56
|
+
placement = kwargs.keys[0]
|
57
|
+
relative_to = kwargs.values[0]
|
58
|
+
|
59
|
+
matching_jobs = find_matching_jobs(relative_to)
|
60
|
+
raise "Could not find a \"#{relative_to}\" job in the chain" if matching_jobs.count == 0
|
61
|
+
raise "Found multiple \"#{relative_to}\" jobs in the chain" if matching_jobs.count > 1
|
62
|
+
|
63
|
+
parent_job, sub_index = matching_jobs[0]
|
64
|
+
chain = self.class.get_chain_parameter(parent_job)
|
65
|
+
needed_parent_type = placement == :with ? ConcurrentBatchJob : SerialBatchJob
|
66
|
+
|
67
|
+
if parent_job[:job] != needed_parent_type
|
68
|
+
old_job = chain[sub_index]
|
69
|
+
parent_job = chain[sub_index] = {
|
70
|
+
job: needed_parent_type,
|
71
|
+
parameters: [],
|
72
|
+
}
|
73
|
+
sub_index = 0
|
74
|
+
chain = self.class.get_chain_parameter(parent_job)
|
75
|
+
chain << old_job
|
76
|
+
end
|
77
|
+
|
78
|
+
if placement == :with
|
79
|
+
chain.insert(-1, *new_jobs)
|
80
|
+
else
|
81
|
+
sub_index += 1 if placement == :after
|
82
|
+
chain.insert(sub_index, *new_jobs)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def get_sub_chain(sub_type)
|
88
|
+
matching_jobs = find_matching_jobs(sub_type)
|
89
|
+
raise "Found multiple \"#{sub_type}\" jobs in the chain" if matching_jobs.count > 1
|
90
|
+
return nil if matching_jobs.count == 0
|
91
|
+
|
92
|
+
new(matching_jobs[0])
|
93
|
+
end
|
94
|
+
|
95
|
+
def normalize!(job_def = self.base_job)
|
96
|
+
if job_def.is_a?(ChainBuilder)
|
97
|
+
job_def.normalize!
|
98
|
+
else
|
99
|
+
job_def[:job] = job_def[:job].to_s
|
100
|
+
if (chain = self.class.get_chain_parameter(job_def, raise_error: false)).present?
|
101
|
+
chain.map! { |sub_job| normalize!(sub_job) }
|
102
|
+
end
|
103
|
+
job_def
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def find_matching_jobs(search_job, parent_job = self.base_job)
|
110
|
+
return to_enum(:find_matching_jobs, search_job, parent_job) unless block_given?
|
111
|
+
|
112
|
+
sub_jobs = self.class.get_chain_parameter(parent_job)
|
113
|
+
sub_jobs.each_with_index do |sub_job, i|
|
114
|
+
if sub_job[:job].to_s == search_job.to_s
|
115
|
+
yield [parent_job, i]
|
116
|
+
elsif self.class._job_type_definitions[sub_job[:job]]
|
117
|
+
find_matching_jobs(search_job) { |item| yield item }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class << self
|
123
|
+
def _job_type_definitions
|
124
|
+
@job_type_definitions ||= {}
|
125
|
+
end
|
126
|
+
|
127
|
+
def register_chain_job(job_class, chain_parameter, **options)
|
128
|
+
_job_type_definitions[job_class.to_s] = {
|
129
|
+
**options,
|
130
|
+
chain_parameter: chain_parameter,
|
131
|
+
}
|
132
|
+
end
|
133
|
+
|
134
|
+
def get_chain_parameter(job_def, raise_error: true)
|
135
|
+
unless _job_type_definitions[job_def[:job].to_s].present?
|
136
|
+
raise "Job Type #{base_job[:job].to_s} does not accept a sub-chain" if raise_error
|
137
|
+
return nil
|
138
|
+
end
|
139
|
+
|
140
|
+
key = _job_type_definitions[job_def[:job].to_s][:chain_parameter]
|
141
|
+
mapper = ParamsMapper.new(job_def[:parameters])
|
142
|
+
mapper[key] ||= []
|
143
|
+
end
|
144
|
+
|
145
|
+
def enqueue_job(job_def)
|
146
|
+
job_class = job_def[:job].constantize
|
147
|
+
job_options = job_def[:parameters] || []
|
148
|
+
|
149
|
+
# Legacy Support
|
150
|
+
if job_def[:options]
|
151
|
+
job_options << {} unless job_options[-1].is_a?(Hash)
|
152
|
+
job_options[-1].merge!(job_def[:options])
|
153
|
+
end
|
154
|
+
|
155
|
+
if job_class.respond_to? :perform_async
|
156
|
+
job_class.perform_async(*job_options)
|
157
|
+
else
|
158
|
+
job_class.perform_later(*job_options)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
ChainBuilder.register_chain_job(ConcurrentBatchJob, 0)
|
165
|
+
ChainBuilder.register_chain_job(SerialBatchJob, 0)
|
166
|
+
|
167
|
+
class ParamsMapper
|
168
|
+
def initialize(backend)
|
169
|
+
@backend = backend
|
170
|
+
end
|
171
|
+
|
172
|
+
def [](key)
|
173
|
+
get_parameter(key)
|
174
|
+
end
|
175
|
+
|
176
|
+
def []=(key, value)
|
177
|
+
set_parameter(key, value)
|
178
|
+
end
|
179
|
+
|
180
|
+
def to_a
|
181
|
+
@backend
|
182
|
+
end
|
183
|
+
|
184
|
+
private
|
185
|
+
|
186
|
+
def get_parameter(key)
|
187
|
+
if key.is_a?(Numeric)
|
188
|
+
@backend[key]
|
189
|
+
else
|
190
|
+
kwargs = @backend.last
|
191
|
+
return nil unless kwargs.is_a?(Hash)
|
192
|
+
kwargs[key]
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def set_parameter(key, value)
|
197
|
+
if key.is_a?(Numeric)
|
198
|
+
@backend[key] = value
|
199
|
+
else
|
200
|
+
kwargs = @backend.last
|
201
|
+
unless kwargs.is_a?(Hash)
|
202
|
+
kwargs = {}
|
203
|
+
@backend.push(kwargs)
|
204
|
+
end
|
205
|
+
kwargs[key] = value
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
module CanvasSync
|
2
|
+
module JobBatches
|
3
|
+
class ContextHash
|
4
|
+
delegate_missing_to :flatten
|
5
|
+
|
6
|
+
def initialize(bid, hash = nil)
|
7
|
+
@bid_stack = [bid]
|
8
|
+
@hash_map = {}
|
9
|
+
@dirty = false
|
10
|
+
@flattened = nil
|
11
|
+
@hash_map[bid] = hash.with_indifferent_access if hash
|
12
|
+
end
|
13
|
+
|
14
|
+
# Local is "the nearest batch with a context value"
|
15
|
+
# This allows for, for example, SerialBatchJob to have a modifiable context stored on it's main Batch
|
16
|
+
# that can be accessed transparently from one of it's internal, context-less Batches
|
17
|
+
def local_bid
|
18
|
+
bid = @bid_stack[-1]
|
19
|
+
while bid.present?
|
20
|
+
bhash = reolve_hash(bid)
|
21
|
+
return bid if bhash
|
22
|
+
bid = get_parent_bid(bid)
|
23
|
+
end
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def local
|
28
|
+
@hash_map[local_bid]
|
29
|
+
end
|
30
|
+
|
31
|
+
def set_local(new_hash)
|
32
|
+
@dirty = true
|
33
|
+
local.clear.merge!(new_hash)
|
34
|
+
end
|
35
|
+
|
36
|
+
def clear
|
37
|
+
local.clear
|
38
|
+
@flattened = nil
|
39
|
+
@dirty = true
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def []=(key, value)
|
44
|
+
@flattened = nil
|
45
|
+
@dirty = true
|
46
|
+
local[key] = value
|
47
|
+
end
|
48
|
+
|
49
|
+
def [](key)
|
50
|
+
bid = @bid_stack[-1]
|
51
|
+
while bid.present?
|
52
|
+
bhash = reolve_hash(bid)
|
53
|
+
return bhash[key] if bhash&.key?(key)
|
54
|
+
bid = get_parent_bid(bid)
|
55
|
+
end
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def reload!
|
60
|
+
@dirty = false
|
61
|
+
@hash_map = {}
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
def save!(force: false)
|
66
|
+
return unless dirty? || force
|
67
|
+
Batch.redis do |r|
|
68
|
+
r.hset("BID-#{local_bid}", 'context', JSON.unparse(local))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def dirty?
|
73
|
+
@dirty
|
74
|
+
end
|
75
|
+
|
76
|
+
def is_a?(arg)
|
77
|
+
return true if Hash <= arg
|
78
|
+
super
|
79
|
+
end
|
80
|
+
|
81
|
+
def flatten
|
82
|
+
return @flattened if @flattened
|
83
|
+
|
84
|
+
load_all
|
85
|
+
flattened = {}
|
86
|
+
@bid_stack.compact.each do |bid|
|
87
|
+
flattened.merge!(@hash_map[bid]) if @hash_map[bid]
|
88
|
+
end
|
89
|
+
flattened.freeze
|
90
|
+
|
91
|
+
@flattened = flattened.with_indifferent_access
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def get_parent_hash(bid)
|
97
|
+
reolve_hash(get_parent_bid(bid)).freeze
|
98
|
+
end
|
99
|
+
|
100
|
+
def get_parent_bid(bid)
|
101
|
+
index = @bid_stack.index(bid)
|
102
|
+
raise "Invalid BID #{bid}" if index.nil? # Sanity Check - this shouldn't happen
|
103
|
+
|
104
|
+
index -= 1
|
105
|
+
if index >= 0
|
106
|
+
@bid_stack[index]
|
107
|
+
else
|
108
|
+
pbid = Batch.redis { |r| r.hget("BID-#{bid}", "parent_bid") }
|
109
|
+
@bid_stack.unshift(pbid)
|
110
|
+
pbid
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def reolve_hash(bid)
|
115
|
+
return nil unless bid.present?
|
116
|
+
return @hash_map[bid] if @hash_map.key?(bid)
|
117
|
+
|
118
|
+
context_json, editable = Batch.redis do |r|
|
119
|
+
r.multi do
|
120
|
+
r.hget("BID-#{bid}", "context")
|
121
|
+
r.hget("BID-#{bid}", "allow_context_changes")
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
if context_json.present?
|
126
|
+
context_hash = JSON.parse(context_json)
|
127
|
+
context_hash = context_hash.with_indifferent_access
|
128
|
+
context_hash.each do |k, v|
|
129
|
+
v.freeze
|
130
|
+
end
|
131
|
+
context_hash.freeze unless editable
|
132
|
+
|
133
|
+
@hash_map[bid] = context_hash
|
134
|
+
else
|
135
|
+
@hash_map[bid] = nil
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def load_all
|
140
|
+
while @bid_stack[0].present?
|
141
|
+
get_parent_hash(@bid_stack[0])
|
142
|
+
end
|
143
|
+
@hash_map
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|