instructure/canvas-lms · error

user/group count mismatch

Error message

user/group count mismatch

What it means

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.

Solutions

  1. Verify the group category's groups.count matches the number of distribution slots the algorithm computed for the current section user counts
  2. Recompute in a transaction: build groups and distribute in one pass so user_counts and @groups come from the same data snapshot
  3. Check for customizations/overrides to create_groups or distribute_students that change @groups after distribution is computed
  4. 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

Example fix

// before
category.determine_group_distribution
// after
# ensure groups exist for every section before distribution
category.groups.reload
raise 'run create_groups first' if category.groups.empty?
dist = category.determine_group_distribution
Defensive patterns

Strategy: validation

Validate before calling

users = category.unassigned_users_by_section
groups_count = category.groups.count
expected = users.values.sum
raise 'group count mismatch before distribution' unless groups_count == expected_total_slots(users)
# only call determine_group_distribution when counts line up

Try / catch

begin
  category.determine_group_distribution
rescue RuntimeError => e
  raise e unless e.message == 'user/group count mismatch'
  Rails.logger.error('group distribution invariant failed, rebuilding groups')
  category.reset_and_rebuild_groups!
end

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/0b41f128ac430d37. Report an issue: GitHub.

Appendix: source

Thrown at app/models/group_category.rb:724

          leftover_sec_id = group_counts.max_by { |k, count| [-1 * (extra_groups[k] || 0), (user_counts[k].to_f / (count + 1)), k] }.first
          group_counts[leftover_sec_id] += 1
          extra_groups[leftover_sec_id] ||= 0
          extra_groups[leftover_sec_id] += 1
          num_groups_assigned += 1
        end
      end

      @group_distributions = {}
      group_counts.each do |section_id, num_groups|
        # turn them into an array of group sizes, e.g. 7 users into 3 groups becomes [3, 2, 2]
        dist = [user_counts[section_id] / num_groups] * num_groups # base
        (user_counts[section_id] % num_groups).times do |idx| # distribute remainder around
          dist[idx % num_groups] += 1
        end
        @group_distributions[section_id] = dist
      end
      if @group_distributions.values.sum(&:count) != @groups.count || @group_distributions.any? { |k, v| v.sum != user_counts[k] }
        raise "user/group count mismatch" # we should make sure this works before going any further
      end

      @group_distributions
    end

    def assign_students_to_groups
      @group_distributions.each do |section_id, group_sizes|
        @users_by_section_id[section_id].shuffle!
        group_sizes.each do |group_size|
          group = @groups.pop
          group.bulk_add_users_to_group(@users_by_section_id[section_id].pop(group_size))
        end
      end
    end
  end

  def clear_permissions_cache_for_selfsignup
    return unless %i[self_signup self_signup_end_at].any? { |k| saved_changes.key?(k) } # Skip if neither setting was changed

View on GitHub (pinned to 1c9f0bb801)