Open Velrok opened 11 months ago
@Velrok some of our other patches (redis/net-http) also use prepend, how does that work then?
@Velrok some of our other patches (redis/net-http) also use prepend, how does that work then?
Hmm not sure. This blows up when we try to run
bin/tapioca dsl
Which will start the rails app and inspect the classes at runtime.
Maybe the other patches are not loaded?
Is sorbet compatible something you are interested in supporting? If so I could probably negotiate some time to produce a minimal failing example.
@Velrok I haven't really thought about it since you're the first person who asked. I can give it some thought if you give me a simple repro, yes. In general, we at sentry like to be as close to 'zero config' as possible so we do have to do some monkeypatching for that but if there's an easy way around I can see.
This is not tested but I found myself in the same situation and thought about adding something like this to ApplicationJob
:
sig { params(slug: String, monitor_config: Sentry::Cron::MonitorConfig).void }
def self.sentry_monitor_check_ins(slug:, monitor_config:)
around_perform do |_job, block|
check_in_id = Sentry.capture_check_in(slug, :in_progress, monitor_config:)
start = Sentry.utc_now.to_i
begin
block.call
duration = Sentry.utc_now.to_i - start
Sentry.capture_check_in(
slug,
:ok,
check_in_id:,
duration:,
monitor_config:,
)
rescue StandardError
duration = Sentry.utc_now.to_i - start
Sentry.capture_check_in(
slug,
:error,
check_in_id:,
duration:,
monitor_config:,
)
raise
end
end
end
It
around_perform
hook on the job using the same interface as the sentry mixin.By having the same interface you should be able to replace that method with the sentry mixin if/when sentry starts playing nice with Sorbet.
In the end I made another solution so I never tested this in production. I did run some tests in development and that seemed to work just fine.
I want to first clarify that while Sorbet doesn't support prepend
, it simply means it can't properly infer the method when it's defined through prepend. It does NOT mean projects can't have any prepend
if they use Sorbet. Even Tapioca itself uses prepend
in a few places (example).
Secondly, if something crashes when running tapioca dsl
, it has nothing to do with Sorbet. The command, as you said, loads the application and uses the runtime info to generate rbi files. If things go wrong here, it's purely a runtime issue, not related to Sorbet.
And since it's a runtime issue, it's likely caused by things not being loaded properly. The solution could be:
sorbet/tapioca/require.rb
sorbet/tapioca/config.yml
to exclude certain gemsHowever, without further information we can't properly suggest a better solution here. Can you please add more information, like:
@st0012 thanks for the feedback. I do believe the crash Sorbet+Tapioca users experience here has to do with prepend and is not a loading issue. Here's the information you requested:
Versions:
sorbet (0.5.11287)
sorbet-static (= 0.5.11287)
sorbet-runtime (0.5.11287)
sorbet-static (0.5.11287-universal-darwin)
sorbet-static (0.5.11287-x86_64-linux)
sorbet-static-and-runtime (0.5.11287)
sorbet (= 0.5.11287)
sorbet-runtime (= 0.5.11287)
sorbet-struct-comparable (1.3.0)
sorbet-runtime (>= 0.5)
tapioca (0.12.0)
Error backtrace:
❯ task tapioca:dsl
task: [tapioca:dsl] bin/tapioca dsl
Loading DSL extension classes... Done
Loading Rails application...
Tapioca attempted to load the Rails application after encountering a `config/application.rb` file, but it failed. If your application uses Rails please ensure it can be loaded correctly before generating RBIs. If your application does not use Rails and you wish to continue RBI generation please pass `--no-halt-upon-load-error` to the tapioca command in sorbet/tapioca/config.yml or in CLI.
You're trying to replace `perform` on `CacheListingMarketCommand::CronJob`, but that method exists in a prepended module (Sentry::Cron::MonitorCheckIns::Patch), which we don't currently support.
/Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/class_utils.rb:115:in `block in replace_method': You're trying to replace `perform` on `CacheListingMarketCommand::CronJob`, but that method exists in a prepended module (Sentry::Cron::MonitorCheckIns::Patch), which we don't currently support. (RuntimeError)
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/class_utils.rb:105:in `each'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/class_utils.rb:105:in `replace_method'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/methods/_methods.rb:257:in `_on_method_added'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/methods/_methods.rb:549:in `method_added'
from /Users/jgrau/src/landfolk/apps/api/app/commands/cache_listing_market_command.rb:75:in `<class:CronJob>'
from /Users/jgrau/src/landfolk/apps/api/app/commands/cache_listing_market_command.rb:66:in `<class:CacheListingMarketCommand>'
from /Users/jgrau/src/landfolk/apps/api/app/commands/cache_listing_market_command.rb:7:in `<main>'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/3.3.0/bundled_gems.rb:74:in `require'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/3.3.0/bundled_gems.rb:74:in `block (2 levels) in replace_require'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/bootsnap-1.18.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:30:in `require'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/zeitwerk-2.6.13/lib/zeitwerk/kernel.rb:26:in `require'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/zeitwerk-2.6.13/lib/zeitwerk/loader/helpers.rb:135:in `const_get'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/zeitwerk-2.6.13/lib/zeitwerk/loader/helpers.rb:135:in `cget'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/zeitwerk-2.6.13/lib/zeitwerk/loader/eager_load.rb:175:in `block in actual_eager_load_dir'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/zeitwerk-2.6.13/lib/zeitwerk/loader/helpers.rb:40:in `block in ls'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/zeitwerk-2.6.13/lib/zeitwerk/loader/helpers.rb:25:in `each'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/zeitwerk-2.6.13/lib/zeitwerk/loader/helpers.rb:25:in `ls'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/zeitwerk-2.6.13/lib/zeitwerk/loader/eager_load.rb:170:in `actual_eager_load_dir'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/zeitwerk-2.6.13/lib/zeitwerk/loader/eager_load.rb:17:in `block (2 levels) in eager_load'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/zeitwerk-2.6.13/lib/zeitwerk/loader/eager_load.rb:16:in `each'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/zeitwerk-2.6.13/lib/zeitwerk/loader/eager_load.rb:16:in `block in eager_load'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/zeitwerk-2.6.13/lib/zeitwerk/loader/eager_load.rb:10:in `synchronize'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/zeitwerk-2.6.13/lib/zeitwerk/loader/eager_load.rb:10:in `eager_load'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/zeitwerk-2.6.13/lib/zeitwerk/loader.rb:379:in `block in eager_load_all'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/zeitwerk-2.6.13/lib/zeitwerk/loader.rb:377:in `each'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/zeitwerk-2.6.13/lib/zeitwerk/loader.rb:377:in `eager_load_all'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/tapioca-0.12.0/lib/tapioca/loaders/loader.rb:206:in `eager_load_rails_app'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/methods/_methods.rb:279:in `bind_call'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/methods/_methods.rb:279:in `block in _on_method_added'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/tapioca-0.12.0/lib/tapioca/loaders/loader.rb:60:in `load_rails_application'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/methods/_methods.rb:279:in `bind_call'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/methods/_methods.rb:279:in `block in _on_method_added'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/tapioca-0.12.0/lib/tapioca/loaders/dsl.rb:84:in `load_application'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/methods/_methods.rb:279:in `bind_call'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/methods/_methods.rb:279:in `block in _on_method_added'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/tapioca-0.12.0/lib/tapioca/loaders/dsl.rb:29:in `load'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/methods/_methods.rb:279:in `bind_call'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/methods/_methods.rb:279:in `block in _on_method_added'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/tapioca-0.12.0/lib/tapioca/loaders/dsl.rb:22:in `load_application'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/methods/_methods.rb:279:in `bind_call'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/methods/_methods.rb:279:in `block in _on_method_added'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/tapioca-0.12.0/lib/tapioca/commands/abstract_dsl.rb:108:in `load_application'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/methods/_methods.rb:279:in `bind_call'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/methods/_methods.rb:279:in `block in _on_method_added'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/tapioca-0.12.0/lib/tapioca/commands/dsl_generate.rb:11:in `execute'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/methods/_methods.rb:279:in `bind_call'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/methods/_methods.rb:279:in `block in _on_method_added'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/tapioca-0.12.0/lib/tapioca/commands/command.rb:27:in `block in run'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/tapioca-0.12.0/lib/tapioca.rb:23:in `block in silence_warnings'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/3.3.0/rubygems/user_interaction.rb:46:in `use_ui'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/tapioca-0.12.0/lib/tapioca.rb:22:in `silence_warnings'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/methods/_methods.rb:279:in `bind_call'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/methods/_methods.rb:279:in `block in _on_method_added'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/tapioca-0.12.0/lib/tapioca/commands/command.rb:26:in `run'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/methods/_methods.rb:279:in `bind_call'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/sorbet-runtime-0.5.11287/lib/types/private/methods/_methods.rb:279:in `block in _on_method_added'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/tapioca-0.12.0/lib/tapioca/cli.rb:168:in `dsl'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/thor-1.3.1/lib/thor/command.rb:28:in `run'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/thor-1.3.1/lib/thor/invocation.rb:127:in `invoke_command'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/thor-1.3.1/lib/thor.rb:527:in `dispatch'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/thor-1.3.1/lib/thor/base.rb:584:in `start'
from /Users/jgrau/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/tapioca-0.12.0/exe/tapioca:25:in `<top (required)>'
from /Users/jgrau/src/landfolk/apps/api/bin/tapioca:27:in `load'
from /Users/jgrau/src/landfolk/apps/api/bin/tapioca:27:in `<main>'
task: Failed to run task "tapioca:dsl": exit status 1
Tapioca config:
gem:
# Add your `gem` command parameters here:
#
exclude:
[]
# - red-arrow
# - red-parquet
# doc: true
# workers: 5
dsl:
# Add your `dsl` command parameters here:
#
# exclude:
# - SomeGeneratorName
# workers: 5
The sorbet-runtime method that raises the error looks like this:
# Replaces a method, either by overwriting it (if it is defined directly on `mod`) or by
# overriding it (if it is defined by one of mod's ancestors). If `original_only` is
# false, returns a ReplacedMethod instance on which you can call `bind(...).call(...)`
# to call the original method, or `restore` to restore the original method (by
# overwriting or removing the override).
#
# If `original_only` is true, return the `UnboundMethod` representing the original method.
def self.replace_method(mod, name, original_only=false, &blk)
original_method = mod.instance_method(name)
original_visibility = visibility_method_name(mod, name)
original_owner = original_method.owner
mod.ancestors.each do |ancestor|
break if ancestor == mod
if ancestor == original_owner
# If we get here, that means the method we're trying to replace exists on a *prepended*
# mixin, which means in order to supersede it, we'd need to create a method on a new
# module that we'd prepend before `ancestor`. The problem with that approach is there'd
# be no way to remove that new module after prepending it, so we'd be left with these
# empty anonymous modules in the ancestor chain after calling `restore`.
#
# That's not necessarily a deal breaker, but for now, we're keeping it as unsupported.
raise "You're trying to replace `#{name}` on `#{mod}`, but that method exists in a " \
"prepended module (#{ancestor}), which we don't currently support."
end
end
overwritten = original_owner == mod
T::Configuration.without_ruby_warnings do
T::Private::DeclState.current.without_on_method_added do
def_with_visibility(mod, name, original_visibility, &blk)
end
end
if original_only
original_method
else
new_method = mod.instance_method(name)
ReplacedMethod.new(mod, original_method, new_method, overwritten, original_visibility)
end
end
which leads me to think that the error is intentional and not a bug or a loading issue.
Thank you for the information, I now understand the problem (also described in great detail in this issue). Unfortunately, I don't think not using prepend is possible because the alternative would be something like alias_method_chain
, which when other gems use prepend
, would cause infinite loop instead (which is also why it's a deprecated practice).
I think for now the only way to avoid this is to opt-out the patch that triggers the prepend. Given the class's name has CronJob
, I assume you probably use sidekiq-cron
? If you do, can you try adding this to your Sentry config file:
Sentry.init { ... }
Sentry.registered_patches.delete(:sidekiq_cron)
A related/separate disadvantage of prepend
is that you can't inherit sentry_monitor_check_ins
from parent classes.
For example:
class BaseScheduledJob
sentry_monitor_check_ins
def perform
# this calls Sentry::Cron::MonitorCheckIns::Patch#perform
end
end
class SomeSpecificScheduledJob < BaseScheduledJob
def perform
# this doesn't call Sentry::Cron::MonitorCheckIns::Patch#perform
end
end
This is subtle and took a while for me to figure out (I learned some new things!). It makes me feel like prepend
might not be the right solution here.
Ended up using something based on @jgrau's snippet:
# app/jobs/application_job.rb
# Alternative to the same method provided in +Sentry::Cron::MonitorCheckIns+,
# which doesn't work with Sorbet, see https://github.com/getsentry/sentry-ruby/issues/2189
sig { params(monitor_config: Sentry::Cron::MonitorConfig, slug: T.nilable(String)).void }
def self.sentry_monitor_check_ins(monitor_config:, slug: nil)
slug ||= T.must(name).gsub("::", "-").downcase
sentry_slug = slug[-50..] || slug
around_perform do |_job, block|
check_in_id = Sentry.capture_check_in(sentry_slug, :in_progress, monitor_config:)
start = Sentry.utc_now.to_i
begin
block.call
duration = Sentry.utc_now.to_i - start
Sentry.capture_check_in(sentry_slug, :ok, check_in_id:, duration:, monitor_config:)
rescue StandardError
duration = Sentry.utc_now.to_i - start
Sentry.capture_check_in(sentry_slug, :error, check_in_id:, duration:, monitor_config:)
raise
end
end
end
Describe the idea
It would be nice if the monitor_check_ins.rb wouldn't use
preprend
, because that means we can't adopt it alongside sorbet atm.Why do you think it's beneficial to most of the users
Sorbet is a mature type checker for ruby, which is great to have in large code bases. They decided not to support prepend, which seams reasonable to me.
Possible implementation
Sorry I don't understand prepend well enough to really comment. Couldn't it be wrapped in some sort of block / yield alternative?