canvas_sync 0.23.4 → 0.24.0.beta1

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 (206) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +2 -4
  3. data/lib/canvas_sync/processors/provisioning_report_processor.rb +1 -0
  4. data/lib/canvas_sync/version.rb +1 -1
  5. data/lib/canvas_sync.rb +6 -4
  6. data/spec/canvas_sync/canvas_sync_spec.rb +11 -11
  7. data/spec/canvas_sync/live_events/process_event_job_spec.rb +1 -0
  8. data/spec/{dummy → internal}/app/models/application_record.rb +1 -0
  9. data/spec/{dummy → internal}/app/models/content_migration.rb +0 -0
  10. data/spec/{dummy → internal}/app/models/course.rb +0 -1
  11. data/spec/{dummy → internal}/app/models/course_nickname.rb +0 -0
  12. data/spec/{dummy → internal}/app/models/learning_outcome.rb +0 -0
  13. data/spec/{dummy → internal}/app/services/live_events/assignment_event.rb +0 -0
  14. data/spec/{dummy → internal}/app/services/live_events/course_event.rb +0 -0
  15. data/spec/{dummy → internal}/app/services/live_events/course_section_event.rb +0 -0
  16. data/spec/{dummy → internal}/app/services/live_events/enrollment_event.rb +0 -0
  17. data/spec/{dummy → internal}/app/services/live_events/grade_event.rb +0 -0
  18. data/spec/{dummy → internal}/app/services/live_events/module_event.rb +0 -0
  19. data/spec/{dummy → internal}/app/services/live_events/module_item_event.rb +0 -0
  20. data/spec/{dummy → internal}/app/services/live_events/submission_event.rb +0 -0
  21. data/spec/{dummy → internal}/app/services/live_events/syllabus_event.rb +0 -0
  22. data/spec/{dummy → internal}/app/services/live_events/user_event.rb +0 -0
  23. data/spec/internal/bin/rails +9 -0
  24. data/spec/internal/config/database.yml +5 -0
  25. data/spec/internal/config/routes.rb +5 -0
  26. data/spec/internal/config/storage.yml +3 -0
  27. data/spec/{dummy/db/migrate/20220926221926_create_users.rb → internal/db/migrate/20250912205136_create_users.rb} +0 -0
  28. data/spec/{dummy/db/migrate/20201016181346_create_pseudonyms.rb → internal/db/migrate/20250912205137_create_pseudonyms.rb} +0 -0
  29. data/spec/{dummy/db/migrate/20200415171620_create_groups.rb → internal/db/migrate/20250912205139_create_groups.rb} +0 -0
  30. data/spec/{dummy/db/migrate/20200416214248_create_group_memberships.rb → internal/db/migrate/20250912205140_create_group_memberships.rb} +0 -0
  31. data/spec/{dummy/db/migrate/20190702203622_create_accounts.rb → internal/db/migrate/20250912205141_create_accounts.rb} +0 -0
  32. data/spec/{dummy/db/migrate/20190702203623_create_terms.rb → internal/db/migrate/20250912205142_create_terms.rb} +0 -0
  33. data/spec/{dummy/db/migrate/20190702203625_create_sections.rb → internal/db/migrate/20250912205144_create_sections.rb} +0 -0
  34. data/spec/{dummy/db/migrate/20190702203626_create_assignments.rb → internal/db/migrate/20250912205145_create_assignments.rb} +0 -0
  35. data/spec/{dummy/db/migrate/20190702203627_create_submissions.rb → internal/db/migrate/20250912205146_create_submissions.rb} +0 -0
  36. data/spec/{dummy/db/migrate/20190927204545_create_roles.rb → internal/db/migrate/20250912205147_create_roles.rb} +2 -2
  37. data/spec/{dummy/db/migrate/20190927204546_create_admins.rb → internal/db/migrate/20250912205148_create_admins.rb} +0 -0
  38. data/spec/{dummy/db/migrate/20190702203630_create_assignment_groups.rb → internal/db/migrate/20250912205149_create_assignment_groups.rb} +0 -0
  39. data/spec/{dummy/db/migrate/20190702203631_create_context_modules.rb → internal/db/migrate/20250912205150_create_context_modules.rb} +0 -0
  40. data/spec/{dummy/db/migrate/20190702203632_create_context_module_items.rb → internal/db/migrate/20250912205151_create_context_module_items.rb} +0 -0
  41. data/spec/{dummy/db/migrate/20210907233329_create_user_observers.rb → internal/db/migrate/20250912205152_create_user_observers.rb} +0 -0
  42. data/spec/{dummy/db/migrate/20210907233330_create_grading_periods.rb → internal/db/migrate/20250912205153_create_grading_periods.rb} +0 -0
  43. data/spec/{dummy/db/migrate/20211001184920_create_grading_period_groups.rb → internal/db/migrate/20250912205154_create_grading_period_groups.rb} +0 -0
  44. data/spec/{dummy/db/migrate/20220308072643_create_content_migrations.rb → internal/db/migrate/20250912205155_create_content_migrations.rb} +0 -0
  45. data/spec/{dummy/db/migrate/20220712210559_create_learning_outcomes.rb → internal/db/migrate/20250912205156_create_learning_outcomes.rb} +0 -0
  46. data/spec/{dummy/db/migrate/20240523101010_create_learning_outcome_results.rb → internal/db/migrate/20250912205157_create_learning_outcome_results.rb} +0 -0
  47. data/spec/{dummy/db/migrate/20240510094100_create_rubric_associations.rb → internal/db/migrate/20250912205160_create_rubric_associations.rb} +0 -0
  48. data/spec/{dummy/db/migrate/20240510101100_create_rubric_assessments.rb → internal/db/migrate/20250912205161_create_rubric_assessments.rb} +0 -0
  49. data/spec/{dummy/db/migrate/20240828161300_create_course_progresses.rb → internal/db/migrate/20250912205162_create_course_progresses.rb} +0 -0
  50. data/spec/internal/db/schema.rb +6 -0
  51. data/spec/spec_helper.rb +8 -4
  52. metadata +182 -372
  53. data/lib/canvas_sync/job_batches/batch.rb +0 -595
  54. data/lib/canvas_sync/job_batches/callback.rb +0 -135
  55. data/lib/canvas_sync/job_batches/chain_builder.rb +0 -247
  56. data/lib/canvas_sync/job_batches/compat/active_job.rb +0 -108
  57. data/lib/canvas_sync/job_batches/compat/sidekiq/web/batches_assets/css/styles.less +0 -182
  58. data/lib/canvas_sync/job_batches/compat/sidekiq/web/batches_assets/js/batch_tree.js +0 -108
  59. data/lib/canvas_sync/job_batches/compat/sidekiq/web/batches_assets/js/util.js +0 -2
  60. data/lib/canvas_sync/job_batches/compat/sidekiq/web/helpers.rb +0 -41
  61. data/lib/canvas_sync/job_batches/compat/sidekiq/web/views/_batch_tree.erb +0 -6
  62. data/lib/canvas_sync/job_batches/compat/sidekiq/web/views/_batches_table.erb +0 -44
  63. data/lib/canvas_sync/job_batches/compat/sidekiq/web/views/_common.erb +0 -13
  64. data/lib/canvas_sync/job_batches/compat/sidekiq/web/views/_jobs_table.erb +0 -21
  65. data/lib/canvas_sync/job_batches/compat/sidekiq/web/views/_pagination.erb +0 -26
  66. data/lib/canvas_sync/job_batches/compat/sidekiq/web/views/batch.erb +0 -81
  67. data/lib/canvas_sync/job_batches/compat/sidekiq/web/views/batches.erb +0 -23
  68. data/lib/canvas_sync/job_batches/compat/sidekiq/web/views/pool.erb +0 -137
  69. data/lib/canvas_sync/job_batches/compat/sidekiq/web/views/pools.erb +0 -47
  70. data/lib/canvas_sync/job_batches/compat/sidekiq/web.rb +0 -218
  71. data/lib/canvas_sync/job_batches/compat/sidekiq.rb +0 -149
  72. data/lib/canvas_sync/job_batches/compat.rb +0 -20
  73. data/lib/canvas_sync/job_batches/context_hash.rb +0 -157
  74. data/lib/canvas_sync/job_batches/hier_batch_ids.lua +0 -25
  75. data/lib/canvas_sync/job_batches/jobs/base_job.rb +0 -5
  76. data/lib/canvas_sync/job_batches/jobs/concurrent_batch_job.rb +0 -20
  77. data/lib/canvas_sync/job_batches/jobs/managed_batch_job.rb +0 -175
  78. data/lib/canvas_sync/job_batches/jobs/serial_batch_job.rb +0 -20
  79. data/lib/canvas_sync/job_batches/pool.rb +0 -254
  80. data/lib/canvas_sync/job_batches/pool_refill.lua +0 -47
  81. data/lib/canvas_sync/job_batches/redis_model.rb +0 -67
  82. data/lib/canvas_sync/job_batches/redis_script.rb +0 -161
  83. data/lib/canvas_sync/job_batches/schedule_callback.lua +0 -14
  84. data/lib/canvas_sync/job_batches/status.rb +0 -89
  85. data/lib/canvas_sync/job_uniqueness/compat/active_job.rb +0 -75
  86. data/lib/canvas_sync/job_uniqueness/compat/sidekiq.rb +0 -135
  87. data/lib/canvas_sync/job_uniqueness/compat.rb +0 -20
  88. data/lib/canvas_sync/job_uniqueness/configuration.rb +0 -25
  89. data/lib/canvas_sync/job_uniqueness/job_uniqueness.rb +0 -47
  90. data/lib/canvas_sync/job_uniqueness/lock_context.rb +0 -199
  91. data/lib/canvas_sync/job_uniqueness/locksmith.rb +0 -92
  92. data/lib/canvas_sync/job_uniqueness/on_conflict/base.rb +0 -32
  93. data/lib/canvas_sync/job_uniqueness/on_conflict/log.rb +0 -13
  94. data/lib/canvas_sync/job_uniqueness/on_conflict/null_strategy.rb +0 -9
  95. data/lib/canvas_sync/job_uniqueness/on_conflict/raise.rb +0 -11
  96. data/lib/canvas_sync/job_uniqueness/on_conflict/reject.rb +0 -21
  97. data/lib/canvas_sync/job_uniqueness/on_conflict/reschedule.rb +0 -20
  98. data/lib/canvas_sync/job_uniqueness/on_conflict.rb +0 -62
  99. data/lib/canvas_sync/job_uniqueness/strategy/base.rb +0 -107
  100. data/lib/canvas_sync/job_uniqueness/strategy/until_and_while_executing.rb +0 -35
  101. data/lib/canvas_sync/job_uniqueness/strategy/until_executed.rb +0 -20
  102. data/lib/canvas_sync/job_uniqueness/strategy/until_executing.rb +0 -20
  103. data/lib/canvas_sync/job_uniqueness/strategy/until_expired.rb +0 -16
  104. data/lib/canvas_sync/job_uniqueness/strategy/while_executing.rb +0 -26
  105. data/lib/canvas_sync/job_uniqueness/strategy.rb +0 -27
  106. data/lib/canvas_sync/job_uniqueness/unique_job_common.rb +0 -79
  107. data/spec/dummy/README.rdoc +0 -1
  108. data/spec/dummy/Rakefile +0 -6
  109. data/spec/dummy/app/services/live_events/assignment_created_event.rb +0 -12
  110. data/spec/dummy/app/services/live_events/assignment_updated_event.rb +0 -12
  111. data/spec/dummy/app/services/live_events/base_event.rb +0 -52
  112. data/spec/dummy/app/services/live_events/course_created_event.rb +0 -12
  113. data/spec/dummy/app/services/live_events/course_section_created_event.rb +0 -12
  114. data/spec/dummy/app/services/live_events/course_section_updated_event.rb +0 -12
  115. data/spec/dummy/app/services/live_events/course_updated_event.rb +0 -12
  116. data/spec/dummy/app/services/live_events/enrollment_created_event.rb +0 -12
  117. data/spec/dummy/app/services/live_events/enrollment_updated_event.rb +0 -12
  118. data/spec/dummy/app/services/live_events/grade_changed_event.rb +0 -12
  119. data/spec/dummy/app/services/live_events/module_created_event.rb +0 -12
  120. data/spec/dummy/app/services/live_events/module_item_created_event.rb +0 -12
  121. data/spec/dummy/app/services/live_events/module_item_updated_event.rb +0 -12
  122. data/spec/dummy/app/services/live_events/module_updated_event.rb +0 -12
  123. data/spec/dummy/app/services/live_events/submission_created_event.rb +0 -12
  124. data/spec/dummy/app/services/live_events/submission_updated_event.rb +0 -12
  125. data/spec/dummy/app/services/live_events/syllabus_updated_event.rb +0 -12
  126. data/spec/dummy/app/services/live_events/user_created_event.rb +0 -12
  127. data/spec/dummy/app/services/live_events/user_updated_event.rb +0 -12
  128. data/spec/dummy/bin/rails +0 -4
  129. data/spec/dummy/config/application.rb +0 -39
  130. data/spec/dummy/config/boot.rb +0 -5
  131. data/spec/dummy/config/database.yml +0 -25
  132. data/spec/dummy/config/environment.rb +0 -5
  133. data/spec/dummy/config/environments/development.rb +0 -41
  134. data/spec/dummy/config/environments/test.rb +0 -44
  135. data/spec/dummy/config/initializers/assets.rb +0 -11
  136. data/spec/dummy/config/initializers/session_store.rb +0 -3
  137. data/spec/dummy/config/initializers/wrap_parameters.rb +0 -14
  138. data/spec/dummy/config/routes.rb +0 -3
  139. data/spec/dummy/config/secrets.yml +0 -22
  140. data/spec/dummy/config.ru +0 -4
  141. data/spec/dummy/db/schema.rb +0 -563
  142. data/spec/job_batching/batch_spec.rb +0 -531
  143. data/spec/job_batching/callback_spec.rb +0 -38
  144. data/spec/job_batching/compat/active_job_spec.rb +0 -107
  145. data/spec/job_batching/compat/sidekiq_spec.rb +0 -127
  146. data/spec/job_batching/context_hash_spec.rb +0 -54
  147. data/spec/job_batching/flow_spec.rb +0 -82
  148. data/spec/job_batching/integration/fail_then_succeed.rb +0 -42
  149. data/spec/job_batching/integration/integration.rb +0 -57
  150. data/spec/job_batching/integration/nested.rb +0 -88
  151. data/spec/job_batching/integration/simple.rb +0 -47
  152. data/spec/job_batching/integration/workflow.rb +0 -134
  153. data/spec/job_batching/integration_helper.rb +0 -50
  154. data/spec/job_batching/pool_spec.rb +0 -161
  155. data/spec/job_batching/status_spec.rb +0 -76
  156. data/spec/job_batching/support/base_job.rb +0 -14
  157. data/spec/job_batching/support/sample_callback.rb +0 -2
  158. data/spec/job_uniqueness/compat/active_job_spec.rb +0 -49
  159. data/spec/job_uniqueness/compat/sidekiq_spec.rb +0 -68
  160. data/spec/job_uniqueness/lock_context_spec.rb +0 -106
  161. data/spec/job_uniqueness/on_conflict/log_spec.rb +0 -11
  162. data/spec/job_uniqueness/on_conflict/raise_spec.rb +0 -10
  163. data/spec/job_uniqueness/on_conflict/reschedule_spec.rb +0 -63
  164. data/spec/job_uniqueness/on_conflict_spec.rb +0 -16
  165. data/spec/job_uniqueness/spec_helper.rb +0 -17
  166. data/spec/job_uniqueness/strategy/base_spec.rb +0 -100
  167. data/spec/job_uniqueness/strategy/until_and_while_executing_spec.rb +0 -48
  168. data/spec/job_uniqueness/strategy/until_executed_spec.rb +0 -23
  169. data/spec/job_uniqueness/strategy/until_executing_spec.rb +0 -23
  170. data/spec/job_uniqueness/strategy/until_expired_spec.rb +0 -23
  171. data/spec/job_uniqueness/strategy/while_executing_spec.rb +0 -33
  172. data/spec/job_uniqueness/support/lock_strategy.rb +0 -28
  173. data/spec/job_uniqueness/support/on_conflict.rb +0 -24
  174. data/spec/job_uniqueness/support/test_worker.rb +0 -19
  175. data/spec/job_uniqueness/unique_job_common_spec.rb +0 -45
  176. /data/spec/{dummy → internal}/app/models/account.rb +0 -0
  177. /data/spec/{dummy → internal}/app/models/admin.rb +0 -0
  178. /data/spec/{dummy → internal}/app/models/assignment.rb +0 -0
  179. /data/spec/{dummy → internal}/app/models/assignment_group.rb +0 -0
  180. /data/spec/{dummy → internal}/app/models/assignment_override.rb +0 -0
  181. /data/spec/{dummy → internal}/app/models/context_module.rb +0 -0
  182. /data/spec/{dummy → internal}/app/models/context_module_item.rb +0 -0
  183. /data/spec/{dummy → internal}/app/models/course_progress.rb +0 -0
  184. /data/spec/{dummy → internal}/app/models/enrollment.rb +0 -0
  185. /data/spec/{dummy → internal}/app/models/grading_period.rb +0 -0
  186. /data/spec/{dummy → internal}/app/models/grading_period_group.rb +0 -0
  187. /data/spec/{dummy → internal}/app/models/group.rb +0 -0
  188. /data/spec/{dummy → internal}/app/models/group_membership.rb +0 -0
  189. /data/spec/{dummy → internal}/app/models/learning_outcome_result.rb +0 -0
  190. /data/spec/{dummy → internal}/app/models/pseudonym.rb +0 -0
  191. /data/spec/{dummy → internal}/app/models/role.rb +0 -0
  192. /data/spec/{dummy → internal}/app/models/rubric.rb +0 -0
  193. /data/spec/{dummy → internal}/app/models/rubric_assessment.rb +0 -0
  194. /data/spec/{dummy → internal}/app/models/rubric_association.rb +0 -0
  195. /data/spec/{dummy → internal}/app/models/score.rb +0 -0
  196. /data/spec/{dummy → internal}/app/models/section.rb +0 -0
  197. /data/spec/{dummy → internal}/app/models/submission.rb +0 -0
  198. /data/spec/{dummy → internal}/app/models/term.rb +0 -0
  199. /data/spec/{dummy → internal}/app/models/user.rb +0 -0
  200. /data/spec/{dummy → internal}/app/models/user_observer.rb +0 -0
  201. /data/spec/{dummy/db/migrate/20190702203621_create_courses.rb → internal/db/migrate/20250912205138_create_courses.rb} +0 -0
  202. /data/spec/{dummy/db/migrate/20190702203624_create_enrollments.rb → internal/db/migrate/20250912205143_create_enrollments.rb} +0 -0
  203. /data/spec/{dummy/db/migrate/20250319194134_create_course_nicknames.rb → internal/db/migrate/20250912205158_create_course_nicknames.rb} +0 -0
  204. /data/spec/{dummy/db/migrate/20250319194135_create_rubrics.rb → internal/db/migrate/20250912205159_create_rubrics.rb} +0 -0
  205. /data/spec/{dummy/db/migrate/20241223080202_create_assignment_overrides.rb → internal/db/migrate/20250912205163_create_assignment_overrides.rb} +0 -0
  206. /data/spec/{dummy/db/migrate/20250626194330_create_scores.rb → internal/db/migrate/20250912205164_create_scores.rb} +0 -0
@@ -1,595 +0,0 @@
1
-
2
- require_relative './redis_model'
3
- require_relative './redis_script'
4
- require_relative "./callback"
5
- require_relative "./context_hash"
6
- require_relative "./status"
7
- require_relative "./pool"
8
- Dir[File.dirname(__FILE__) + "/jobs/*.rb"].each { |file| require file }
9
- require_relative "./chain_builder"
10
-
11
- # Implement Job Batching similar to Sidekiq::Batch. Supports ActiveJob and Sidekiq, or a mix thereof.
12
- # Much of this code is modifed/extended from https://github.com/breamware/sidekiq-batch
13
-
14
- module CanvasSync::JobBatches
15
- CURRENT_BATCH_THREAD_KEY = :job_batches_batch
16
-
17
- class Batch
18
- include RedisModel
19
-
20
- class NoBlockGivenError < StandardError; end
21
-
22
- delegate :redis, to: :class
23
-
24
- BID_EXPIRE_TTL = 90.days.to_i
25
- INDEX_ALL_BATCHES = false
26
- SCHEDULE_CALLBACK = RedisScript.new(Pathname.new(__FILE__) + "../schedule_callback.lua")
27
- BID_HIERARCHY = RedisScript.new(Pathname.new(__FILE__) + "../hier_batch_ids.lua")
28
-
29
- attr_reader :bid
30
-
31
- def self.current
32
- Thread.current[CURRENT_BATCH_THREAD_KEY]
33
- end
34
-
35
- def self.current_context
36
- self.current&.context
37
- end
38
-
39
- def initialize(existing_bid = nil)
40
- @bid = existing_bid || SecureRandom.urlsafe_base64(10)
41
- @existing = !(!existing_bid || existing_bid.empty?) # Basically existing_bid.present?
42
- @initialized = false
43
- @bidkey = "BID-" + @bid.to_s
44
- self.created_at = Time.now.utc.to_f unless @existing
45
- end
46
-
47
- redis_attr :description
48
- redis_attr :created_at
49
- redis_attr :callback_queue, read_only: false
50
- redis_attr :callback_params, :json
51
- redis_attr :allow_context_changes, :bool
52
-
53
- def context
54
- return @context if defined?(@context)
55
-
56
- if (@initialized || @existing)
57
- @context = ContextHash.new(bid)
58
- else
59
- @context = ContextHash.new(bid, {})
60
- end
61
- end
62
-
63
- def context=(value)
64
- raise "context is read-only once the batch has been started" if (@initialized || @existing) # && !allow_context_changes
65
- raise "context must be a Hash" unless value.is_a?(Hash) || value.nil?
66
- return nil if value.nil? && @context.nil?
67
-
68
- value = {} if value.nil?
69
- value = value.local if value.is_a?(ContextHash)
70
-
71
- @context ||= ContextHash.new(bid, {})
72
- @context.set_local(value)
73
- # persist_bid_attr('context', JSON.unparse(@context.local))
74
- end
75
-
76
- def save_context_changes
77
- @context&.save!
78
- end
79
-
80
- # Events:
81
- # :complete - triggered once all jobs have been executed, regardless of success or failure.
82
- # :success - triggered once all jobs haves successfully executed.
83
- # :death - triggered after any job enters the dead state.
84
- # :stagnated - triggered when a job dies and no other jobs/batches are active.
85
- def on(event, callback, options = {})
86
- return unless Callback::VALID_CALLBACKS.include?(event.to_sym)
87
-
88
- callback_key = "#{@bidkey}-callbacks-#{event}"
89
- redis.multi do |r|
90
- r.sadd(callback_key, JSON.unparse({
91
- callback: callback,
92
- opts: options
93
- }))
94
- r.expire(callback_key, BID_EXPIRE_TTL)
95
- end
96
- end
97
-
98
- def jobs
99
- raise NoBlockGivenError unless block_given?
100
-
101
- persist!
102
-
103
- # TODO This keep_open! block is probably desired, but it has some caveats:
104
- # - Old logic didn't auto-clean empty batches
105
- # - Could be an issue if the Thread crashes at a bad moment (do we even need to plan for this?)
106
- # - Technically there could be a race condition without it, but such hasn't been observed
107
-
108
- # keep_open! do
109
- begin
110
- parent = Thread.current[CURRENT_BATCH_THREAD_KEY]
111
- Thread.current[CURRENT_BATCH_THREAD_KEY] = self
112
- yield
113
- ensure
114
- Thread.current[CURRENT_BATCH_THREAD_KEY] = parent
115
- end
116
- # end
117
-
118
- nil
119
- end
120
-
121
- # Mark this Batch as a placeholder. It will be persisted to Redis, but no jobs will be added.
122
- # From here, you can either use `.jobs` on the batch to add jobs as usual, or call `.let_close!` to allow cleanup to occur.
123
- def placeholder!
124
- # TODO Provide a stable `let_close!` token?
125
- persist!
126
- end
127
-
128
- def invalidate_all
129
- redis.setex("invalidated-bid-#{bid}", BID_EXPIRE_TTL, 1)
130
- end
131
-
132
- def parent_bid
133
- redis.hget(@bidkey, "parent_bid")
134
- end
135
-
136
- def parent
137
- if parent_bid
138
- Batch.new(parent_bid)
139
- end
140
- end
141
-
142
- def valid?(batch = self)
143
- valid = !redis.exists?("invalidated-bid-#{batch.bid}")
144
- batch.parent ? valid && valid?(batch.parent) : valid
145
- end
146
-
147
- def keep_open!(token = SecureRandom.urlsafe_base64(10))
148
- if block_given?
149
- begin
150
- token = keep_open!(token)
151
- yield
152
- ensure
153
- let_close!(token)
154
- end
155
- else
156
- persist!
157
-
158
- redis.multi do |r|
159
- r.sadd("#{@bidkey}-holds", token)
160
- r.expire("#{@bidkey}-holds", BID_EXPIRE_TTL)
161
- end
162
-
163
- assert_batch_is_open
164
-
165
- token
166
- end
167
- end
168
-
169
- def let_close!(token = :unset)
170
- self.class.with_callback_check(bid, only: %i[complete success]) do |r|
171
- if token == :unset # Legacy
172
- r.del("#{@bidkey}-holds")
173
- r.hset(@bidkey, 'keep_open', "false")
174
- else
175
- r.srem("#{@bidkey}-holds", token)
176
- end
177
- end
178
- end
179
-
180
- def self.with_batch(batch)
181
- batch = self.new(batch) if batch.is_a?(String)
182
- parent = Thread.current[CURRENT_BATCH_THREAD_KEY]
183
- Thread.current[CURRENT_BATCH_THREAD_KEY] = batch
184
- yield
185
- ensure
186
- Thread.current[CURRENT_BATCH_THREAD_KEY] = parent
187
- end
188
-
189
- # Any Batches or Jobs created in the given block won't be assocaiated to the current batch
190
- def self.without_batch(&blk)
191
- with_batch(nil, &blk)
192
- end
193
-
194
- def append_jobs(jids)
195
- jids = Array(jids)
196
- jids = jids.uniq
197
- return unless jids.size > 0
198
-
199
- redis do |r|
200
- tme = Time.now.utc.to_f
201
- added = r.zadd(@bidkey + "-jids", jids.map{|jid| [tme, jid] }, nx: true)
202
- r.multi do |r|
203
- r.hincrby(@bidkey, "pending", added)
204
- r.hincrby(@bidkey, "job_count", added)
205
- r.expire(@bidkey, BID_EXPIRE_TTL)
206
- r.expire(@bidkey + "-jids", BID_EXPIRE_TTL)
207
- end
208
- end
209
- end
210
-
211
- protected
212
-
213
- def redis_key
214
- @bidkey
215
- end
216
-
217
- def flush_pending_attrs
218
- super
219
- redis.zadd("batches", created_at, bid) if INDEX_ALL_BATCHES
220
- end
221
-
222
- def assert_batch_is_open
223
- unless defined?(@closed)
224
- @closed = redis.hget(@bidkey, 'complete') == 'true'
225
- end
226
- raise "Cannot add jobs to Batch #{} bid - it has already entered the callback-stage" if @closed
227
- end
228
-
229
- def persist!
230
- if !@existing && !@initialized
231
- parent_bid = Thread.current[CURRENT_BATCH_THREAD_KEY]&.bid
232
-
233
- redis.multi do |r|
234
- r.hset(@bidkey, "parent_bid", parent_bid.to_s) if parent_bid
235
- r.expire(@bidkey, BID_EXPIRE_TTL)
236
-
237
- if parent_bid
238
- r.hincrby("BID-#{parent_bid}", "children", 1)
239
- r.expire("BID-#{parent_bid}", BID_EXPIRE_TTL)
240
- r.zadd("BID-#{parent_bid}-bids", created_at, bid)
241
- else
242
- r.zadd("BID-ROOT-bids", created_at, bid)
243
- end
244
- end
245
-
246
- flush_pending_attrs
247
- @context&.save!
248
-
249
- @initialized = true
250
- end
251
- end
252
-
253
- class << self
254
- def current
255
- Thread.current[CURRENT_BATCH_THREAD_KEY]
256
- end
257
-
258
- def current_context
259
- current&.context || {}
260
- end
261
-
262
- # Perform a success/failure/complete/etc transaction against Redis, checking
263
- # if any callbacks should be triggered
264
- def with_callback_check(bid, except: [], only: Callback::VALID_CALLBACKS, &blk)
265
- except = Array(except).map(&:to_sym)
266
- only = Array(only).map(&:to_sym)
267
-
268
- precount = 0
269
-
270
- all_results = redis do |r|
271
- return unless r.exists?("BID-#{bid}")
272
-
273
- r.multi do |r|
274
- # The block probably doesn't _have_ to be part of the same transaction, but it was easier than
275
- # re-assessing possible race-conditions and is technically more performant
276
- blk.call(r)
277
- futures = r.instance_variable_get(:@futures) || r.instance_variable_get(:@pipeline)&.futures || r.instance_variable_get(:@client)&.futures
278
- precount = futures.size
279
-
280
- # Misc
281
- r.hget("BID-#{bid}", "parent_bid")
282
- r.hget("BID-#{bid}", "keep_open")
283
- r.scard("BID-#{bid}-holds")
284
-
285
- # Jobs
286
- r.hincrby("BID-#{bid}", "pending", 0)
287
- r.scard("BID-#{bid}-failed")
288
- r.scard("BID-#{bid}-dead")
289
-
290
- # Batches
291
- r.hincrby("BID-#{bid}", "children", 0)
292
- r.scard("BID-#{bid}-batches-complete")
293
- r.scard("BID-#{bid}-batches-success")
294
- r.scard("BID-#{bid}-batches-failed")
295
- r.scard("BID-#{bid}-batches-stagnated")
296
-
297
- # Touch Expirations
298
- r.expire("BID-#{bid}", BID_EXPIRE_TTL)
299
- r.expire("BID-#{bid}-batches-success", BID_EXPIRE_TTL)
300
- end
301
- end
302
-
303
- # Exclude return values from the passed block
304
- actual_results = all_results[precount..-1]
305
-
306
- # "pending" = not successful (yet)
307
- # "failed" = dead or retrying
308
- # "complete" = successful or failed
309
-
310
- parent_bid, keep_open, holds, \
311
- pending_jobs, failed_jobs, dead_jobs, \
312
- child_batches, complete_batches, success_batches, failed_batches, stagnated_batches \
313
- = actual_results
314
-
315
- pending_batches = child_batches - success_batches
316
-
317
- if keep_open == 'true' || (holds && holds > 0)
318
- except << :complete
319
- except << :success
320
- end
321
-
322
- trigger_callback = ->(callback) {
323
- next if except.include?(callback.to_sym)
324
- next unless only.include?(callback.to_sym)
325
-
326
- Batch.logger.debug {"Finalize #{callback} bid: #{parent_bid}"}
327
- enqueue_callbacks(callback, bid)
328
- }
329
-
330
- # Handling all of these cases in one method may be a little less performant than in more specialized methods, but
331
- # I was dealing with duplicate Redis queries and checks being made in up to 4 places and it was getting out of
332
- # hand - this combined method should be easier to maintain and reason about
333
-
334
- all_successful = pending_jobs.zero? && child_batches == success_batches
335
-
336
- if all_successful || (pending_jobs == failed_jobs && child_batches == complete_batches) # All Complete
337
- trigger_callback.call(:complete)
338
- end
339
-
340
- if all_successful # All Successful
341
- trigger_callback.call(:success)
342
- elsif pending_jobs == dead_jobs && pending_batches == stagnated_batches # Stagnated
343
- trigger_callback.call(:stagnated)
344
- end
345
-
346
- all_results[0...precount]
347
- end
348
-
349
- def process_failed_job(bid, jid)
350
- with_callback_check(bid, except: [:success]) do |r|
351
- r.sadd("BID-#{bid}-failed", jid)
352
- r.expire("BID-#{bid}-failed", BID_EXPIRE_TTL)
353
- end
354
- end
355
-
356
- # Dead jobs are a Sidekiq feature.
357
- # If this is called for a job, process_failed_job was also called
358
- def process_dead_job(bid, jid)
359
- enqueue_callbacks(:death, bid)
360
-
361
- with_callback_check(bid) do |r|
362
- r.sadd("BID-#{bid}-dead", jid)
363
- r.expire("BID-#{bid}-dead", BID_EXPIRE_TTL)
364
- end
365
- end
366
-
367
- def process_successful_job(bid, jid)
368
- with_callback_check(bid) do |r|
369
- r.srem("BID-#{bid}-failed", jid)
370
- r.hincrby("BID-#{bid}", "pending", -1)
371
- r.hincrby("BID-#{bid}", "successful-jobs", 1)
372
- r.zrem("BID-#{bid}-jids", jid)
373
- end
374
- end
375
-
376
- def enqueue_callbacks(event, bid)
377
- batch_key = "BID-#{bid}"
378
- callback_key = "#{batch_key}-callbacks-#{event}"
379
-
380
- exists, callbacks, queue, parent_bid, callback_params = redis do |r|
381
- r.multi do |r|
382
- r.exists?(batch_key)
383
-
384
- r.smembers(callback_key)
385
- r.hget(batch_key, "callback_queue")
386
- r.hget(batch_key, "parent_bid")
387
- r.hget(batch_key, "callback_params")
388
- end
389
- end
390
-
391
- return unless exists
392
-
393
- queue ||= "default"
394
- parent_bid = !parent_bid || parent_bid.empty? ? nil : parent_bid # Basically parent_bid.blank?
395
-
396
- # Internal callback params. If this is present, we're trying to enqueue callbacks for a callback, which is a special case that
397
- # indicates that the callback completed and we need to close the triggering batch (which is in a done-but-not-cleaned state)
398
- callback_params = JSON.parse(callback_params) if callback_params.present?
399
-
400
- # User-configured parameters/arguments to pass to the callback
401
- callback_args = callbacks.reduce([]) do |memo, jcb|
402
- cb = JSON.load(jcb)
403
- memo << [cb['callback'], event.to_s, cb['opts'], bid, parent_bid]
404
- end
405
-
406
- opts = {"bid" => bid, "event" => event}
407
- should_schedule_batch = callback_args.present? && !callback_params.present?
408
- already_processed = redis do |r|
409
- SCHEDULE_CALLBACK.call(r, [batch_key], [event.to_s, should_schedule_batch.to_s, BID_EXPIRE_TTL])
410
- end
411
-
412
- return if already_processed == 'true'
413
-
414
- if should_schedule_batch
415
- logger.debug {"Enqueue callback bid: #{bid} event: #{event} args: #{callback_args.inspect}"}
416
-
417
- # Create a new Batch to handle the callbacks and add it to the _parent_ batch
418
- # (this ensures that the parent's lifecycle status can't change until the child's callbacks are done)
419
- with_batch(parent_bid) do
420
- cb_batch = self.new
421
- cb_batch.callback_params = {
422
- for_bid: bid,
423
- event: event,
424
- }
425
- opts['callback_bid'] = cb_batch.bid
426
-
427
- logger.debug {"Adding callback batch: #{cb_batch.bid} for batch: #{bid}"}
428
- cb_batch.jobs do
429
- push_callbacks(callback_args, queue)
430
- end
431
- end
432
- end
433
-
434
- if callback_params.present?
435
- # This is a callback for a callback. Passing `origin` to the Finalizer allows it to also cleanup the original/callback-triggering batch
436
- opts['origin'] = callback_params
437
- end
438
-
439
- # The Finalizer marks this batch as complete, bumps any necessary counters, cleans up this Batch _if_ no callbacks were scheduled,
440
- # and enqueues parent-Batch callbacks if needed.
441
- logger.debug {"Run batch finalizer bid: #{bid} event: #{event} args: #{callback_args.inspect}"}
442
- finalizer = Batch::Callback::Finalize.new
443
- status = Status.new bid
444
- finalizer.dispatch(status, opts)
445
- end
446
-
447
- def cleanup_redis(bid)
448
- logger.debug {"Cleaning redis of batch #{bid}"}
449
- redis do |r|
450
- r.zrem("batches", bid)
451
- r.zrem("BID-ROOT-bids", bid)
452
- r.unlink(
453
- "BID-#{bid}",
454
- "BID-#{bid}-callbacks-complete",
455
- "BID-#{bid}-callbacks-success",
456
- "BID-#{bid}-failed",
457
- "BID-#{bid}-dead",
458
-
459
- "BID-#{bid}-batches-success",
460
- "BID-#{bid}-batches-complete",
461
- "BID-#{bid}-batches-failed",
462
- "BID-#{bid}-bids",
463
- "BID-#{bid}-jids",
464
- "BID-#{bid}-pending_callbacks",
465
- )
466
- end
467
- end
468
-
469
- def delete_prematurely!(bid)
470
- child_bids = redis do |r|
471
- r.zrange("BID-#{bid}-bids", 0, -1)
472
- end
473
- child_bids.each do |cbid|
474
- delete_prematurely!(cbid)
475
- end
476
- cleanup_redis(bid)
477
- end
478
-
479
- # Internal method to cleanup a Redis Hash and related keys
480
- def cleanup_redis_index_for(key, suffixes = [""])
481
- redis do |r|
482
- if r.hget(k, "created_at").present?
483
- r.multi do |r|
484
- suffixes.each do |suffix|
485
- r.expire(key + suffix, BID_EXPIRE_TTL)
486
- end
487
- end
488
- false
489
- else
490
- r.multi do |r|
491
- suffixes.each do |suffix|
492
- r.unlink(key + suffix)
493
- end
494
- end
495
- true
496
- end
497
- end
498
- end
499
-
500
- # Administrative/console method to cleanup expired batches from the WebUI
501
- def cleanup_redis_index!
502
- suffixes = ["", "-callbacks-complete", "-callbacks-success", "-failed", "-dead", "-batches-success", "-batches-complete", "-batches-failed", "-bids", "-jids", "-pending_callbacks"]
503
-
504
- redis do |r|
505
- cleanup_index = ->(index) {
506
- r.zrangebyscore(index, "0", BID_EXPIRE_TTL.seconds.ago.to_i).each do |bid|
507
- r.zrem(index, bid) if cleanup_redis_index_for("BID-#{bid}", suffixes)
508
- end
509
- }
510
-
511
- cleanup_index.("BID-ROOT-bids")
512
- cleanup_index.("batches")
513
- end
514
- end
515
-
516
- def redis(&blk)
517
- return RedisProxy.new unless block_given?
518
-
519
- if Thread.current[:job_batches_redis]
520
- yield Thread.current[:job_batches_redis]
521
- else
522
- ::Bearcat.redis do |r|
523
- Thread.current[:job_batches_redis] = r
524
- yield r
525
- ensure
526
- Thread.current[:job_batches_redis] = nil
527
- end
528
- end
529
- end
530
-
531
- def logger
532
- ::CanvasSync.logger
533
- end
534
-
535
- def push_callbacks(args, queue)
536
- Batch::Callback::worker_class.enqueue_all(args, queue)
537
- end
538
-
539
- def bid_hierarchy(bid, depth: 4, per_depth: 5, slice: nil)
540
- args = [bid, depth, per_depth]
541
- args << slice if slice
542
- redis do |r|
543
- BID_HIERARCHY.call(r, [], args)
544
- end
545
- end
546
- end
547
-
548
- class RedisProxy
549
- def multi(*args, &block)
550
- Batch.redis do |r|
551
- r.multi(*args) do |r|
552
- block.call(r)
553
- end
554
- end
555
- end
556
-
557
- def pipelined(*args, &block)
558
- Batch.redis do |r|
559
- r.pipelined(*args) do |r2|
560
- block.call(r2 || r)
561
- end
562
- end
563
- end
564
-
565
- def uget(key)
566
- Batch.redis do |r|
567
- case r.type(key)
568
- when 'string'
569
- r.get(key)
570
- when 'list'
571
- r.lrange(key, 0, -1)
572
- when 'hash'
573
- r.hgetall(key)
574
- when 'set'
575
- r.smembers(key)
576
- when 'zset'
577
- r.zrange(key, 0, -1)
578
- end
579
- end
580
- end
581
-
582
- def method_missing(method_name, *arguments, &block)
583
- Batch.redis do |r|
584
- r.send(method_name, *arguments, &block)
585
- end
586
- end
587
-
588
- def respond_to_missing?(method_name, include_private = false)
589
- super || Redis.method_defined?(method_name)
590
- end
591
- end
592
- end
593
- end
594
-
595
- require_relative './compat'