{"record":{"id":"e61ef00be39f71bc","repo":"instructure/canvas-lms","slug":"cannot-call-with-more-than-1000-enrollments","errorCode":null,"errorMessage":"Cannot call with more than 1000 enrollments","messagePattern":"Cannot call with more than 1000 enrollments","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"app/models/enrollment/batch_state_updater.rb","lineNumber":49,"sourceCode":"  # after_save :copy_scores_from_existing_enrollment, if: :need_to_copy_scores? - not needed for destroy\n  # after_save :restore_submissions_and_scores - not needed for destroy\n  # after_save :clear_email_caches - handled in clear_email_caches\n  # after_save :cancel_future_appointments - handled in cancel_future_appointments\n  # after_save :update_linked_enrollments - handled in update_linked_enrollments\n  # after_save :set_update_cached_due_dates - handled in update_cached_due_dates\n  # after_save :touch_graders_if_needed - handled in touch_all_graders_if_needed\n  # after_save :reset_notifications_cache - handled in reset_notifications_cache\n  # after_save :dispatch_invitations_later - not needed for destroy\n  # after_save :add_to_favorites_later - not needed for destroy\n  # after_commit :update_cached_due_dates - handled in update_cached_due_dates\n  # after_save :update_assignment_overrides_if_needed - handled in update_assignment_overrides\n  # after_save :needs_grading_count_updated -handled in needs_grading_count_updated\n  # after_commit :sync_microsoft_group - handled in sync_microsoft_group\n  #\n  # ignore_due_date_caching_for: optional hash of (course_id => user_ids_array) of users to _not_\n  # re-cache their scores for (presumably the caller already queued one of their own)\n  def self.destroy_batch(batch, sis_batch: nil, batch_mode: false, ignore_due_date_caching_for: {})\n    raise ArgumentError, \"Cannot call with more than 1000 enrollments\" if batch.count > 1000\n\n    Enrollment.transaction do\n      # cache some data before the destroy that is needed after the destroy\n      @invited_user_ids = Enrollment.where(id: batch, workflow_state: \"invited\").distinct.pluck(:user_id)\n      @students = Enrollment.of_student_type.where(id: batch).preload({ user: :linked_observers }, :root_account).to_a\n      @students.each do |e|\n        e.workflow_state = \"deleted\"\n        e.readonly!\n      end\n      @user_course_tuples = Enrollment.where(id: batch).active.select(%i[user_id course_id]).distinct.to_a\n      @user_ids = Enrollment.where(id: batch).order(:user_id).distinct.pluck(:user_id)\n      @courses = Course.where(id: Enrollment.where(id: batch).select(:course_id).distinct).to_a\n      @root_account = @courses.first.root_account\n      @data = mark_enrollments_as_deleted(batch, sis_batch:, batch_mode:)\n      gms = remove_group_memberships(batch, @courses, @user_ids, sis_batch:, batch_mode:)\n      @data&.push(*gms) if gms\n      # touch users after removing group_memberships to invalidate the cache.\n      cancel_future_appointments(@courses, @user_ids)","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/enrollment/batch_state_updater.rb#L31-L67","documentation":"Enrollment::BatchStateUpdater.destroy_batch mass-deletes enrollments in one transaction, preloading users, observers and root accounts to fix up counters. The work is capped at 1000 enrollments to keep transactions and job payloads bounded; larger batches raise ArgumentError before any deletion.","triggerScenarios":"Calling destroy_batch(batch) where batch.count > 1000, e.g. deleting all enrollments of a large course or a big SIS batch-mode delete.","commonSituations":"Course merges/unpublishes, SIS imports deleting large enrollment sets, cleanup rake tasks passing unscoped relations.","solutions":["Partition the batch into chunks of <= 1000 and call destroy_batch per chunk.","Ensure batch is a proper relation/id scope so count is cheap.","For very large deletes, use a dedicated workflow (e.g. course conclude/delete) instead of one batch call."],"exampleFix":"// before\nEnrollment::BatchStateUpdater.destroy_batch(enrollments)\n// after\nenrollments.find_in_batches(batch_size: 1000) { |chunk| Enrollment::BatchStateUpdater.destroy_batch(chunk) }","handlingStrategy":"validation","validationCode":"enrollments.find_in_batches(batch_size: 1000) { |chunk| Enrollment::BatchStateUpdater.destroy_batch(chunk) }","typeGuard":null,"tryCatchPattern":"begin\n  Enrollment::BatchStateUpdater.destroy_batch(batch)\nrescue ArgumentError\n  batch.find_in_batches(batch_size: 1000) { |c| Enrollment::BatchStateUpdater.destroy_batch(c) }\nend","preventionTips":["Always chunk large enrollment sets to <= 1000","Check batch.count before bulk calls","Prefer id-based relations over loaded arrays for counting"],"tags":["batch","guard-clause","limit"],"backgroundTag":"argument-out-of-range","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}