{"record":{"id":"0b41f128ac430d37","repo":"instructure/canvas-lms","slug":"user-group-count-mismatch","errorCode":null,"errorMessage":"user/group count mismatch","messagePattern":"user/group count mismatch","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/models/group_category.rb","lineNumber":724,"sourceCode":"          leftover_sec_id = group_counts.max_by { |k, count| [-1 * (extra_groups[k] || 0), (user_counts[k].to_f / (count + 1)), k] }.first\n          group_counts[leftover_sec_id] += 1\n          extra_groups[leftover_sec_id] ||= 0\n          extra_groups[leftover_sec_id] += 1\n          num_groups_assigned += 1\n        end\n      end\n\n      @group_distributions = {}\n      group_counts.each do |section_id, num_groups|\n        # turn them into an array of group sizes, e.g. 7 users into 3 groups becomes [3, 2, 2]\n        dist = [user_counts[section_id] / num_groups] * num_groups # base\n        (user_counts[section_id] % num_groups).times do |idx| # distribute remainder around\n          dist[idx % num_groups] += 1\n        end\n        @group_distributions[section_id] = dist\n      end\n      if @group_distributions.values.sum(&:count) != @groups.count || @group_distributions.any? { |k, v| v.sum != user_counts[k] }\n        raise \"user/group count mismatch\" # we should make sure this works before going any further\n      end\n\n      @group_distributions\n    end\n\n    def assign_students_to_groups\n      @group_distributions.each do |section_id, group_sizes|\n        @users_by_section_id[section_id].shuffle!\n        group_sizes.each do |group_size|\n          group = @groups.pop\n          group.bulk_add_users_to_group(@users_by_section_id[section_id].pop(group_size))\n        end\n      end\n    end\n  end\n\n  def clear_permissions_cache_for_selfsignup\n    return unless %i[self_signup self_signup_end_at].any? { |k| saved_changes.key?(k) } # Skip if neither setting was changed","sourceCodeStart":706,"sourceCodeEnd":742,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/group_category.rb#L706-L742","documentation":"GroupCategory#determine_group_distribution assigns unassigned members across the groups it created, then asserts an internal invariant: the per-group counts must total exactly the number of groups and each section's distribution must sum to that section's user count. If the arithmetic doesn't line up, it raises this RuntimeError to abort before groups are actually populated, since a bad distribution would silently lose or duplicate memberships.","triggerScenarios":"Calling group_category.determine_group_distribution (directly or via assign_unassigned_members) when @groups was built from a different set of sections/users than user_counts, or when group creation partially failed so @groups.count doesn't match the computed distribution slots.","commonSituations":"Course sections changed between user-count snapshot and group creation; a plugin or customization overriding group creation; self-signup groups where some groups were deleted mid-distribution; large imports creating users while distribution runs.","solutions":["Verify the group category's groups.count matches the number of distribution slots the algorithm computed for the current section user counts","Recompute in a transaction: build groups and distribute in one pass so user_counts and @groups come from the same data snapshot","Check for customizations/overrides to create_groups or distribute_students that change @groups after distribution is computed","Reproduce locally with the same section/user data and log user_counts, @group_distributions, and @groups.count to find which side of the invariant is off"],"exampleFix":"// before\ncategory.determine_group_distribution\n// after\n# ensure groups exist for every section before distribution\ncategory.groups.reload\nraise 'run create_groups first' if category.groups.empty?\ndist = category.determine_group_distribution","handlingStrategy":"validation","validationCode":"users = category.unassigned_users_by_section\ngroups_count = category.groups.count\nexpected = users.values.sum\nraise 'group count mismatch before distribution' unless groups_count == expected_total_slots(users)\n# only call determine_group_distribution when counts line up","typeGuard":null,"tryCatchPattern":"begin\n  category.determine_group_distribution\nrescue RuntimeError => e\n  raise e unless e.message == 'user/group count mismatch'\n  Rails.logger.error('group distribution invariant failed, rebuilding groups')\n  category.reset_and_rebuild_groups!\nend","preventionTips":["Build groups and compute distributions in one transaction against the same user snapshot","Reload the association before distribution so group counts are current","Add tests covering multi-section courses with remainder users"],"tags":["ruby","groups","invariant","canvas"],"backgroundTag":"internal-invariant-violation","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"}