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,245 @@
1
+ module CanvasSync
2
+ module JobBatches
3
+ class Pool
4
+ include RedisModel
5
+
6
+ POOL_REFILL = RedisScript.new(Pathname.new(__FILE__) + "../pool_refill.lua")
7
+
8
+ attr_reader :pid
9
+ redis_attr :description
10
+ redis_attr :created_at
11
+ redis_attr :concurrency, :int
12
+ redis_attr :complete_count, :int
13
+ redis_attr :order
14
+ redis_attr :on_failed_job, :symbol
15
+ redis_attr :clean_when_empty, :bool
16
+
17
+ def initialize(pooolid = nil, **kwargs)
18
+ if pooolid
19
+ @existing = true
20
+ @pid = pooolid
21
+ else
22
+ @pid = SecureRandom.urlsafe_base64(10)
23
+ initialize_new(**kwargs)
24
+ end
25
+ end
26
+
27
+ def self.from_pid(pid)
28
+ raise "PID must be given" unless pid.present?
29
+ new(pid)
30
+ end
31
+
32
+ def <<(job_desc)
33
+ add_job(job_desc)
34
+ end
35
+
36
+ def add_job(job_desc)
37
+ add_jobs([job_desc])
38
+ end
39
+
40
+ def add_jobs(job_descs, skip_refill: false)
41
+ job_descs.each do |job_desc|
42
+ wrapper = Batch.new
43
+ wrapper.description = "Pool Job Wrapper (PID: #{pid})"
44
+ checkin_event = (on_failed_job == :wait) ? :success : :complete
45
+ wrapper.on(checkin_event, "#{self.class.to_s}.job_checked_in", pool_id: pid)
46
+ wrapper.jobs {}
47
+
48
+ job_desc = job_desc.symbolize_keys
49
+ job_desc = job_desc.merge!(
50
+ job: job_desc[:job].to_s,
51
+ pool_wrapper_batch: wrapper.bid,
52
+ )
53
+
54
+ push_job_to_pool(job_desc)
55
+ end
56
+ refill_allotment unless skip_refill
57
+ end
58
+
59
+ def keep_open!
60
+ if block_given?
61
+ begin
62
+ keep_open!
63
+ yield
64
+ ensure
65
+ let_close!
66
+ end
67
+ else
68
+ redis.hset(redis_key, 'keep_open', 'true')
69
+ end
70
+ end
71
+
72
+ def let_close!
73
+ redis.hset(redis_key, 'keep_open', 'false')
74
+ cleanup_if_empty
75
+ end
76
+
77
+ def cleanup_redis
78
+ Batch.logger.debug {"Cleaning redis of pool #{pid}"}
79
+ redis do |r|
80
+ r.zrem("pools", pid)
81
+ r.unlink(
82
+ "#{redis_key}",
83
+ "#{redis_key}-jobs",
84
+ )
85
+ end
86
+ end
87
+
88
+ def cleanup_if_empty
89
+ self.order
90
+
91
+ activec, pactivec, pendingc, clean_when_empty, keep_open = redis.multi do |r|
92
+ r.hlen("#{redis_key}-active")
93
+ r.hget(redis_key, "_active_count")
94
+ pending_count(r)
95
+ r.hget(redis_key, 'clean_when_empty')
96
+ r.hget(redis_key, 'keep_open')
97
+ end
98
+
99
+ return if keep_open == 'true' || clean_when_empty == 'false'
100
+
101
+ if activec <= 0 && (pactivec.try(:to_i) || 0) <= 0 && pendingc <= 0
102
+ cleanup_redis
103
+ end
104
+ end
105
+
106
+ def active_count(r = redis)
107
+ r.hlen("#{redis_key}-active") + r.hincrby(redis_key, "_active_count", 0)
108
+ end
109
+
110
+ def active_jobs(r = redis)
111
+ r.hvals("#{redis_key}-active").map {|desc| JSON.parse(desc)[0] }
112
+ end
113
+
114
+ def pending_count(r = redis)
115
+ jobs_key = "#{redis_key}-jobs"
116
+ order = self.order || 'fifo'
117
+ case order.to_sym
118
+ when :fifo, :lifo
119
+ r.llen(jobs_key)
120
+ when :random
121
+ r.scard(jobs_key)
122
+ when :priority
123
+ r.zcard(jobs_key)
124
+ end
125
+ end
126
+
127
+ def job_checked_in(status, options)
128
+ active_count = refill_allotment(status.bid)
129
+ cleanup_if_empty unless active_count > 0
130
+ end
131
+
132
+ def self.job_checked_in(status, options)
133
+ pid = options['pool_id']
134
+ from_pid(pid).job_checked_in(status, options)
135
+ end
136
+
137
+ # Administrative/console method to cleanup expired pools from the WebUI
138
+ def self.cleanup_redis_index!
139
+ suffixes = ["", "-active", "-jobs"]
140
+ r.zrangebyscore("pools", "0", Batch::BID_EXPIRE_TTL.seconds.ago.to_i).each do |pid|
141
+ r.zrem("pools", pid) if Batch.cleanup_redis_index_for("POOLID-#{pid}", suffixes)
142
+ end
143
+ end
144
+
145
+ protected
146
+
147
+ def redis_key
148
+ "POOLID-#{pid}"
149
+ end
150
+
151
+ def refill_allotment(checkin_bid = nil)
152
+ active_count, job_descs = POOL_REFILL.call(redis, [redis_key, "#{redis_key}-jobs", "#{redis_key}-active"], [checkin_bid || ""])
153
+ return active_count if active_count < 0
154
+
155
+ pending_job_descs = job_descs.dup
156
+
157
+ added_jobs = {}
158
+ failed_to_add_jobs = []
159
+ add_exception = nil
160
+
161
+ while pending_job_descs.count > 0
162
+ begin
163
+ job_json = pending_job_descs.shift
164
+ job_desc = ::ActiveJob::Arguments.deserialize(JSON.parse(job_json))[0]&.symbolize_keys
165
+
166
+ wbid = job_desc[:pool_wrapper_batch]
167
+
168
+ Batch.new(wbid).jobs do
169
+ ChainBuilder.enqueue_job(job_desc)
170
+ end
171
+
172
+ added_jobs[wbid] = job_json
173
+ rescue => ex
174
+ failed_to_add_jobs << job_json
175
+ add_exception = ex
176
+ end
177
+ end
178
+
179
+ redis.multi do |r|
180
+ r.mapped_hmset("#{redis_key}-active", added_jobs) if added_jobs.count > 0
181
+ # Release reserved slots now that we've added the jobs to `-active`
182
+ r.hincrby(redis_key, "_active_count", -job_descs.count)
183
+
184
+ r.expire(redis_key, Batch::BID_EXPIRE_TTL)
185
+ r.expire("#{redis_key}-active", Batch::BID_EXPIRE_TTL)
186
+ r.expire("#{redis_key}-jobs", Batch::BID_EXPIRE_TTL)
187
+ end
188
+
189
+ # If this happens, we end up in a bad state (as we don't try to re-add items to the pool or refill_allotment again), but
190
+ # this should be a _really_ rare case that should only occur if we've lost connection to Redis or something, so we're
191
+ # operating on the assumption that if we get here, any recovery logic will fail too
192
+ if add_exception.present?
193
+ Batch.logger.error {"Error popping jobs from Pool #{pid}: #{add_exception}"}
194
+ raise add_exception
195
+ end
196
+
197
+ active_count + added_jobs.count
198
+ end
199
+
200
+ def push_job_to_pool(job_desc)
201
+ jobs_key = "#{redis_key}-jobs"
202
+ # This allows duplicate jobs when a Redis Set is used
203
+ job_desc[:_pool_random_key_] = SecureRandom.urlsafe_base64(10)
204
+ job_json = JSON.unparse(::ActiveJob::Arguments.serialize([job_desc]))
205
+ order = self.order
206
+
207
+ redis.multi do |r|
208
+ case order.to_sym
209
+ when :fifo, :lifo
210
+ r.rpush(jobs_key, job_json)
211
+ when :random
212
+ r.sadd(jobs_key, job_json)
213
+ when :priority
214
+ r.zadd(jobs_key, job_desc[:priority] || 0, job_json)
215
+ end
216
+ r.expire(redis_key, Batch::BID_EXPIRE_TTL)
217
+ r.expire(jobs_key, Batch::BID_EXPIRE_TTL)
218
+ end
219
+ end
220
+
221
+ def self.redis(&blk)
222
+ Batch.redis &blk
223
+ end
224
+ delegate :redis, to: :class
225
+
226
+ def flush_pending_attrs
227
+ super
228
+ redis.expire(redis_key, Batch::BID_EXPIRE_TTL)
229
+ redis.zadd("pools", created_at, pid)
230
+ end
231
+
232
+ private
233
+
234
+ def initialize_new(concurrency: nil, order: :fifo, clean_when_empty: true, on_failed_job: :wait, description: nil)
235
+ self.created_at = Time.now.utc.to_f
236
+ self.description = description
237
+ self.order = order
238
+ self.concurrency = concurrency
239
+ self.clean_when_empty = clean_when_empty
240
+ self.on_failed_job = on_failed_job
241
+ flush_pending_attrs
242
+ end
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,47 @@
1
+
2
+ local poolkey = KEYS[1]
3
+ local qkey = KEYS[2]
4
+ local activekey = KEYS[3]
5
+
6
+ local checkin_item = ARGV[1]
7
+
8
+ if redis.call('EXISTS', poolkey) == 0 then
9
+ return { -1, {} } -- pool doesn't exist
10
+ end
11
+
12
+ if checkin_item ~= "" then
13
+ redis.call("HDEL", activekey, checkin_item)
14
+ redis.call("HINCRBY", poolkey, "complete_count", 1)
15
+ end
16
+
17
+ local pool_type = redis.call('HGET', poolkey, "order")
18
+ local allotment = tonumber(redis.call("HGET", poolkey, "concurrency"))
19
+ local active = redis.call("HLEN", activekey) + (redis.call("HGET", poolkey, "_active_count") or 0)
20
+
21
+ local pop_count = allotment - active
22
+
23
+ local popped_items = {}
24
+
25
+ if pop_count > 0 then
26
+ if pool_type == "fifo" then
27
+ popped_items = redis.call("LPOP", qkey, pop_count) or {}
28
+ elseif pool_type == "lifo" then
29
+ popped_items = redis.call("RPOP", qkey, pop_count) or {}
30
+ elseif pool_type == "random" then
31
+ popped_items = redis.call("SPOP", qkey, pop_count) or {}
32
+ elseif pool_type == "priority" then
33
+ local temp_items = redis.call("ZPOPMAX", qkey, pop_count) or {}
34
+ for i,v in ipairs(temp_items) do
35
+ if i % 2 == 1 then
36
+ table.insert(popped_items, v)
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ -- Reserve slots for these jobs while we return to Ruby and deserialize them
43
+ -- This could also be inlined by just storing a key in the queue and storing parameters
44
+ -- in a Hash, but this seems more efficient.
45
+ redis.call('HINCRBY', poolkey, "_active_count", #popped_items)
46
+
47
+ return { active, popped_items }
@@ -0,0 +1,69 @@
1
+ module CanvasSync
2
+ module JobBatches
3
+ module RedisModel
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ def redis_attr(key, type = :string, read_only: true)
8
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
9
+ def #{key}=(value)
10
+ raise "#{key} is read-only once the batch has been started" if #{read_only.to_s} && (@initialized || @existing)
11
+ @#{key} = value
12
+ if :#{type} == :json
13
+ value = JSON.unparse(value)
14
+ end
15
+ persist_bid_attr('#{key}', value)
16
+ end
17
+
18
+ def #{key}
19
+ return @#{key} if defined?(@#{key})
20
+ if (@initialized || @existing)
21
+ value = read_bid_attr('#{key}')
22
+ if :#{type} == :bool
23
+ value = value == 'true'
24
+ elsif :#{type} == :int
25
+ value = value.to_i
26
+ elsif :#{type} == :float
27
+ value = value.to_f
28
+ elsif :#{type} == :json
29
+ value = JSON.parse(value)
30
+ elsif :#{type} == :symbol
31
+ value = value&.to_sym
32
+ end
33
+ @#{key} = value
34
+ end
35
+ end
36
+ RUBY
37
+ end
38
+ end
39
+
40
+ def persist_bid_attr(attribute, value)
41
+ if @initialized || @existing
42
+ redis do |r|
43
+ r.multi do |r|
44
+ r.hset(redis_key, attribute, value.to_s)
45
+ r.expire(redis_key, Batch::BID_EXPIRE_TTL)
46
+ end
47
+ end
48
+ else
49
+ @pending_attrs ||= {}
50
+ @pending_attrs[attribute] = value.to_s
51
+ end
52
+ end
53
+
54
+ def read_bid_attr(attribute)
55
+ redis do |r|
56
+ r.hget(redis_key, attribute)
57
+ end
58
+ end
59
+
60
+ def flush_pending_attrs
61
+ redis do |r|
62
+ r.mapped_hmset(redis_key, @pending_attrs)
63
+ end
64
+ @initialized = true
65
+ @pending_attrs = {}
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,163 @@
1
+ require 'pathname'
2
+ require 'digest/sha1'
3
+ require 'erb'
4
+
5
+ # Modified from https://github.com/Shopify/wolverine/blob/master/lib/wolverine/script.rb
6
+
7
+ module CanvasSync
8
+ module JobBatches
9
+ # {RedisScript} represents a lua script in the filesystem. It loads the script
10
+ # from disk and handles talking to redis to execute it. Error handling
11
+ # is handled by {LuaError}.
12
+ class RedisScript
13
+
14
+ # Loads the script file from disk and calculates its +SHA1+ sum.
15
+ #
16
+ # @param file [Pathname] the full path to the indicated file
17
+ def initialize(file)
18
+ @file = Pathname.new(file)
19
+ end
20
+
21
+ # Passes the script and supplied arguments to redis for evaulation.
22
+ # It first attempts to use a script redis has already cached by using
23
+ # the +EVALSHA+ command, but falls back to providing the full script
24
+ # text via +EVAL+ if redis has not seen this script before. Future
25
+ # invocations will then use +EVALSHA+ without erroring.
26
+ #
27
+ # @param redis [Redis] the redis connection to run against
28
+ # @param args [*Objects] the arguments to the script
29
+ # @return [Object] the value passed back by redis after script execution
30
+ # @raise [LuaError] if the script failed to compile of encountered a
31
+ # runtime error
32
+ def call(redis, *args)
33
+ t = Time.now
34
+ begin
35
+ redis.evalsha(digest, *args)
36
+ rescue => e
37
+ e.message =~ /NOSCRIPT/ ? redis.eval(content, *args) : raise
38
+ end
39
+ rescue => e
40
+ if LuaError.intercepts?(e)
41
+ raise LuaError.new(e, @file, content)
42
+ else
43
+ raise
44
+ end
45
+ end
46
+
47
+ def content
48
+ @content ||= load_lua(@file)
49
+ end
50
+
51
+ def digest
52
+ @digest ||= Digest::SHA1.hexdigest content
53
+ end
54
+
55
+ private
56
+
57
+ def script_path
58
+ Rails.root + 'app/redis_lua'
59
+ end
60
+
61
+ def relative_path
62
+ @path ||= @file.relative_path_from(script_path)
63
+ end
64
+
65
+ def load_lua(file)
66
+ TemplateContext.new(script_path).template(script_path + file)
67
+ end
68
+
69
+ class TemplateContext
70
+ def initialize(script_path)
71
+ @script_path = script_path
72
+ end
73
+
74
+ def template(pathname)
75
+ @partial_templates ||= {}
76
+ ERB.new(File.read(pathname)).result binding
77
+ end
78
+
79
+ # helper method to include a lua partial within another lua script
80
+ #
81
+ # @param relative_path [String] the relative path to the script from
82
+ # `script_path`
83
+ def include_partial(relative_path)
84
+ unless @partial_templates.has_key? relative_path
85
+ @partial_templates[relative_path] = nil
86
+ template( Pathname.new("#{@script_path}/#{relative_path}") )
87
+ end
88
+ end
89
+ end
90
+
91
+ # Reformats errors raised by redis representing failures while executing
92
+ # a lua script. The default errors have confusing messages and backtraces,
93
+ # and a type of +RuntimeError+. This class improves the message and
94
+ # modifies the backtrace to include the lua script itself in a reasonable
95
+ # way.
96
+ class LuaError < StandardError
97
+ PATTERN = /ERR Error (compiling|running) script \(.*?\): .*?:(\d+): (.*)/
98
+ WOLVERINE_LIB_PATH = File.expand_path('../../', __FILE__)
99
+ CONTEXT_LINE_NUMBER = 2
100
+
101
+ attr_reader :error, :file, :content
102
+
103
+ # Is this error one that should be reformatted?
104
+ #
105
+ # @param error [StandardError] the original error raised by redis
106
+ # @return [Boolean] is this an error that should be reformatted?
107
+ def self.intercepts? error
108
+ error.message =~ PATTERN
109
+ end
110
+
111
+ # Initialize a new {LuaError} from an existing redis error, adjusting
112
+ # the message and backtrace in the process.
113
+ #
114
+ # @param error [StandardError] the original error raised by redis
115
+ # @param file [Pathname] full path to the lua file the error ocurred in
116
+ # @param content [String] lua file content the error ocurred in
117
+ def initialize error, file, content
118
+ @error = error
119
+ @file = file
120
+ @content = content
121
+
122
+ @error.message =~ PATTERN
123
+ _stage, line_number, message = $1, $2, $3
124
+ error_context = generate_error_context(content, line_number.to_i)
125
+
126
+ super "#{message}\n\n#{error_context}\n\n"
127
+ set_backtrace generate_backtrace file, line_number
128
+ end
129
+
130
+ private
131
+
132
+ def generate_error_context(content, line_number)
133
+ lines = content.lines.to_a
134
+ beginning_line_number = [1, line_number - CONTEXT_LINE_NUMBER].max
135
+ ending_line_number = [lines.count, line_number + CONTEXT_LINE_NUMBER].min
136
+ line_number_width = ending_line_number.to_s.length
137
+
138
+ (beginning_line_number..ending_line_number).map do |number|
139
+ indicator = number == line_number ? '=>' : ' '
140
+ formatted_number = "%#{line_number_width}d" % number
141
+ " #{indicator} #{formatted_number}: #{lines[number - 1]}"
142
+ end.join.chomp
143
+ end
144
+
145
+ def generate_backtrace(file, line_number)
146
+ pre_wolverine = backtrace_before_entering_wolverine(@error.backtrace)
147
+ index_of_first_wolverine_line = (@error.backtrace.size - pre_wolverine.size - 1)
148
+ pre_wolverine.unshift(@error.backtrace[index_of_first_wolverine_line])
149
+ pre_wolverine.unshift("#{file}:#{line_number}")
150
+ pre_wolverine
151
+ end
152
+
153
+ def backtrace_before_entering_wolverine(backtrace)
154
+ backtrace.reverse.take_while { |line| ! line_from_wolverine(line) }.reverse
155
+ end
156
+
157
+ def line_from_wolverine(line)
158
+ line.split(':').first.include?(WOLVERINE_LIB_PATH)
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,14 @@
1
+
2
+ local previously_scheduled = redis.call('HGET', KEYS[1], ARGV[1])
3
+ redis.call('HSET', KEYS[1], ARGV[1], 'true')
4
+
5
+ if previously_scheduled ~= 'true' then
6
+ local pcb_key = KEYS[1] .. '-pending_callbacks'
7
+ redis.call('SADD', pcb_key, ARGV[1] .. '-finalize')
8
+ if ARGV[2] == 'true' then
9
+ redis.call('SADD', pcb_key, ARGV[1])
10
+ end
11
+ redis.call('EXPIRE', pcb_key, ARGV[3])
12
+ end
13
+
14
+ return previously_scheduled