canvas_sync 0.16.4 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (260) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +235 -151
  3. data/app/controllers/{api → canvas_sync/api}/v1/health_check_controller.rb +1 -1
  4. data/app/controllers/canvas_sync/api/v1/live_events_controller.rb +122 -0
  5. data/app/models/canvas_sync/sync_batch.rb +5 -0
  6. data/config/initializers/apartment.rb +10 -1
  7. data/config/routes.rb +7 -0
  8. data/db/migrate/20170915210836_create_canvas_sync_job_log.rb +12 -31
  9. data/db/migrate/20180725155729_add_job_id_to_canvas_sync_job_logs.rb +4 -13
  10. data/db/migrate/20190916154829_add_fork_count_to_canvas_sync_job_logs.rb +3 -11
  11. data/db/migrate/20201018210836_create_canvas_sync_sync_batches.rb +11 -0
  12. data/db/migrate/20201030210836_add_full_sync_to_canvas_sync_sync_batch.rb +7 -0
  13. data/lib/canvas_sync/batch_processor.rb +41 -0
  14. data/lib/canvas_sync/concerns/ability_helper.rb +72 -0
  15. data/lib/canvas_sync/concerns/account/ancestry.rb +2 -0
  16. data/lib/canvas_sync/concerns/account/base.rb +15 -0
  17. data/lib/canvas_sync/concerns/api_syncable.rb +17 -10
  18. data/lib/canvas_sync/concerns/live_event_sync.rb +46 -0
  19. data/lib/canvas_sync/concerns/role/base.rb +57 -0
  20. data/lib/canvas_sync/concerns/sync_mapping.rb +120 -0
  21. data/lib/canvas_sync/engine.rb +80 -0
  22. data/lib/canvas_sync/generators/install_generator.rb +1 -0
  23. data/lib/canvas_sync/generators/install_live_events_generator.rb +0 -1
  24. data/lib/canvas_sync/generators/templates/migrations/create_content_migrations.rb +24 -0
  25. data/lib/canvas_sync/generators/templates/migrations/create_course_nicknames.rb +17 -0
  26. data/lib/canvas_sync/generators/templates/migrations/create_grading_period_groups.rb +18 -0
  27. data/lib/canvas_sync/generators/templates/migrations/create_grading_periods.rb +22 -0
  28. data/lib/canvas_sync/generators/templates/migrations/create_learning_outcome_results.rb +46 -0
  29. data/lib/canvas_sync/generators/templates/migrations/create_learning_outcomes.rb +30 -0
  30. data/lib/canvas_sync/generators/templates/migrations/create_rubric_assessments.rb +31 -0
  31. data/lib/canvas_sync/generators/templates/migrations/create_rubric_associations.rb +36 -0
  32. data/lib/canvas_sync/generators/templates/migrations/create_rubrics.rb +38 -0
  33. data/lib/canvas_sync/generators/templates/migrations/create_user_observers.rb +17 -0
  34. data/lib/canvas_sync/generators/templates/migrations/create_users.rb +0 -1
  35. data/lib/canvas_sync/generators/templates/models/account.rb +3 -0
  36. data/lib/canvas_sync/generators/templates/models/admin.rb +2 -0
  37. data/lib/canvas_sync/generators/templates/models/assignment.rb +3 -0
  38. data/lib/canvas_sync/generators/templates/models/assignment_group.rb +3 -0
  39. data/lib/canvas_sync/generators/templates/models/content_migration.rb +12 -0
  40. data/lib/canvas_sync/generators/templates/models/context_module.rb +3 -0
  41. data/lib/canvas_sync/generators/templates/models/context_module_item.rb +3 -0
  42. data/lib/canvas_sync/generators/templates/models/course.rb +11 -0
  43. data/lib/canvas_sync/generators/templates/models/course_nickname.rb +13 -0
  44. data/lib/canvas_sync/generators/templates/models/enrollment.rb +14 -0
  45. data/lib/canvas_sync/generators/templates/models/grading_period.rb +10 -0
  46. data/lib/canvas_sync/generators/templates/models/grading_period_group.rb +9 -0
  47. data/lib/canvas_sync/generators/templates/models/group.rb +2 -0
  48. data/lib/canvas_sync/generators/templates/models/group_membership.rb +2 -0
  49. data/lib/canvas_sync/generators/templates/models/learning_outcome.rb +24 -0
  50. data/lib/canvas_sync/generators/templates/models/learning_outcome_result.rb +48 -0
  51. data/lib/canvas_sync/generators/templates/models/pseudonym.rb +2 -0
  52. data/lib/canvas_sync/generators/templates/models/role.rb +2 -0
  53. data/lib/canvas_sync/generators/templates/models/rubric.rb +29 -0
  54. data/lib/canvas_sync/generators/templates/models/rubric_assessment.rb +17 -0
  55. data/lib/canvas_sync/generators/templates/models/rubric_association.rb +14 -0
  56. data/lib/canvas_sync/generators/templates/models/section.rb +9 -0
  57. data/lib/canvas_sync/generators/templates/models/submission.rb +3 -0
  58. data/lib/canvas_sync/generators/templates/models/term.rb +3 -0
  59. data/lib/canvas_sync/generators/templates/models/user.rb +11 -0
  60. data/lib/canvas_sync/generators/templates/models/user_observer.rb +13 -0
  61. data/lib/canvas_sync/generators/templates/services/live_events/assignment_event.rb +1 -1
  62. data/lib/canvas_sync/generators/templates/services/live_events/assignment_group_event.rb +1 -1
  63. data/lib/canvas_sync/generators/templates/services/live_events/course_event.rb +1 -3
  64. data/lib/canvas_sync/generators/templates/services/live_events/course_section_event.rb +1 -1
  65. data/lib/canvas_sync/generators/templates/services/live_events/enrollment_event.rb +1 -1
  66. data/lib/canvas_sync/generators/templates/services/live_events/grade_event.rb +1 -1
  67. data/lib/canvas_sync/generators/templates/services/live_events/module_event.rb +1 -1
  68. data/lib/canvas_sync/generators/templates/services/live_events/module_item_event.rb +1 -1
  69. data/lib/canvas_sync/generators/templates/services/live_events/submission_event.rb +1 -1
  70. data/lib/canvas_sync/generators/templates/services/live_events/syllabus_event.rb +1 -1
  71. data/lib/canvas_sync/generators/templates/services/live_events/user_event.rb +1 -3
  72. data/lib/canvas_sync/importers/bulk_importer.rb +138 -31
  73. data/lib/canvas_sync/job.rb +7 -5
  74. data/lib/canvas_sync/job_batches/active_job.rb +108 -0
  75. data/lib/canvas_sync/job_batches/batch.rb +543 -0
  76. data/lib/canvas_sync/job_batches/callback.rb +149 -0
  77. data/lib/canvas_sync/job_batches/chain_builder.rb +249 -0
  78. data/lib/canvas_sync/job_batches/context_hash.rb +159 -0
  79. data/lib/canvas_sync/job_batches/hier_batch_ids.lua +25 -0
  80. data/lib/canvas_sync/job_batches/jobs/base_job.rb +7 -0
  81. data/lib/canvas_sync/job_batches/jobs/concurrent_batch_job.rb +22 -0
  82. data/lib/canvas_sync/job_batches/jobs/managed_batch_job.rb +170 -0
  83. data/lib/canvas_sync/job_batches/jobs/serial_batch_job.rb +22 -0
  84. data/lib/canvas_sync/job_batches/pool.rb +245 -0
  85. data/lib/canvas_sync/job_batches/pool_refill.lua +47 -0
  86. data/lib/canvas_sync/job_batches/redis_model.rb +69 -0
  87. data/lib/canvas_sync/job_batches/redis_script.rb +163 -0
  88. data/lib/canvas_sync/job_batches/schedule_callback.lua +14 -0
  89. data/lib/canvas_sync/job_batches/sidekiq/web/batches_assets/css/styles.less +182 -0
  90. data/lib/canvas_sync/job_batches/sidekiq/web/batches_assets/js/batch_tree.js +108 -0
  91. data/lib/canvas_sync/job_batches/sidekiq/web/batches_assets/js/util.js +2 -0
  92. data/lib/canvas_sync/job_batches/sidekiq/web/helpers.rb +41 -0
  93. data/lib/canvas_sync/job_batches/sidekiq/web/views/_batch_tree.erb +6 -0
  94. data/lib/canvas_sync/job_batches/sidekiq/web/views/_batches_table.erb +44 -0
  95. data/lib/canvas_sync/job_batches/sidekiq/web/views/_common.erb +13 -0
  96. data/lib/canvas_sync/job_batches/sidekiq/web/views/_jobs_table.erb +21 -0
  97. data/lib/canvas_sync/job_batches/sidekiq/web/views/_pagination.erb +26 -0
  98. data/lib/canvas_sync/job_batches/sidekiq/web/views/batch.erb +81 -0
  99. data/lib/canvas_sync/job_batches/sidekiq/web/views/batches.erb +23 -0
  100. data/lib/canvas_sync/job_batches/sidekiq/web/views/pool.erb +137 -0
  101. data/lib/canvas_sync/job_batches/sidekiq/web/views/pools.erb +47 -0
  102. data/lib/canvas_sync/job_batches/sidekiq/web.rb +218 -0
  103. data/lib/canvas_sync/job_batches/sidekiq.rb +136 -0
  104. data/lib/canvas_sync/job_batches/status.rb +91 -0
  105. data/lib/canvas_sync/jobs/begin_sync_chain_job.rb +99 -0
  106. data/lib/canvas_sync/jobs/canvas_process_waiter.rb +41 -0
  107. data/lib/canvas_sync/jobs/report_checker.rb +70 -8
  108. data/lib/canvas_sync/jobs/report_processor_job.rb +4 -7
  109. data/lib/canvas_sync/jobs/report_starter.rb +34 -20
  110. data/lib/canvas_sync/jobs/sync_accounts_job.rb +3 -5
  111. data/lib/canvas_sync/jobs/sync_admins_job.rb +2 -4
  112. data/lib/canvas_sync/jobs/sync_assignment_groups_job.rb +2 -4
  113. data/lib/canvas_sync/jobs/sync_assignments_job.rb +2 -4
  114. data/lib/canvas_sync/jobs/sync_content_migrations_job.rb +20 -0
  115. data/lib/canvas_sync/jobs/sync_context_module_items_job.rb +2 -4
  116. data/lib/canvas_sync/jobs/sync_context_modules_job.rb +2 -4
  117. data/lib/canvas_sync/jobs/sync_provisioning_report_job.rb +16 -50
  118. data/lib/canvas_sync/jobs/sync_roles_job.rb +2 -5
  119. data/lib/canvas_sync/jobs/sync_rubric_assessments_job.rb +15 -0
  120. data/lib/canvas_sync/jobs/sync_rubric_associations_job.rb +15 -0
  121. data/lib/canvas_sync/jobs/sync_rubrics_job.rb +15 -0
  122. data/lib/canvas_sync/jobs/sync_simple_table_job.rb +11 -32
  123. data/lib/canvas_sync/jobs/sync_submissions_job.rb +6 -4
  124. data/lib/canvas_sync/jobs/sync_terms_job.rb +9 -8
  125. data/lib/canvas_sync/jobs/term_batches_job.rb +50 -0
  126. data/lib/canvas_sync/{generators/templates/services/live_events/base_event.rb → live_events/base_handler.rb} +6 -10
  127. data/lib/canvas_sync/live_events/process_event_job.rb +26 -0
  128. data/lib/canvas_sync/live_events.rb +38 -0
  129. data/lib/canvas_sync/misc_helper.rb +63 -0
  130. data/lib/canvas_sync/processors/assignment_groups_processor.rb +3 -8
  131. data/lib/canvas_sync/processors/assignments_processor.rb +3 -8
  132. data/lib/canvas_sync/processors/content_migrations_processor.rb +19 -0
  133. data/lib/canvas_sync/processors/context_module_items_processor.rb +3 -8
  134. data/lib/canvas_sync/processors/context_modules_processor.rb +3 -8
  135. data/lib/canvas_sync/processors/model_mappings.yml +420 -0
  136. data/lib/canvas_sync/processors/normal_processor.rb +3 -3
  137. data/lib/canvas_sync/processors/provisioning_report_processor.rb +42 -55
  138. data/lib/canvas_sync/processors/report_processor.rb +15 -9
  139. data/lib/canvas_sync/processors/rubric_assessments_processor.rb +19 -0
  140. data/lib/canvas_sync/processors/rubric_associations_processor.rb +19 -0
  141. data/lib/canvas_sync/processors/rubrics_processor.rb +19 -0
  142. data/lib/canvas_sync/processors/submissions_processor.rb +3 -8
  143. data/lib/canvas_sync/record.rb +103 -0
  144. data/lib/canvas_sync/version.rb +1 -1
  145. data/lib/canvas_sync.rb +124 -125
  146. data/spec/canvas_sync/canvas_sync_spec.rb +224 -155
  147. data/spec/canvas_sync/jobs/canvas_process_waiter_spec.rb +34 -0
  148. data/spec/canvas_sync/jobs/job_spec.rb +9 -17
  149. data/spec/canvas_sync/jobs/report_checker_spec.rb +1 -3
  150. data/spec/canvas_sync/jobs/report_processor_job_spec.rb +0 -3
  151. data/spec/canvas_sync/jobs/report_starter_spec.rb +19 -28
  152. data/spec/canvas_sync/jobs/sync_admins_job_spec.rb +1 -4
  153. data/spec/canvas_sync/jobs/sync_assignment_groups_job_spec.rb +2 -1
  154. data/spec/canvas_sync/jobs/sync_assignments_job_spec.rb +3 -2
  155. data/spec/canvas_sync/jobs/sync_content_migrations_job_spec.rb +30 -0
  156. data/spec/canvas_sync/jobs/sync_context_module_items_job_spec.rb +3 -2
  157. data/spec/canvas_sync/jobs/sync_context_modules_job_spec.rb +3 -2
  158. data/spec/canvas_sync/jobs/sync_provisioning_report_job_spec.rb +7 -41
  159. data/spec/canvas_sync/jobs/sync_roles_job_spec.rb +1 -4
  160. data/spec/canvas_sync/jobs/sync_simple_table_job_spec.rb +5 -12
  161. data/spec/canvas_sync/jobs/sync_submissions_job_spec.rb +8 -2
  162. data/spec/canvas_sync/jobs/sync_terms_job_spec.rb +1 -4
  163. data/spec/canvas_sync/live_events/live_event_sync_spec.rb +27 -0
  164. data/spec/canvas_sync/live_events/live_events_controller_spec.rb +54 -0
  165. data/spec/canvas_sync/live_events/process_event_job_spec.rb +38 -0
  166. data/spec/canvas_sync/misc_helper_spec.rb +58 -0
  167. data/spec/canvas_sync/models/assignment_spec.rb +1 -1
  168. data/spec/canvas_sync/processors/content_migrations_processor_spec.rb +13 -0
  169. data/spec/canvas_sync/processors/provisioning_report_processor_spec.rb +101 -1
  170. data/spec/canvas_sync/processors/rubric_assessments_spec.rb +16 -0
  171. data/spec/canvas_sync/processors/rubric_associations_spec.rb +16 -0
  172. data/spec/canvas_sync/processors/rubrics_processor_spec.rb +17 -0
  173. data/spec/dummy/app/models/account.rb +6 -0
  174. data/spec/dummy/app/models/admin.rb +2 -0
  175. data/spec/dummy/app/models/assignment.rb +3 -0
  176. data/spec/dummy/app/models/assignment_group.rb +3 -0
  177. data/spec/dummy/app/models/content_migration.rb +18 -0
  178. data/spec/dummy/app/models/context_module.rb +3 -0
  179. data/spec/dummy/app/models/context_module_item.rb +3 -0
  180. data/spec/dummy/app/models/course.rb +11 -0
  181. data/spec/dummy/app/models/course_nickname.rb +19 -0
  182. data/spec/dummy/app/models/enrollment.rb +14 -0
  183. data/spec/dummy/app/models/grading_period.rb +16 -0
  184. data/spec/dummy/app/models/grading_period_group.rb +15 -0
  185. data/spec/dummy/app/models/group.rb +2 -0
  186. data/spec/dummy/app/models/group_membership.rb +2 -0
  187. data/spec/dummy/app/models/learning_outcome.rb +30 -0
  188. data/spec/dummy/app/models/learning_outcome_result.rb +54 -0
  189. data/spec/dummy/app/models/pseudonym.rb +16 -0
  190. data/spec/dummy/app/models/role.rb +2 -0
  191. data/spec/dummy/app/models/rubric.rb +35 -0
  192. data/spec/dummy/app/models/rubric_assessment.rb +22 -0
  193. data/spec/dummy/app/models/rubric_association.rb +20 -0
  194. data/spec/dummy/app/models/section.rb +9 -0
  195. data/spec/dummy/app/models/submission.rb +4 -0
  196. data/spec/dummy/app/models/term.rb +3 -0
  197. data/spec/dummy/app/models/user.rb +11 -0
  198. data/spec/dummy/app/models/user_observer.rb +19 -0
  199. data/spec/dummy/app/services/live_events/assignment_event.rb +1 -1
  200. data/spec/dummy/app/services/live_events/course_event.rb +1 -3
  201. data/spec/dummy/app/services/live_events/course_section_event.rb +1 -1
  202. data/spec/dummy/app/services/live_events/enrollment_event.rb +1 -1
  203. data/spec/dummy/app/services/live_events/grade_event.rb +1 -1
  204. data/spec/dummy/app/services/live_events/module_event.rb +1 -1
  205. data/spec/dummy/app/services/live_events/module_item_event.rb +1 -1
  206. data/spec/dummy/app/services/live_events/submission_event.rb +1 -1
  207. data/spec/dummy/app/services/live_events/syllabus_event.rb +1 -1
  208. data/spec/dummy/app/services/live_events/user_event.rb +1 -3
  209. data/spec/dummy/config/environments/test.rb +2 -0
  210. data/spec/dummy/config/routes.rb +1 -0
  211. data/spec/dummy/db/migrate/20201016181346_create_pseudonyms.rb +24 -0
  212. data/spec/dummy/db/migrate/20210907233329_create_user_observers.rb +23 -0
  213. data/spec/dummy/db/migrate/20210907233330_create_grading_periods.rb +28 -0
  214. data/spec/dummy/db/migrate/20211001184920_create_grading_period_groups.rb +24 -0
  215. data/spec/dummy/db/migrate/20220308072643_create_content_migrations.rb +30 -0
  216. data/spec/dummy/db/migrate/20220712210559_create_learning_outcomes.rb +36 -0
  217. data/spec/dummy/db/migrate/{20190702203620_create_users.rb → 20220926221926_create_users.rb} +0 -1
  218. data/spec/dummy/db/migrate/20240408223326_create_course_nicknames.rb +23 -0
  219. data/spec/dummy/db/migrate/20240509105100_create_rubrics.rb +44 -0
  220. data/spec/dummy/db/migrate/20240510094100_create_rubric_associations.rb +42 -0
  221. data/spec/dummy/db/migrate/20240510101100_create_rubric_assessments.rb +37 -0
  222. data/spec/dummy/db/migrate/20240523101010_create_learning_outcome_results.rb +52 -0
  223. data/spec/dummy/db/schema.rb +244 -5
  224. data/spec/factories/user_factory.rb +0 -1
  225. data/spec/job_batching/active_job_spec.rb +107 -0
  226. data/spec/job_batching/batch_spec.rb +489 -0
  227. data/spec/job_batching/callback_spec.rb +38 -0
  228. data/spec/job_batching/context_hash_spec.rb +54 -0
  229. data/spec/job_batching/flow_spec.rb +82 -0
  230. data/spec/job_batching/integration/fail_then_succeed.rb +42 -0
  231. data/spec/job_batching/integration/integration.rb +57 -0
  232. data/spec/job_batching/integration/nested.rb +88 -0
  233. data/spec/job_batching/integration/simple.rb +47 -0
  234. data/spec/job_batching/integration/workflow.rb +134 -0
  235. data/spec/job_batching/integration_helper.rb +50 -0
  236. data/spec/job_batching/pool_spec.rb +161 -0
  237. data/spec/job_batching/sidekiq_spec.rb +125 -0
  238. data/spec/job_batching/status_spec.rb +76 -0
  239. data/spec/job_batching/support/base_job.rb +14 -0
  240. data/spec/job_batching/support/sample_callback.rb +2 -0
  241. data/spec/spec_helper.rb +17 -0
  242. data/spec/support/fixtures/reports/content_migrations.csv +3 -0
  243. data/spec/support/fixtures/reports/course_nicknames.csv +3 -0
  244. data/spec/support/fixtures/reports/grading_period_groups.csv +2 -0
  245. data/spec/support/fixtures/reports/grading_periods.csv +3 -0
  246. data/spec/support/fixtures/reports/learning_outcome_results.csv +3 -0
  247. data/spec/support/fixtures/reports/learning_outcomes.csv +3 -0
  248. data/spec/support/fixtures/reports/provisioning_csv_unzipped/courses.csv +3 -0
  249. data/spec/support/fixtures/reports/provisioning_csv_unzipped/users.csv +4 -0
  250. data/spec/support/fixtures/reports/rubric_assessments.csv +3 -0
  251. data/spec/support/fixtures/reports/rubric_associations.csv +3 -0
  252. data/spec/support/fixtures/reports/rubrics.csv +3 -0
  253. data/spec/support/fixtures/reports/user_observers.csv +3 -0
  254. data/spec/support/fixtures/reports/users.csv +3 -2
  255. data/spec/support/fixtures/reports/xlist.csv +1 -1
  256. metadata +329 -27
  257. data/app/controllers/api/v1/live_events_controller.rb +0 -18
  258. data/lib/canvas_sync/job_chain.rb +0 -57
  259. data/lib/canvas_sync/jobs/fork_gather.rb +0 -59
  260. data/spec/canvas_sync/jobs/fork_gather_spec.rb +0 -73
@@ -0,0 +1,543 @@
1
+
2
+ begin
3
+ require 'sidekiq'
4
+ rescue LoadError
5
+ end
6
+
7
+ require_relative './redis_model'
8
+ require_relative './redis_script'
9
+ require_relative "./callback"
10
+ require_relative "./context_hash"
11
+ require_relative "./status"
12
+ require_relative "./pool"
13
+ Dir[File.dirname(__FILE__) + "/jobs/*.rb"].each { |file| require file }
14
+ require_relative "./chain_builder"
15
+
16
+ # Implement Job Batching similar to Sidekiq::Batch. Supports ActiveJob and Sidekiq, or a mix thereof.
17
+ # Much of this code is modifed/extended from https://github.com/breamware/sidekiq-batch
18
+
19
+ module CanvasSync
20
+ module JobBatches
21
+ CURRENT_BATCH_THREAD_KEY = :job_batches_batch
22
+
23
+ class Batch
24
+ include RedisModel
25
+
26
+ class NoBlockGivenError < StandardError; end
27
+
28
+ delegate :redis, to: :class
29
+
30
+ BID_EXPIRE_TTL = 90.days.to_i
31
+ INDEX_ALL_BATCHES = false
32
+ SCHEDULE_CALLBACK = RedisScript.new(Pathname.new(__FILE__) + "../schedule_callback.lua")
33
+ BID_HIERARCHY = RedisScript.new(Pathname.new(__FILE__) + "../hier_batch_ids.lua")
34
+
35
+ attr_reader :bid
36
+
37
+ def self.current
38
+ Thread.current[CURRENT_BATCH_THREAD_KEY]
39
+ end
40
+
41
+ def self.current_context
42
+ self.current&.context
43
+ end
44
+
45
+ def initialize(existing_bid = nil)
46
+ @bid = existing_bid || SecureRandom.urlsafe_base64(10)
47
+ @existing = !(!existing_bid || existing_bid.empty?) # Basically existing_bid.present?
48
+ @initialized = false
49
+ @bidkey = "BID-" + @bid.to_s
50
+ self.created_at = Time.now.utc.to_f unless @existing
51
+ end
52
+
53
+ redis_attr :description
54
+ redis_attr :created_at
55
+ redis_attr :callback_queue, read_only: false
56
+ redis_attr :callback_params, :json
57
+ redis_attr :allow_context_changes, :bool
58
+
59
+ def context
60
+ return @context if defined?(@context)
61
+
62
+ if (@initialized || @existing)
63
+ @context = ContextHash.new(bid)
64
+ else
65
+ @context = ContextHash.new(bid, {})
66
+ end
67
+ end
68
+
69
+ def context=(value)
70
+ raise "context is read-only once the batch has been started" if (@initialized || @existing) # && !allow_context_changes
71
+ raise "context must be a Hash" unless value.is_a?(Hash) || value.nil?
72
+ return nil if value.nil? && @context.nil?
73
+
74
+ value = {} if value.nil?
75
+ value = value.local if value.is_a?(ContextHash)
76
+
77
+ @context ||= ContextHash.new(bid, {})
78
+ @context.set_local(value)
79
+ # persist_bid_attr('context', JSON.unparse(@context.local))
80
+ end
81
+
82
+ def save_context_changes
83
+ @context&.save!
84
+ end
85
+
86
+ def on(event, callback, options = {})
87
+ return unless Callback::VALID_CALLBACKS.include?(event.to_s)
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
+ if !@existing && !@initialized
102
+ parent_bid = Thread.current[CURRENT_BATCH_THREAD_KEY]&.bid
103
+
104
+ redis.multi do |r|
105
+ r.hset(@bidkey, "parent_bid", parent_bid.to_s) if parent_bid
106
+ r.expire(@bidkey, BID_EXPIRE_TTL)
107
+
108
+ if parent_bid
109
+ r.hincrby("BID-#{parent_bid}", "children", 1)
110
+ r.expire("BID-#{parent_bid}", BID_EXPIRE_TTL)
111
+ r.zadd("BID-#{parent_bid}-bids", created_at, bid)
112
+ else
113
+ r.zadd("BID-ROOT-bids", created_at, bid)
114
+ end
115
+ end
116
+
117
+ flush_pending_attrs
118
+ @context&.save!
119
+
120
+ @initialized = true
121
+ else
122
+ assert_batch_is_open
123
+ end
124
+
125
+ begin
126
+ parent = Thread.current[CURRENT_BATCH_THREAD_KEY]
127
+ Thread.current[CURRENT_BATCH_THREAD_KEY] = self
128
+ yield
129
+ ensure
130
+ Thread.current[CURRENT_BATCH_THREAD_KEY] = parent
131
+ end
132
+
133
+ nil
134
+ end
135
+
136
+ def increment_job_queue(jid)
137
+ assert_batch_is_open
138
+ append_jobs([jid])
139
+ end
140
+
141
+ def invalidate_all
142
+ redis.setex("invalidated-bid-#{bid}", BID_EXPIRE_TTL, 1)
143
+ end
144
+
145
+ def parent_bid
146
+ redis.hget(@bidkey, "parent_bid")
147
+ end
148
+
149
+ def parent
150
+ if parent_bid
151
+ Batch.new(parent_bid)
152
+ end
153
+ end
154
+
155
+ def valid?(batch = self)
156
+ valid = !redis.exists?("invalidated-bid-#{batch.bid}")
157
+ batch.parent ? valid && valid?(batch.parent) : valid
158
+ end
159
+
160
+ def keep_open!
161
+ if block_given?
162
+ begin
163
+ keep_open!
164
+ yield
165
+ ensure
166
+ let_close!
167
+ end
168
+ else
169
+ redis.hset(@bidkey, 'keep_open', "true")
170
+ end
171
+ end
172
+
173
+ def let_close!
174
+ _, failed, pending, children, complete, success = redis.multi do |r|
175
+ r.hset(@bidkey, 'keep_open', "false")
176
+
177
+ r.scard("BID-#{bid}-failed")
178
+ r.hincrby("BID-#{bid}", "pending", 0)
179
+ r.hincrby("BID-#{bid}", "children", 0)
180
+ r.scard("BID-#{bid}-batches-complete")
181
+ r.scard("BID-#{bid}-batches-success")
182
+ end
183
+
184
+ all_success = pending.to_i.zero? && children == success
185
+ # if complete or successfull call complete callback (the complete callback may then call successful)
186
+ if (pending.to_i == failed.to_i && children == complete) || all_success
187
+ self.class.enqueue_callbacks(:complete, bid)
188
+ self.class.enqueue_callbacks(:success, bid) if all_success
189
+ end
190
+ end
191
+
192
+ def self.with_batch(batch)
193
+ batch = self.new(batch) if batch.is_a?(String)
194
+ parent = Thread.current[CURRENT_BATCH_THREAD_KEY]
195
+ Thread.current[CURRENT_BATCH_THREAD_KEY] = batch
196
+ yield
197
+ ensure
198
+ Thread.current[CURRENT_BATCH_THREAD_KEY] = parent
199
+ end
200
+
201
+ # Any Batches or Jobs created in the given block won't be assocaiated to the current batch
202
+ def self.without_batch(&blk)
203
+ with_batch(nil, &blk)
204
+ end
205
+
206
+ protected
207
+
208
+ def redis_key
209
+ @bidkey
210
+ end
211
+
212
+ def flush_pending_attrs
213
+ super
214
+ redis.zadd("batches", created_at, bid) if INDEX_ALL_BATCHES
215
+ end
216
+
217
+ private
218
+
219
+ def assert_batch_is_open
220
+ unless defined?(@closed)
221
+ @closed = redis.hget(@bidkey, 'success') == 'true'
222
+ end
223
+ raise "Cannot add jobs to Batch #{} bid - it has already entered the callback-stage" if @closed
224
+ end
225
+
226
+ def append_jobs(jids)
227
+ jids = jids.uniq
228
+ return unless jids.size > 0
229
+
230
+ redis do |r|
231
+ tme = Time.now.utc.to_f
232
+ added = r.zadd(@bidkey + "-jids", jids.map{|jid| [tme, jid] }, nx: true)
233
+ r.multi do |r|
234
+ r.hincrby(@bidkey, "pending", added)
235
+ r.hincrby(@bidkey, "job_count", added)
236
+ r.expire(@bidkey, BID_EXPIRE_TTL)
237
+ r.expire(@bidkey + "-jids", BID_EXPIRE_TTL)
238
+ end
239
+ end
240
+ end
241
+
242
+ class << self
243
+ def current
244
+ Thread.current[CURRENT_BATCH_THREAD_KEY]
245
+ end
246
+
247
+ def current_context
248
+ current&.context || {}
249
+ end
250
+
251
+ def process_failed_job(bid, jid)
252
+ _, pending, failed, children, complete, parent_bid = redis do |r|
253
+ return unless r.exists?("BID-#{bid}")
254
+
255
+ r.multi do |r|
256
+ r.sadd("BID-#{bid}-failed", jid)
257
+
258
+ r.hincrby("BID-#{bid}", "pending", 0)
259
+ r.scard("BID-#{bid}-failed")
260
+ r.hincrby("BID-#{bid}", "children", 0)
261
+ r.scard("BID-#{bid}-batches-complete")
262
+ r.hget("BID-#{bid}", "parent_bid")
263
+
264
+ r.expire("BID-#{bid}-failed", BID_EXPIRE_TTL)
265
+ end
266
+ end
267
+
268
+ if pending.to_i == failed.to_i && children == complete
269
+ enqueue_callbacks(:complete, bid)
270
+ end
271
+ end
272
+
273
+ # Dead jobs are a Sidekiq feature.
274
+ # If this is called for a job, process_failed_job was also called
275
+ def process_dead_job(bid, jid)
276
+ _, dead_count = redis do |r|
277
+ return unless r.exists?("BID-#{bid}")
278
+
279
+ r.multi do |r|
280
+ r.sadd("BID-#{bid}-dead", jid)
281
+ r.scard("BID-#{bid}-dead")
282
+ r.expire("BID-#{bid}-dead", BID_EXPIRE_TTL)
283
+ end
284
+ end
285
+
286
+ enqueue_callbacks(:death, bid)
287
+ end
288
+
289
+ def process_successful_job(bid, jid)
290
+ _, failed, pending, children, complete, success, parent_bid, keep_open = redis do |r|
291
+ return unless r.exists?("BID-#{bid}")
292
+
293
+ r.multi do |r|
294
+ r.srem("BID-#{bid}-failed", jid)
295
+
296
+ r.scard("BID-#{bid}-failed")
297
+ r.hincrby("BID-#{bid}", "pending", -1)
298
+ r.hincrby("BID-#{bid}", "children", 0)
299
+ r.scard("BID-#{bid}-batches-complete")
300
+ r.scard("BID-#{bid}-batches-success")
301
+ r.hget("BID-#{bid}", "parent_bid")
302
+ r.hget("BID-#{bid}", "keep_open")
303
+
304
+ r.hincrby("BID-#{bid}", "successful-jobs", 1)
305
+ r.zrem("BID-#{bid}-jids", jid)
306
+ r.expire("BID-#{bid}", BID_EXPIRE_TTL)
307
+ end
308
+ end
309
+
310
+ all_success = pending.to_i.zero? && children == success
311
+ # if complete or successfull call complete callback (the complete callback may then call successful)
312
+ if (pending.to_i == failed.to_i && children == complete) || all_success
313
+ enqueue_callbacks(:complete, bid)
314
+ enqueue_callbacks(:success, bid) if all_success
315
+ end
316
+ end
317
+
318
+ def enqueue_callbacks(event, bid)
319
+ batch_key = "BID-#{bid}"
320
+ callback_key = "#{batch_key}-callbacks-#{event}"
321
+
322
+ callbacks, queue, parent_bid, callback_params = redis do |r|
323
+ return unless r.exists?(batch_key)
324
+ return if r.hget(batch_key, 'keep_open') == 'true'
325
+
326
+ r.multi do |r|
327
+ r.smembers(callback_key)
328
+ r.hget(batch_key, "callback_queue")
329
+ r.hget(batch_key, "parent_bid")
330
+ r.hget(batch_key, "callback_params")
331
+ end
332
+ end
333
+
334
+ queue ||= "default"
335
+ parent_bid = !parent_bid || parent_bid.empty? ? nil : parent_bid # Basically parent_bid.blank?
336
+
337
+ # Internal callback params. If this is present, we're trying to enqueue callbacks for a callback, which is a special case that
338
+ # indicates that the callback completed and we need to close the triggering batch (which is in a done-but-not-cleaned state)
339
+ callback_params = JSON.parse(callback_params) if callback_params.present?
340
+
341
+ # User-configured parameters/arguments to pass to the callback
342
+ callback_args = callbacks.reduce([]) do |memo, jcb|
343
+ cb = JSON.load(jcb)
344
+ memo << [cb['callback'], event.to_s, cb['opts'], bid, parent_bid]
345
+ end
346
+
347
+ opts = {"bid" => bid, "event" => event}
348
+ should_schedule_batch = callback_args.present? && !callback_params.present?
349
+ already_processed = redis do |r|
350
+ SCHEDULE_CALLBACK.call(r, [batch_key], [event.to_s, should_schedule_batch.to_s, BID_EXPIRE_TTL])
351
+ end
352
+
353
+ return if already_processed == 'true'
354
+
355
+ if should_schedule_batch
356
+ logger.debug {"Enqueue callback bid: #{bid} event: #{event} args: #{callback_args.inspect}"}
357
+
358
+ # Create a new Batch to handle the callbacks and add it to the _parent_ batch
359
+ # (this ensures that the parent's lifecycle status can't change until the child's callbacks are done)
360
+ with_batch(parent_bid) do
361
+ cb_batch = self.new
362
+ cb_batch.callback_params = {
363
+ for_bid: bid,
364
+ event: event,
365
+ }
366
+ opts['callback_bid'] = cb_batch.bid
367
+
368
+ logger.debug {"Adding callback batch: #{cb_batch.bid} for batch: #{bid}"}
369
+ cb_batch.jobs do
370
+ push_callbacks(callback_args, queue)
371
+ end
372
+ end
373
+ end
374
+
375
+ if callback_params.present?
376
+ # This is a callback for a callback. Passing `origin` to the Finalizer allows it to also cleanup the original/callback-triggering batch
377
+ opts['origin'] = callback_params
378
+ end
379
+
380
+ # The Finalizer marks this batch as complete, bumps any necessary counters, cleans up this Batch _if_ no callbacks were scheduled,
381
+ # and enqueues parent-Batch callbacks if needed.
382
+ logger.debug {"Run batch finalizer bid: #{bid} event: #{event} args: #{callback_args.inspect}"}
383
+ finalizer = Batch::Callback::Finalize.new
384
+ status = Status.new bid
385
+ finalizer.dispatch(status, opts)
386
+ end
387
+
388
+ def cleanup_redis(bid)
389
+ logger.debug {"Cleaning redis of batch #{bid}"}
390
+ redis do |r|
391
+ r.zrem("batches", bid)
392
+ r.zrem("BID-ROOT-bids", bid)
393
+ r.unlink(
394
+ "BID-#{bid}",
395
+ "BID-#{bid}-callbacks-complete",
396
+ "BID-#{bid}-callbacks-success",
397
+ "BID-#{bid}-failed",
398
+ "BID-#{bid}-dead",
399
+
400
+ "BID-#{bid}-batches-success",
401
+ "BID-#{bid}-batches-complete",
402
+ "BID-#{bid}-batches-failed",
403
+ "BID-#{bid}-bids",
404
+ "BID-#{bid}-jids",
405
+ "BID-#{bid}-pending_callbacks",
406
+ )
407
+ end
408
+ end
409
+
410
+ def delete_prematurely!(bid)
411
+ child_bids = redis do |r|
412
+ r.zrange("BID-#{bid}-bids", 0, -1)
413
+ end
414
+ child_bids.each do |cbid|
415
+ delete_prematurely!(cbid)
416
+ end
417
+ cleanup_redis(bid)
418
+ end
419
+
420
+ # Internal method to cleanup a Redis Hash and related keys
421
+ def cleanup_redis_index_for(key, suffixes = [""])
422
+ if r.hget(k, "created_at").present?
423
+ r.multi do |r|
424
+ suffixes.each do |suffix|
425
+ r.expire(key + suffix, BID_EXPIRE_TTL)
426
+ end
427
+ end
428
+ false
429
+ else
430
+ r.multi do |r|
431
+ suffixes.each do |suffix|
432
+ r.unlink(key + suffix)
433
+ end
434
+ end
435
+ true
436
+ end
437
+ end
438
+
439
+ # Administrative/console method to cleanup expired batches from the WebUI
440
+ def cleanup_redis_index!
441
+ suffixes = ["", "-callbacks-complete", "-callbacks-success", "-failed", "-dead", "-batches-success", "-batches-complete", "-batches-failed", "-bids", "-jids", "-pending_callbacks"]
442
+
443
+ cleanup_index = ->(index) {
444
+ r.zrangebyscore(index, "0", BID_EXPIRE_TTL.seconds.ago.to_i).each do |bid|
445
+ r.zrem(index, bid) if cleanup_redis_index_for("BID-#{bid}", suffixes)
446
+ end
447
+ }
448
+
449
+ cleanup_index.call("BID-ROOT-bids")
450
+ cleanup_index.call("batches")
451
+ end
452
+
453
+ def redis(&blk)
454
+ return RedisProxy.new unless block_given?
455
+
456
+ if Thread.current[:job_batches_redis]
457
+ yield Thread.current[:job_batches_redis]
458
+ else
459
+ ::Bearcat.redis do |r|
460
+ Thread.current[:job_batches_redis] = r
461
+ yield r
462
+ ensure
463
+ Thread.current[:job_batches_redis] = nil
464
+ end
465
+ end
466
+ end
467
+
468
+ def logger
469
+ ::CanvasSync.logger
470
+ end
471
+
472
+ def push_callbacks(args, queue)
473
+ Batch::Callback::worker_class.enqueue_all(args, queue)
474
+ end
475
+
476
+ def bid_hierarchy(bid, depth: 4, per_depth: 5, slice: nil)
477
+ args = [bid, depth, per_depth]
478
+ args << slice if slice
479
+ redis do |r|
480
+ BID_HIERARCHY.call(r, [], args)
481
+ end
482
+ end
483
+ end
484
+
485
+ class RedisProxy
486
+ def multi(*args, &block)
487
+ Batch.redis do |r|
488
+ r.multi(*args) do |r|
489
+ block.call(r)
490
+ end
491
+ end
492
+ end
493
+
494
+ def pipelined(*args, &block)
495
+ Batch.redis do |r|
496
+ r.pipelined(*args) do |r2|
497
+ block.call(r2 || r)
498
+ end
499
+ end
500
+ end
501
+
502
+ def uget(key)
503
+ Batch.redis do |r|
504
+ case r.type(key)
505
+ when 'string'
506
+ r.get(key)
507
+ when 'list'
508
+ r.lrange(key, 0, -1)
509
+ when 'hash'
510
+ r.hgetall(key)
511
+ when 'set'
512
+ r.smembers(key)
513
+ when 'zset'
514
+ r.zrange(key, 0, -1)
515
+ end
516
+ end
517
+ end
518
+
519
+ def method_missing(method_name, *arguments, &block)
520
+ Batch.redis do |r|
521
+ r.send(method_name, *arguments, &block)
522
+ end
523
+ end
524
+
525
+ def respond_to_missing?(method_name, include_private = false)
526
+ super || Redis.method_defined?(method_name)
527
+ end
528
+ end
529
+ end
530
+ end
531
+ end
532
+
533
+ # Automatically integrate with Sidekiq if it is present.
534
+ if defined?(::Sidekiq)
535
+ require_relative './sidekiq'
536
+ CanvasSync::JobBatches::Sidekiq.configure
537
+ end
538
+
539
+ # Automatically integrate with ActiveJob if it is present.
540
+ if defined?(::ActiveJob)
541
+ require_relative './active_job'
542
+ CanvasSync::JobBatches::ActiveJob.configure
543
+ end
@@ -0,0 +1,149 @@
1
+ module CanvasSync
2
+ module JobBatches
3
+ class Batch
4
+ module Callback
5
+ mattr_accessor :worker_class
6
+
7
+ VALID_CALLBACKS = %w[success complete death].freeze
8
+
9
+ module CallbackWorkerCommon
10
+ def perform(definition, event, opts, bid, parent_bid)
11
+ return unless VALID_CALLBACKS.include?(event)
12
+
13
+ method = nil
14
+ target = :instance
15
+ clazz = definition
16
+ if clazz.is_a?(String)
17
+ if clazz.include?('#')
18
+ clazz, method = clazz.split("#")
19
+ elsif clazz.include?('.')
20
+ clazz, method = clazz.split(".")
21
+ target = :class
22
+ end
23
+ end
24
+
25
+ method ||= "on_#{event}"
26
+ status = Batch::Status.new(bid)
27
+
28
+ if clazz && object = Object.const_get(clazz)
29
+ target = target == :instance ? object.new : object
30
+ if target.respond_to?(method, true)
31
+ target.send(method, status, opts)
32
+ else
33
+ Batch.logger.warn("Invalid callback method #{definition} - #{target.to_s} does not respond to #{method}")
34
+ end
35
+ else
36
+ Batch.logger.warn("Invalid callback method #{definition} - Class #{clazz} not found")
37
+ end
38
+ end
39
+ end
40
+
41
+ class Finalize
42
+ def dispatch(status, opts)
43
+ bid = opts["bid"]
44
+ event = opts["event"].to_sym
45
+
46
+ Batch.logger.debug {"Finalize #{event} batch id: #{opts["bid"]}"}
47
+
48
+ batch_status = Status.new bid
49
+ send(event, bid, batch_status, batch_status.parent_bid)
50
+
51
+ Batch.redis do |r|
52
+ r.srem("BID-#{bid}-pending_callbacks", "#{event}-finalize")
53
+ end
54
+
55
+ if event == :success
56
+ if opts['origin'].present?
57
+ # This is a callback for a callback. In this case we need to check if we should cleanup the original bid.
58
+ origin_bid = opts['origin']['for_bid']
59
+ _, pending, success_ran = Batch.redis do |r|
60
+ r.multi do |r|
61
+ r.srem("BID-#{origin_bid}-pending_callbacks", opts['origin']['event'])
62
+ r.scard("BID-#{origin_bid}-pending_callbacks")
63
+ r.hget("BID-#{origin_bid}", "success")
64
+ end
65
+ end
66
+ Batch.cleanup_redis(origin_bid) if pending == 0 && success_ran == 'true'
67
+ end
68
+
69
+ if (Batch.redis {|r| r.scard("BID-#{bid}-pending_callbacks") }) == 0
70
+ Batch.cleanup_redis(bid)
71
+ end
72
+ end
73
+ end
74
+
75
+ def success(bid, status, parent_bid)
76
+ return unless parent_bid
77
+
78
+ _, _, success, _, _, complete, pending, children, success, failure = Batch.redis do |r|
79
+ r.multi do |r|
80
+ r.sadd("BID-#{parent_bid}-batches-success", bid)
81
+ r.expire("BID-#{parent_bid}-batches-success", Batch::BID_EXPIRE_TTL)
82
+ r.scard("BID-#{parent_bid}-batches-success")
83
+
84
+ r.srem("BID-#{parent_bid}-batches-failed", bid)
85
+ r.sadd("BID-#{parent_bid}-batches-complete", bid)
86
+ r.scard("BID-#{parent_bid}-batches-complete")
87
+
88
+ r.hincrby("BID-#{parent_bid}", "pending", 0)
89
+ r.hincrby("BID-#{parent_bid}", "children", 0)
90
+ r.scard("BID-#{parent_bid}-batches-success")
91
+ r.scard("BID-#{parent_bid}-failed")
92
+ end
93
+ end
94
+
95
+ if complete == children && pending == failure
96
+ Batch.logger.debug {"Finalize parent complete bid: #{parent_bid}"}
97
+ Batch.enqueue_callbacks(:complete, parent_bid)
98
+ end
99
+ if pending.to_i.zero? && children == success
100
+ Batch.logger.debug {"Finalize parent success bid: #{parent_bid}"}
101
+ Batch.enqueue_callbacks(:success, parent_bid)
102
+ end
103
+ end
104
+
105
+ def complete(bid, status, parent_bid)
106
+ pending, children, success = Batch.redis do |r|
107
+ r.multi do |r|
108
+ r.hincrby("BID-#{bid}", "pending", 0)
109
+ r.hincrby("BID-#{bid}", "children", 0)
110
+ r.scard("BID-#{bid}-batches-success")
111
+ end
112
+ end
113
+
114
+ if parent_bid && !(pending.to_i.zero? && children == success)
115
+ # If batch was not successfull check and see if its parent is complete
116
+ # if the parent is complete we trigger its complete callback.
117
+ #
118
+ # Otherwise, we don't want to to trigger the parent's :complete here (and
119
+ # instead opt to have success tigger parent :complete) - this
120
+ # allows the success callback to add additional jobs to the parent batch
121
+ # before triggering :complete.
122
+
123
+ Batch.logger.debug {"Finalize parent complete bid: #{parent_bid}"}
124
+ _, _, complete, pending, children, failure = Batch.redis do |r|
125
+ r.multi do |r|
126
+ r.sadd("BID-#{parent_bid}-batches-complete", bid)
127
+ r.sadd("BID-#{parent_bid}-batches-failed", bid)
128
+ r.scard("BID-#{parent_bid}-batches-complete")
129
+ r.hincrby("BID-#{parent_bid}", "pending", 0)
130
+ r.hincrby("BID-#{parent_bid}", "children", 0)
131
+ r.scard("BID-#{parent_bid}-failed")
132
+ end
133
+ end
134
+ if complete == children && pending == failure
135
+ Batch.enqueue_callbacks(:complete, parent_bid)
136
+ end
137
+ end
138
+ end
139
+
140
+ def death(bid, status, parent_bid)
141
+ return unless parent_bid
142
+
143
+ Batch.enqueue_callbacks(:death, parent_bid)
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end