`ActiveSupport::MessagePack` is a serializer that integrates with the
`msgpack` gem to serialize a variety of Ruby objects. `AS::MessagePack`
supports several types beyond the base types that `msgpack` supports,
including `Time` and `Range`, as well as Active Support types such as
`AS::TimeWithZone` and `AS::HashWithIndifferentAccess`.
Compared to `JSON` and `Marshal`, `AS::MessagePack` can provide a
performance improvement and message size reduction. For example, when
used with `MessageVerifier`:
```ruby
# frozen_string_literal: true
require "benchmark/ips"
require "active_support/all"
require "active_support/message_pack"
marshal_verifier = ActiveSupport::MessageVerifier.new("secret", serializer: Marshal)
json_verifier = ActiveSupport::MessageVerifier.new("secret", serializer: JSON)
asjson_verifier = ActiveSupport::MessageVerifier.new("secret", serializer: ActiveSupport::JSON)
msgpack_verifier = ActiveSupport::MessageVerifier.new("secret", serializer: ActiveSupport::MessagePack)
ActiveSupport::Messages::Metadata.use_message_serializer_for_metadata = true
expiry = 1.year.from_now
data = { bool: true, num: 123456789, string: "x" * 50 }
Benchmark.ips do |x|
x.report("Marshal") do
marshal_verifier.verify(marshal_verifier.generate(data, expires_at: expiry))
end
x.report("JSON") do
json_verifier.verify(json_verifier.generate(data, expires_at: expiry))
end
x.report("AS::JSON") do
asjson_verifier.verify(asjson_verifier.generate(data, expires_at: expiry))
end
x.report("MessagePack") do
msgpack_verifier.verify(msgpack_verifier.generate(data, expires_at: expiry))
end
x.compare!
end
puts "Marshal size: #{marshal_verifier.generate(data, expires_at: expiry).bytesize}"
puts "JSON size: #{json_verifier.generate(data, expires_at: expiry).bytesize}"
puts "MessagePack size: #{msgpack_verifier.generate(data, expires_at: expiry).bytesize}"
```
```
Warming up --------------------------------------
Marshal 1.206k i/100ms
JSON 1.165k i/100ms
AS::JSON 790.000 i/100ms
MessagePack 1.798k i/100ms
Calculating -------------------------------------
Marshal 11.748k (± 1.3%) i/s - 59.094k in 5.031071s
JSON 11.498k (± 1.4%) i/s - 58.250k in 5.066957s
AS::JSON 7.867k (± 2.4%) i/s - 39.500k in 5.024055s
MessagePack 17.865k (± 0.8%) i/s - 89.900k in 5.032592s
Comparison:
MessagePack: 17864.9 i/s
Marshal: 11747.8 i/s - 1.52x (± 0.00) slower
JSON: 11498.4 i/s - 1.55x (± 0.00) slower
AS::JSON: 7866.9 i/s - 2.27x (± 0.00) slower
Marshal size: 254
JSON size: 234
MessagePack size: 194
```
Additionally, `ActiveSupport::MessagePack::CacheSerializer` is a
serializer that is suitable for use as an `ActiveSupport::Cache` coder.
`AS::MessagePack::CacheSerializer` can serialize `ActiveRecord::Base`
instances, including loaded associations. Like `AS::MessagePack`, it
provides a performance improvement and payload size reduction:
```ruby
# frozen_string_literal: true
require "benchmark/ips"
require "active_support/message_pack"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Schema.define do
create_table :posts, force: true do |t|
t.string :body
t.timestamps
end
create_table :comments, force: true do |t|
t.integer :post_id
t.string :body
t.timestamps
end
end
class Post < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
end
post = Post.create!(body: "x" * 100)
2.times { post.comments.create!(body: "x" * 100) }
post.comments.load
cache_entry = ActiveSupport::Cache::Entry.new(post)
Rails70Coder = ActiveSupport::Cache::Coders::Rails70Coder
CacheSerializer = ActiveSupport::MessagePack::CacheSerializer
Benchmark.ips do |x|
x.report("Rails70Coder") do
Rails70Coder.load(Rails70Coder.dump(cache_entry))
end
x.report("CacheSerializer") do
CacheSerializer.load(CacheSerializer.dump(cache_entry))
end
x.compare!
end
puts "Rails70Coder size: #{Rails70Coder.dump(cache_entry).bytesize}"
puts "CacheSerializer size: #{CacheSerializer.dump(cache_entry).bytesize}"
```
```
Warming up --------------------------------------
Rails70Coder 329.000 i/100ms
CacheSerializer 492.000 i/100ms
Calculating -------------------------------------
Rails70Coder 3.285k (± 1.7%) i/s - 16.450k in 5.008447s
CacheSerializer 4.895k (± 2.4%) i/s - 24.600k in 5.028803s
Comparison:
CacheSerializer: 4894.7 i/s
Rails70Coder: 3285.4 i/s - 1.49x slower
Rails70Coder size: 808
CacheSerializer size: 593
```
Co-authored-by: Jean Boussier <jean.boussier@gmail.com>
When 8b2f57dc6f was
introduced it broke delegation of a method with implicit block like this:
```ruby
class Foo
def self.bar
yield
end
end
class Bar
delegate :bar, to: Foo
end
Bar.new.bar { } # raises LocalJumpError: no block given (yield)
```
It happens because it's impossible to detect a method with implicit block and
we rely on method's parameters when we generate delegated methods.
I've found only one solution to this issue: we always generate a method that explicitly accepts
block. It kind of makes sense since evey Ruby method accepts block implicitly anyway.
Fixes https://github.com/rails/rails/issues/47624
In Rails 7, if you do `Rails.cache.write(key, value, expires_in: 1.minute.from_now)`, it will work. The actual expiration will be much more than a minute away, but it won't raise. (The correct code is `expires_in: 1.minute` or `expires_at: 1.minute.from_now`.)
Since https://github.com/rails/rails/pull/45892 the same code will error with:
```
NoMethodError: undefined method `negative?' for 2008-04-24 00:01:00 -0600:Time
/Users/alex/Code/rails/activesupport/lib/active_support/cache.rb:743:in `merged_options'
/Users/alex/Code/rails/activesupport/lib/active_support/cache.rb:551:in `write'
```
To make it a bit easier to upgrade to Rails 7.1, this PR introduces a better error if you pass a `Time` object to `expires_in:`
```
ArgumentError: expires_in parameter should not be a Time. Did you mean to use expires_at? Got: 2023-04-07 14:47:45 -0600
/Users/alex/Code/rails/activesupport/lib/active_support/cache.rb:765:in `handle_invalid_expires_in'
/Users/alex/Code/rails/activesupport/lib/active_support/cache.rb:745:in `merged_options'
/Users/alex/Code/rails/activesupport/lib/active_support/cache.rb:551:in `write'
```
3d00c8b97f introduced a regression:
```ruby
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym("API")
end
```
If you have a controller called `APIController`, you may get a crash backtrace that looks like this:
```
FrozenError: can't modify frozen String: "API"
rails (b8a399cd6ffe) actionpack/lib/action_dispatch/http/request.rb:89:in `controller_class_for'
rails (b8a399cd6ffe) actionpack/lib/action_dispatch/request/utils.rb💯in `action_encoding_template'
rails (b8a399cd6ffe) actionpack/lib/action_dispatch/request/utils.rb:85:in `encode'
rails (b8a399cd6ffe) actionpack/lib/action_dispatch/request/utils.rb:45:in `set_binary_encoding'
rails (b8a399cd6ffe) actionpack/lib/action_dispatch/http/parameters.rb:68:in `path_parameters='
```
And you can see the issue in action like this:
```ruby
Tanda @ Rails 7.1.0.alpha (test) :004 > "API".freeze.underscore.frozen?
false # this is expected
Tanda @ Rails 7.1.0.alpha (test) :004 > "API".freeze.underscore.camelize.frozen?
false # this is *not* expected, and is what causes the above code to crash
```
I think the correct behaviour is for the inflector to always return non-frozen strings, even if it's using a cached frozen string as is the case in 3d00c8b97f
Other semi-related PRs: https://github.com/rails/rails/pull/41174, https://github.com/rails/rails/pull/41881
cc @amatsuda @byroot
`Listen.to` starts a bunch of background threads that need to perform
some work before they are able to receive events, but it doesn't block
until they are ready, which expose us to a race condition.
With `wait_for_state(:processing_events)` we can ensure that it's ready on
Linux, however on macOS, the Darwin backend has a second background thread
we can't wait on.
As a workaround we wait a bit after the fork to allow that thread to
reach it's listning state.
Fanout is a singleton shared among threads, so all of its hash maps
should be concurrent hash maps otherwise we'll see errors like this:
```
RuntimeError: can't add a new key into hash during iteration
```
Currently when opening the main framework pages there is no introduction
to the framework. Instead we only see a whole lot of modules and the
`gem_version` and `version` methods.
By including the READMEs using the `:include:` directive each frameworks
has a nice introduction.
For markdown READMEs we need to add the :markup: directive.
[ci-skip]
Co-authored-by: zzak <zzakscott@gmail.com>
All methods called on ActiveSupport::Deprecation are delegated to the
singleton instance, but now that each framework has its own Deprecation
instance, it's no longer necessary for internal deprecations.
For external code, instead of using ActiveSupport::Deprecation.warn,
they should create their own instance of the class and call warn on it.
Use case
A very common pattern in Ruby, especially in testing is to save the value of an attribute, set a new value, and then restore the old value in an `ensure` clause.
e.g. in unit tests
```ruby
def test_something_when_enabled
enabled_was, SomeLibrary.enabled = SomeLibrary.enabled, true
# test things
ensure
SomeLibrary.enabled = enabled_was
end
```
Or sometime in actual APIs:
```ruby
def with_something_enabled
enabled_was = @enabled
@enabled = true
yield
ensure
@enabled = enabled_was
end
```
There is no inherent problem with this pattern, but it can be easy to make a mistake, for instance the unit test example:
```ruby
def test_something_when_enabled
some_call_that_may_raise
enabled_was, SomeLibrary.enabled = SomeLibrary.enabled, true
# test things
ensure
SomeLibrary.enabled = enabled_was
end
```
In the above if `some_call_that_may_raise` actually raises, `SomeLibrary.enabled` is set back to `nil` rather than its original value. I've seen this mistake quite frequently.
Object#with
I think it would be very useful to have a method on Object to implement this pattern in a correct and easy to use way.
NB: `public_send` is used because I don't think such method should be usable if the accessors are private.
With usage:
```ruby
def test_something_when_enabled
SomeLibrary.with(enabled: true) do
# test things
end
end
```
```ruby
GC.with(measure_total_time: true, auto_compact: false) do
# do something
end
```
Lots of tests in Rails's codebase could be simplified, e.g.:
- Changing `Thread.report_on_exception`: 2d2fdc941e/activerecord/test/cases/connection_pool_test.rb (L583-L595)
- Changing a class attribute: 2d2fdc941e/activerecord/test/cases/associations/belongs_to_associations_test.rb (L136-L150)