instructure/canvas-lms · error · RuntimeError

`increment_completion!` can only be invoked after a total…

Error message

`increment_completion!` can only be invoked after a total has been set with `calculate_completion!`

What it means

Progress#increment_completion! requires that Progress#calculate_completion! has already run to initialize @total (and @current_value). Calling increment_completion! before a total exists would divide by zero/nil, so the model raises this guard error. It protects the completion-percentage bookkeeping invariant.

Solutions

  1. Call calculate_completion!(total, current_value) once at the start before any increment_completion! calls
  2. Check that the same Progress instance is used for both calls (don't reload between them)
  3. For persisted records, verify completion/total columns are set before incrementing (re-initialize if total is nil)
  4. Default to calculate_completion! in the initialization path of the background job that tracks progress

Example fix

// before
progress.increment_completion!
// after
progress.calculate_completion!(total_items, 0)
progress.increment_completion!
Defensive patterns

Strategy: validation

Validate before calling

raise 'progress total not initialized' if progress.instance_variable_get(:@total).nil?
# or for AR-backed records: initialize first
progress.calculate_completion!(total, 0) if progress.total.nil?

Type guard

can_increment = ->(p) { p.instance_variable_get(:@total).present? }

Try / catch

begin
  progress.increment_completion!
rescue RuntimeError => e
  Rails.logger.warn("#{e.message}; initializing progress")
  progress.calculate_completion!(total, 0)
  retry
end

Prevention

When it happens

Trigger: Calling progress.increment_completion!(n) on a Progress object/row where calculate_completion!(total, current_value) was never invoked (or was invoked on a different instance than the one reloaded from DB).

Common situations: Job or controller code increments progress on a freshly found Progress record without first calling calculate_completion!; long-running process caches a stale instance after reset; refactoring removed the initial calculate_completion! call.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

Thrown at app/models/progress.rb:101

  end

  def set_results(results)
    self.results = results
    save
  end

  def update_completion!(value)
    update_attribute(:completion, value)
  end

  def calculate_completion!(current_value, total)
    @total = total
    @current_value = current_value
    update_completion!(100.0 * @current_value / @total)
  end

  def increment_completion!(increment = 1)
    raise "`increment_completion!` can only be invoked after a total has been set with `calculate_completion!`" if @total.nil?

    @current_value += increment
    new_value = 100.0 * @current_value / @total
    # only update the db if we're at a different integral percentage point or it's been > 15s
    if new_value.to_i != completion.to_i || (Time.now.utc - updated_at) > 15
      update_completion!(new_value)
    else
      self.completion = new_value
    end
  end

  def pending?
    queued? || running? || waiting_for_external_tool?
  end

  # Tie this Progress model to a delayed job. Rather than `obj.delay.long_method`, use:
  # `progress.process_job(obj, :long_method)`. This will transition from queued
  # => running when the job starts, from running => completed when the job

View on GitHub (pinned to 1c9f0bb801)