Shopify / Shopify/tapioca

Tapioca::Dsl::Compilers::ActiveJob missing Exception as a possible return type in perform_now type compiled signature

Open
#2,353 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Ruby
Stars
873
Forks
164
Avg merge
4d 27m
Merged PRs (30d)
9

Description

Description

Tapioca's compiled type signature for ActiveJob::Base.perform_now essentially uses the same as ActiveJob::Base#perform. However, this is not fully representative of the behaviour because .perform_now will return the exception that got raised inside #perform when that exception has been rescued with rescue_from or retried with retry_on. Other scenarios are possible, these are just the 2 scenarios I tested.

To illustrate this point, consider the following job definition:

# failing_job_with_rescue_from_noop.rb

# typed: strict
# frozen_string_literal: true

class FailingJobWithRescueFromNoop < ActiveJob::Base CustomCops/EssentialsApplicationJob,Rails/ApplicationJob
  rescue_from(StandardError) { p "error!" }

  sig { void }
  def perform
    raise "FailingJobWithRescueFromNoop"
  end
end

Actual behaviour

Running bin/tapioca dsl FailingJobWithRescueFromNoop produces the following RBI. Note the type signature for .perform_now:

# failing_job_with_rescue_from_noop.rbi

# typed: true

# DO NOT EDIT MANUALLY
# This is an autogenerated file for dynamic methods in `FailingJobWithRescueFromNoop`.
# Please instead update this file by running `bin/tapioca dsl FailingJobWithRescueFromNoop`.


class FailingJobWithRescueFromNoop
  class << self
    sig { params(block: T.nilable(T.proc.params(job: FailingJobWithRescueFromNoop).void)).returns(T.any(FailingJobWithRescueFromNoop, FalseClass)) }
    def perform_later(&block); end

    sig { void }
    def perform_now; end
  end
end

However, when one runs the following in a rails console:

> result = FailingJobWithRescueFromNoop.perform_now rescue "error raised"
> result
=> #<RuntimeError: FailingJob>

As illustrated above, .perform_now actually returns the error, it does not raise it. This appears to be expected behaviour as confirmed in a comment in rails/rails#48281.
Shopify/job-iteration also had to update their custom Tapioca compiler with Shopify/job-iteration#537.

Expected behaviour

Assuming we want the RBI to reflect the possibility of returning exceptions, running bin/tapioca dsl FailingJobWithRescueFromNoop should instead produce something like the following for perform_now:

# failing_job_with_rescue_from_noop.rbi

# ...

class FailingJobWithRescueFromNoop
  class << self
    # ...

    sig { returns(T.any(NilClass, Exception)) }
    def perform_now; end
  end
end

This is what was done in Shopify/job-iteration#537, but this is only possible because that gem provides a #perform method that always returns nil. If used in the general case, any type sigs for #perform that currently have "void" as the return type will fail typechecks if the method returns a non-nil value. Perhaps using T.untyped is the next best thing here?

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at Tapioca::Dsl::Compilers::ActiveJob and reproduce the issue with bin/tapioca dsl FailingJobWithRescueNoop. Compare the generated perform_now signature with the described ActiveJob behavior and consider how it interacts with existing perform signatures, including void. Done means the generated RBI represents the possible exception return without causing incompatible type checks.

Written by the indexing model from the issue text.

Assessment

Tech stack
rails, ruby
Domain
tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.