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,199 +0,0 @@
1
-
2
- module CanvasSync::JobUniqueness
3
- class LockContext
4
- def self.from_serialized(data, **kwargs)
5
- context_class = data[:clazz]&.constantize || self
6
- context_class.new(data, **kwargs)
7
- end
8
-
9
- attr_reader :lock_id
10
-
11
- # { job_clazz, jid, queue, args?, kwargs?, base_key? }
12
- def initialize(data, job_instance: nil, config: nil)
13
- @base_key = data[:base_key]
14
- @context_data = data
15
- @job_instance = job_instance
16
- @config = config || @context_data[:config]
17
-
18
- # TODO Consider (somewhere) updating the lock_id to the BID of the wrapping Batch (when applicable)
19
- @lock_id ||= data[:lid] || Thread.current[:unique_jobs_previous_context]&.lock_id || job_id
20
- end
21
-
22
- # This is primarily for rehydrating in a Batch Callback, so it is unlikely that args and kwargs are needed.
23
- def serialize
24
- {
25
- lid: lock_id,
26
- clazz: self.class.to_s,
27
- job_clazz: @context_data[:job_clazz].to_s,
28
- jid: @context_data[:jid],
29
- queue: @context_data[:queue],
30
- **cache_data,
31
- }
32
- end
33
-
34
- # Properties to cache on the serialized Job object to prevent issues arising from code changes between enqueue and perform
35
- def cache_data
36
- {
37
- lid: lock_id,
38
- base_key: base_key,
39
- job_score: job_score,
40
- # TODO Should config also be cached on the Job at time of enqueue?
41
- # config: config,
42
- }
43
- end
44
-
45
- def debug_data
46
- {
47
- lid: lock_id,
48
- context_class: self.class.to_s,
49
- job_class: job_class.to_s,
50
- queue: job_queue,
51
- limit: config[:limit],
52
- timeout: config[:timeout],
53
- ttl: config[:ttl],
54
- strategy: config[:strategy],
55
- time: Time.now.to_f,
56
- at: job_scheduled_at,
57
- }
58
- end
59
-
60
- def handle_lifecycle!(stage, *args, **kwargs, &blk)
61
- lock_strategy.send(:"on_#{stage}", *args, **kwargs, &blk)
62
- rescue CouldNotLockError => e
63
- call_conflict_strategy(stage)
64
- end
65
-
66
- def lock_strategy
67
- return @lock_strategy if defined?(@lock_strategy)
68
-
69
- strat_name = config[:strategy]
70
- @lock_strategy = Strategy.lookup(strat_name).new(self)
71
- end
72
-
73
- def config
74
- @config ||= job_class.unique_job_options
75
- end
76
-
77
- def job_class
78
- @job_class ||= begin
79
- if (job_clazz = @context_data[:job_clazz]).is_a?(String)
80
- job_clazz.constantize
81
- else
82
- job_clazz
83
- end
84
- end
85
- end
86
-
87
- def job_id
88
- @context_data[:jid]
89
- end
90
-
91
- def job_queue
92
- @context_data[:queue]
93
- end
94
-
95
- def job_scheduled_at
96
- nil
97
- end
98
-
99
- def job_score
100
- @context_data[:job_score] || job_scheduled_at.to_s
101
- end
102
-
103
- def base_key(any_hash: false)
104
- @base_key ||= begin
105
- queue = @context_data[:queue] || "default"
106
-
107
- base_key = [
108
- CanvasSync::JobUniqueness.config.lock_prefix.presence,
109
- ].compact
110
-
111
- scope = config[:scope]
112
- if scope.is_a?(Proc)
113
- base_key << scope.call(queue: queue)
114
- elsif scope == :global
115
- base_key << job_class.name
116
- elsif scope == :per_queue
117
- base_key << job_class.name
118
- base_key << queue
119
- else
120
- base_key << scope
121
- end
122
-
123
- args = @context_data[:args] || []
124
- kwargs = @context_data[:kwargs] || {}
125
- hash = config[:hash]
126
- if config[:hash].is_a?(Proc)
127
- hash = config[:hash].call(*args, **kwargs)
128
- elsif config[:hash].nil?
129
- hash = [*args, kwargs]
130
- end
131
-
132
- hash = ":#{hash}" if hash.is_a?(Symbol)
133
-
134
- if hash && !hash.is_a?(String)
135
- hash = Array(hash)
136
-
137
- # Normalize the hash to ensure that the order of any Hash keys don't matter
138
- hash = normalize_hash_chunk(hash)
139
-
140
- normalized = ActiveJob::Arguments.serialize(hash)
141
- hash = OpenSSL::Digest::MD5.hexdigest(JSON.dump(normalized))
142
- end
143
-
144
- base_key << hash if hash
145
-
146
- base_key.join(":")
147
- end
148
- end
149
-
150
- def reenqueue
151
- raise NotImplementedError, "needs to be implemented in child class"
152
- end
153
-
154
- protected
155
-
156
- attr_reader :job_instance
157
-
158
- #
159
- # Call whatever strategy that has been configured
160
- #
161
- # @param [Symbol] origin the origin `:client` or `:server`
162
- #
163
- # @return [void] the return value is irrelevant
164
- #
165
- # @yieldparam [void] if a new job id was set and a block is given
166
- # @yieldreturn [void] the yield is irrelevant, it only provides a mechanism in
167
- # one specific situation to yield back to the middleware.
168
- def call_conflict_strategy(origin)
169
- strategy = conflict_strategy_for(origin)
170
-
171
- strategy.call
172
- end
173
-
174
- def conflict_strategy_for(origin)
175
- raise ArgumentError, "#origin needs to be either `:enqueue` or `:perform`" unless %i[enqueue perform].include?(origin)
176
-
177
- @conflict_strategies ||= {}
178
- @conflict_strategies[origin] ||= begin
179
- cfg = config[:on_conflict]
180
- cfg = cfg[origin] if cfg.is_a?(Hash)
181
-
182
- cstrat_cls = OnConflict.lookup(cfg || :null_strategy)
183
- cstrat = cstrat_cls.new(self) # TODO Pass redis_pool?
184
- end
185
- end
186
-
187
- private
188
-
189
- def normalize_hash_chunk(chunk)
190
- if chunk.is_a?(Hash)
191
- chunk.map { |k, v| [k, normalize_hash_chunk(v)] }.sort.to_h
192
- elsif chunk.is_a?(Array)
193
- chunk.map { |c| normalize_hash_chunk(c) }
194
- else
195
- chunk
196
- end
197
- end
198
- end
199
- end
@@ -1,92 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "sidekiq_unique_jobs"
4
-
5
- module CanvasSync::JobUniqueness
6
- # This class is intended to be the complete translation layer between CanvasSync::JobUniqueness and SidekiqUniqueJobs.
7
- # In other words, you could consider it the "locking backend" and thus could potentially swap out SUJ for a more succinct solution.
8
- #
9
- # SUJ's implementation is somewhat complex, but is somewhat pre-tailored over (eg) https://github.com/leandromoreira/redlock-rb.
10
- # Mainly SUJ tracks the JID so that if a process dies, another can pick up the job without having to figure out how to unlock it.
11
- # SUJ also handles the integration into Sidekiq Web, which is a nice bonus.
12
- class Locksmith < SidekiqUniqueJobs::Locksmith
13
- attr_reader :lock_context
14
-
15
- def initialize(key, lock_context, redis_pool = nil)
16
- @lock_context = lock_context
17
- @job_id = lock_context.lock_id # Yes, .lock_id is intentional
18
- @item = lock_context
19
- @key = SidekiqUniqueJobs::Key.new(key)
20
-
21
- lcfg = lock_context.config
22
- @config = OpenStruct.new({
23
- :"type" => lcfg[:strategy],
24
- :"pttl" => lcfg[:ttl] * 1000,
25
- :"timeout" => lcfg[:timeout],
26
- :"wait_for_lock?" => lcfg[:ttl]&.positive?,
27
- :"lock_info" => false,
28
- :"limit" => lcfg[:limit],
29
- })
30
-
31
- @redis_pool = redis_pool
32
- end
33
-
34
- def locked_jids
35
- SidekiqUniqueJobs::Lock.new(@key).locked_jids
36
- end
37
-
38
- def swap_locks(old_jid)
39
- olimit = lock_context.config[:limit]
40
- new_jid = @job_id
41
- return if old_jid == new_jid
42
-
43
- # NB This is quite hacky, but should work
44
- #
45
- # Ideally the unlock(old) and lock(new) would be atomic, but that increases the amount of coupling with Sidekiq-Unique-Jobs - right now,
46
- # we're using fairly stable (though still internal) SUJ APIs; I fear that writing custom Lua will be significantly more brittle
47
- #
48
- # In the general case, we'd only bump limit by 1, but that leaves a potential race-condition when limit is configured > 1:
49
- # (Assuming until_and_while_executing, reschedule, limit = 2):
50
- # (Workers are performing 2 Jobs, RUN lock count = 2)
51
- # Worker 1 pulls Job A
52
- # Worker 2 pulls Job B
53
- # W1 and W2 both fail to get the runtime lock
54
- # W1 and W2 call swap_locks
55
- # W1 calls lock(limit+1), lock is granted, lock count becomes limit+1
56
- # W2 calls lock(limit+1), lock is denied because count would be limit+2
57
- # W1 calls unlock(old_jid)
58
-
59
- # Force creation of another lock, ignoring the limit
60
- @config.limit = olimit + 100
61
- result = lock
62
-
63
- # Release the old lock, bringing us back within the limit
64
- @job_id = old_jid
65
- unlock
66
-
67
- result
68
- ensure
69
- @config.limit = olimit
70
- @job_id = new_jid
71
- end
72
-
73
- private
74
-
75
- def lock_score
76
- lock_context.job_score
77
- end
78
-
79
- def lock_info
80
- @lock_info = JSON.dump(lock_context.debug_data)
81
- end
82
-
83
- # def taken?(conn)
84
- # v = conn.hexists(key.locked, job_id)
85
- # v.is_a?(Numeric) ? v != 0 : v
86
- # end
87
-
88
- def redis_version
89
- @redis_version ||= CanvasSync::JobUniqueness.config.redis_version || SidekiqUniqueJobs.fetch_redis_version
90
- end
91
- end
92
- end
@@ -1,32 +0,0 @@
1
- module CanvasSync::JobUniqueness
2
- module OnConflict
3
- class Base
4
- attr_reader :lock_context
5
- attr_reader :redis_pool
6
-
7
- class_attribute :_valid_for, instance_writer: false
8
-
9
- def self.valid_for(*origins)
10
- if origins.present?
11
- orgins = Array(origins).map(&:to_sym)
12
- self._valid_for = origins
13
- else
14
- self._valid_for || [:enqueue, :perform]
15
- end
16
- end
17
-
18
- def initialize(lock_context, redis_pool = nil)
19
- @lock_context = lock_context
20
- @redis_pool = redis_pool
21
- end
22
-
23
- def call
24
- raise NotImplementedError, "needs to be implemented in child class"
25
- end
26
-
27
- def replace?
28
- is_a?(Replace)
29
- end
30
- end
31
- end
32
- end
@@ -1,13 +0,0 @@
1
- module CanvasSync::JobUniqueness
2
- module OnConflict
3
- class Log < Base
4
- valid_for :enqueue, :perform
5
-
6
- def call
7
- CanvasSync::JobUniqueness.logger.info(<<~MESSAGE.chomp)
8
- Skipping job with id (#{lock_context.job_id}) because key: (#{lock_context.base_key}) is locked
9
- MESSAGE
10
- end
11
- end
12
- end
13
- end
@@ -1,9 +0,0 @@
1
- module CanvasSync::JobUniqueness
2
- module OnConflict
3
- class NullStrategy < Base
4
- valid_for :enqueue, :perform
5
-
6
- def call; end
7
- end
8
- end
9
- end
@@ -1,11 +0,0 @@
1
- module CanvasSync::JobUniqueness
2
- module OnConflict
3
- class Raise < Base
4
- valid_for :enqueue, :perform
5
-
6
- def call
7
- raise Conflict, lock_context
8
- end
9
- end
10
- end
11
- end
@@ -1,21 +0,0 @@
1
- module CanvasSync::JobUniqueness
2
- module OnConflict
3
- class Reject < Base
4
- valid_for :perform
5
-
6
- def call
7
- # TODO Allow this to work on Sidekiq-backed ActiveJob
8
- unless lock_context.is_a?(CanvasSync::JobUniqueness::Compat::Sidekiq::SidekiqLockContext)
9
- CanvasSync::JobUniqueness.logger.error(":reject conflict strategy is not supported for non-Sidekiq-backed jobs")
10
- return
11
- end
12
-
13
- kwargs = {}
14
- kwargs[:notify_failure] = false if Sidekiq::DeadSet.instance_method(:kill).arity > 1
15
-
16
- sidekiq_message = lock_context.instance_variable_get(:@job_instance)
17
- Sidekiq::DeadSet.new.kill(JSON.dump(sidekiq_message), **kwargs)
18
- end
19
- end
20
- end
21
- end
@@ -1,20 +0,0 @@
1
- module CanvasSync::JobUniqueness
2
- module OnConflict
3
- class Reschedule < OnConflict::Base
4
- valid_for :perform
5
-
6
- def call
7
- Thread.current[:unique_jobs_previous_context] = lock_context
8
- rescheduled = lock_context.reenqueue(
9
- schedule_in: schedule_in,
10
- )
11
- ensure
12
- Thread.current[:unique_jobs_previous_context] = nil
13
- end
14
-
15
- def schedule_in
16
- lock_context.config[:reschedule_in] || 60
17
- end
18
- end
19
- end
20
- end
@@ -1,62 +0,0 @@
1
- module CanvasSync::JobUniqueness
2
- module OnConflict
3
- extend ActiveSupport::Autoload
4
-
5
- autoload :Base
6
-
7
- autoload :Log
8
- autoload :NullStrategy
9
- autoload :Raise
10
- autoload :Reject
11
- autoload :Reschedule
12
-
13
- # Replace is... hard. It involves hacking into some of the inner workings of Sidekiq and/or ActiveJob.
14
- # The best solution would be to somehow mark the previously-scheduled job as dead and let it be picked up by workers but not invoke
15
- # its perform method (technically this would mess with success numbers, but does anybody really care about those?).
16
- # Replace will also conflict with the UntilExecuted strategy as it wouldn't be able to replace a job that has begun executing.
17
- #
18
- # Replace is useful for two cases:
19
- # 1. Moving the job to the back of the queue, eg for debouncing purposes
20
- # 2. Replacing non-hashed params with updated values
21
- # A solution to (2) would be to store the latest params outside of the Job params - eg in the DB or in Redis directly.
22
- # (1) is solvebale in a similar out-of-band way, but other, better solutions may exist as well
23
- #
24
- # If the param in question is a (eg) a list that you want to append to, you could:
25
- # Push to the list in Redis
26
- # Queue the job
27
- # (Push another param to the list in Redis)
28
- # (Re-enqueue the job, but it won't enqueue because it is locked)
29
- # When the job begins, it pulls all values from the field in Redis an processes
30
- # Adding more params from this point would allow another job to enqueue
31
- #
32
- # We may implement some tooling for this at some point, but for now the recommendation is to handle this yourself,
33
- # autoload :Replace
34
-
35
- class << self
36
- def lookup(strategy)
37
- matching_strategy(strategy.to_s.camelize) ||
38
- CanvasSync::JobUniqueness.config.conflict_strategies[strategy] ||
39
- raise(StrategyNotFound, "on_conflict: #{strategy} is not found. Is it declared in the configuration?")
40
- end
41
-
42
- def validate!(on_conflict, lock_strategy)
43
- on_conflict = { enqueue: on_conflict, perform: on_conflict } unless on_conflict.is_a?(Hash)
44
-
45
- lock_strategy = Strategy.lookup(lock_strategy) if lock_strategy.is_a?(Symbol)
46
- on_conflict.each do |origin, strategy|
47
- strategy = OnConflict.lookup(strategy) if strategy.is_a?(Symbol)
48
-
49
- if lock_strategy.locks_on.include?(origin) && !strategy.valid_for.include?(origin)
50
- raise ArgumentError, "(#{origin.to_s.titleize}) conflict strategy #{strategy.name.underscore} is not valid for lock strategy #{lock_strategy.name.underscore}"
51
- end
52
- end
53
- end
54
-
55
- private
56
-
57
- def matching_strategy(const)
58
- const_get(const, false) if const_defined?(const, false)
59
- end
60
- end
61
- end
62
- end
@@ -1,107 +0,0 @@
1
- module CanvasSync::JobUniqueness
2
- module Strategy
3
- class Base
4
- attr_reader :lock_context
5
-
6
- def initialize(lock_context)
7
- @lock_context = lock_context
8
- @conflict_strategies = {}
9
- end
10
-
11
- class_attribute :_locks_on, instance_writer: false
12
-
13
- def self.locks_on(*origins)
14
- if origins.present?
15
- orgins = Array(origins).map(&:to_sym)
16
- self._locks_on = origins
17
- else
18
- self._locks_on || [:enqueue, :perform]
19
- end
20
- end
21
-
22
- def on_enqueue; end
23
- def on_perform; end
24
-
25
- def batch_callback(event, batch_status)
26
- if event == :success
27
- unlock
28
- else
29
- unlock_cond = lock_context.config[:unlock_on_failure]
30
-
31
- if (event == :complete && unlock_cond == :any) || (event == :death && unlock_cond == :death) || (event == :stagnated && unlock_cond == :stagnant)
32
- unlock
33
- end
34
- end
35
- end
36
-
37
- delegate :locked?, to: :locksmith
38
-
39
- protected
40
-
41
- def key
42
- lock_context.base_key
43
- end
44
-
45
- def wrap_in_batch(&blk)
46
- if Thread.current[:unique_jobs_previous_context] # Ensure we don't re-wrap in a batch when rescheduling
47
- return blk.call
48
- end
49
-
50
- batch = CanvasSync::JobBatches::Batch.new
51
- batch.context = {
52
- uniqueness_lock_key: key,
53
- }
54
-
55
- CanvasSync::JobBatches::Batch::Callback::VALID_CALLBACKS.each do |callback|
56
- callback = callback.to_sym
57
- batch.on(callback, self.class.to_s + ".internal_batch_callback", {
58
- event: callback,
59
- lock_strategy: self.class.to_s,
60
- lock_key: key,
61
- lock_context: lock_context.serialize,
62
- })
63
- end
64
-
65
- CanvasSync::JobUniqueness.logger.debug("Wrapped job (#{lock_context.lock_id}) in Locking Batch #{batch.bid} for #{key}")
66
-
67
- batch.jobs do
68
- return blk.call
69
- end
70
- end
71
-
72
- def self.internal_batch_callback(batch_status, opts)
73
- CanvasSync::JobUniqueness.logger.debug("Received Batch(#{batch_status.bid}) callback for #{opts[:lock_strategy]} #{opts[:lock_key]} - #{opts[:event]}")
74
- CanvasSync::JobUniqueness.logger.debug("Context data: #{opts[:lock_context]}")
75
- strategy_class = opts[:lock_strategy].constantize
76
- lock_context = LockContext.from_serialized(opts[:lock_context])
77
- CanvasSync::JobUniqueness.logger.debug("Rehydrated LockContext: #{lock_context.lock_id} #{lock_context.debug_data}")
78
- strategy = strategy_class.new(lock_context)
79
- # TODO Should this route through LockContext#handle_lifecycle!?
80
- strategy.batch_callback(opts[:event].to_sym, batch_status)
81
- end
82
-
83
- def lock!(purpose, wait: nil)
84
- locked = nil
85
- if purpose == :enqueue
86
- locked = locksmith.lock()
87
- elsif purpose == :perform
88
- locked = locksmith.execute { lock_context.lock_id }
89
- end
90
-
91
- CanvasSync::JobUniqueness.logger.debug { "Requested lock of #{key} for #{purpose} - (#{locked || 'Not Obtained!'})" }
92
-
93
- raise CouldNotLockError.new(lock_context, source: purpose) if !locked
94
- end
95
-
96
- def unlock()
97
- CanvasSync::JobUniqueness.logger.debug { "Trying to unlock #{key} for LID #{lock_context.lock_id}" }
98
- result = locksmith.unlock
99
- CanvasSync::JobUniqueness.logger.debug { "Unlocked #{key} - (#{result || 'Not Unlocked!'})" }
100
- end
101
-
102
- def locksmith
103
- @locksmith ||= Locksmith.new(key, lock_context)
104
- end
105
- end
106
- end
107
- end
@@ -1,35 +0,0 @@
1
- module CanvasSync::JobUniqueness
2
- module Strategy
3
- # Implements two locks - one while enqueued and one while performing
4
- class UntilAndWhileExecuting < Base
5
- locks_on :enqueue, :perform
6
-
7
- def on_enqueue
8
- # Obtain lock
9
- lock!(:enqueue)
10
-
11
- # Proceed with enqueuing the job, wrapping it in a batch
12
- runtime_lock.on_enqueue do
13
- yield
14
- end
15
- end
16
-
17
- def on_perform
18
- # Obtain Runtime lock
19
- runtime_lock.on_perform do
20
- # Release Queue lock
21
- unlock()
22
-
23
- # Run the job
24
- yield
25
- end
26
- end
27
-
28
- private
29
-
30
- def runtime_lock
31
- @runtime_lock ||= Strategy::WhileExecuting.new(lock_context)
32
- end
33
- end
34
- end
35
- end
@@ -1,20 +0,0 @@
1
- module CanvasSync::JobUniqueness
2
- module Strategy
3
- class UntilExecuted < Base
4
- locks_on :enqueue, :perform
5
-
6
- def on_enqueue
7
- lock!(:enqueue)
8
-
9
- wrap_in_batch do
10
- yield
11
- end
12
- end
13
-
14
- def on_perform
15
- lock!(:perform)
16
- yield
17
- end
18
- end
19
- end
20
- end
@@ -1,20 +0,0 @@
1
- module CanvasSync::JobUniqueness
2
- module Strategy
3
- class UntilExecuting < Base
4
- locks_on :enqueue
5
-
6
- def on_enqueue
7
- lock!(:enqueue)
8
- yield
9
- end
10
-
11
- def on_perform
12
- unlock
13
- yield
14
- end
15
-
16
- # TODO Define behavior when an error occurs during perform().
17
- # SUJ's behavior is to relock, but this has some edge-cases (like how do we handle if another job already took the lock?)
18
- end
19
- end
20
- end
@@ -1,16 +0,0 @@
1
- module CanvasSync::JobUniqueness
2
- module Strategy
3
- class UntilExpired < UntilExecuted
4
- locks_on :enqueue, :perform
5
-
6
- def on_enqueue
7
- lock!(:enqueue)
8
- yield
9
- end
10
-
11
- def on_perform
12
- lock!(:perform)
13
- end
14
- end
15
- end
16
- end
@@ -1,26 +0,0 @@
1
- module CanvasSync::JobUniqueness
2
- module Strategy
3
- class WhileExecuting < Base
4
- locks_on :perform
5
-
6
- RUN_SUFFIX = ":RUN"
7
-
8
- def on_enqueue
9
- wrap_in_batch do
10
- yield
11
- end
12
- end
13
-
14
- def on_perform
15
- lock!(:perform)
16
- yield
17
- end
18
-
19
- protected
20
-
21
- def key
22
- super + RUN_SUFFIX
23
- end
24
- end
25
- end
26
- end