we’re running into transient NoMethodError errors in code paths using T.cast on our production services. Here are some notable facts
- Is happening in
sorbet-runtimeas of version0.5.10012 - It only affects 1 host at a time and often happens in short spurts (ex. errors for < 10m and goes away).
- On some of our production services, we’ll see errors daily. On others, it’ll be weekly
- We were not seeing this error until we upgraded sorbet-runtime versions from
0.5.6001to0.5.9976(2 years of changes). We were pinned on the old version until we could migrate off the now deprecated--dsl-plugins - Ruby version 2.7.4
- We have disabled runtime validation in production -
T::Configuration.default_checked_level = :never
What it looks like is that T::Utils.coerce(type) returns an unexpected value (not a subclass of T::Types::Base) for a short period of time, which leads to errors here when checking error_message_for_obj here: code
Recent stacktrace
NoMethodError: undefined method `valid?' for {:mode=>:compat}:Hash
from sorbet-runtime (0.5.10012) lib/types/types/intersection.rb:33:in `block in valid?'
from sorbet-runtime (0.5.10012) lib/types/types/intersection.rb:33:in `all?'
from sorbet-runtime (0.5.10012) lib/types/types/intersection.rb:33:in `valid?'
from sorbet-runtime (0.5.10012) lib/types/types/base.rb:133:in `error_message_for_obj'
from sorbet-runtime (0.5.10012) lib/types/private/casts.rb:8:in `cast'
from sorbet-runtime (0.5.10012) lib/types/_types.rb:133:in `cast'
from < our code>
Where the line in our code looks like this
T.cast(self, T.all(T::Props::Serializable, Object)).as_json
I've not figured out a way to reproduce locally but I selectively enabled a debugging monkeypatch to T.cast to not raise some errors and log some more information.
# frozen_string_literal: true
module SorbetMonkeyPatches
module TcastDebug
module ClassMethods
def cast(value, type, checked: true)
super
rescue NoMethodError => e
Rails.logger.error(
message: "[sorbet] - #{value.try(:class)} to #{type.try(:class)}",
src_value: value.inspect,
src_type: type.inspect,
coerce_res: T::Utils.coerce(type).inspect,
)
value # ultimately don't want assertions from T.cast...this should return the same value unchanged
end
end
end
end
if ENV["SORBET_DEBUG_UPGRADE"] == "true"
::T.singleton_class.prepend(SorbetMonkeyPatches::TcastDebug::ClassMethods)
end
When enabled, here are some sample log statements we see
[sorbet] - Class to Module {"src_value":"ProductsDomain::Api::LocaleCode","src_type":"T::Props::CustomType","coerce_res":"\"ssl_min_protocol_version\""}
...
[sorbet] - Class to Module {"src_value":"ProductsDomain::Api::LocaleCode","src_type":"T::Props::CustomType","coerce_res":"[\"workflow_state\", \"active\"]"}
If working correctly, I would’ve expected coerce_res to return a subclass of T::Types::Base, not random strings
My uninformed opinion is that we’re somehow corrupting the cache for coerced types introduced by this PR - https://github.com/sorbet/sorbet/pull/4397 .
The questions I have
- Has this been seen before?
- Any advice for how to address or how to continue debugging?
- Ultimately we’d hope not to see errors from
T.castwhen runtime checks are disabled…should we override the optional checked param in theT.castmethod definition?
Thanks for your help! Please let me know if you'd like any more debugging information