Sometimes a controller or a job has to perform multiple independent queries, e.g.:
```
def index
@posts = Post.published
@categories = Category.active
end
```
Since these two queries are totally independent, ideally you could
execute them in parallel, so that assuming that each take 50ms, the
total query time would be 50ms rather than 100ms.
A very naive way to do this is to simply call `Relation#to_a` in a
background thread, the problem is that most Rails applications, and
even Rails itself rely on thread local state (`PerThreadRegistry`,
`CurrentAttributes`, etc). So executing such a high level interface
from another thread is likely to lead to many context loss problems
or even thread safety issues.
What we can do instead, is to schedule a much lower level operation
(`Adapter#select_all`) in a thread pool, and return a future/promise.
This way we kepp most of the risky code on the main thread, but perform
the slow IO in background, with very little chance of executing some
code that rely on state stored in thread local storage.
Also since most users are on MRI, only the IO can really be parallelized,
so scheduling more code to be executed in background wouldn't lead
to better performance.
Follow up to #41342.
The reason why the problem of #41342 occurred is that unlike `create`,
`build` did not support the creation of multiple records, so it did not
address the problem.
It is weird that `post.commnets.where(foo: "bar").build([obj1, obj2])`
is allowed but `Comment.where(foo: "bar").build([obj1, obj2])` is not
allowed.
To avoid the confusion, it allows `build` multiple records even on non
association relation.
`bind_call(obj, ...)` is a faster alternative to `bind(obj).call(...)`.
https://bugs.ruby-lang.org/issues/15955
Also, enable `Performance/BindCall` cop to detect those in the future.
Commit: 9ca7389272
Discussion: https://bugs.ruby-lang.org/issues/14473
It seems to be compatible with the original ActiveSupport's
implementation, at least based on the test suite.
It also works faster:
```
Warming up --------------------------------------
Ruby's Range#cover? 1.196M i/100ms
ActiveSupport's Range#cover?
396.369k i/100ms
Calculating -------------------------------------
Ruby's Range#cover? 11.889M (± 1.7%) i/s - 59.820M in 5.033066s
ActiveSupport's Range#cover?
3.951M (± 1.2%) i/s - 19.818M in 5.017176s
Comparison:
Ruby's Range#cover?: 11888979.3 i/s
ActiveSupport's Range#cover?: 3950671.0 i/s - 3.01x (± 0.00) slower
```
Benchmark script:
```ruby
require "minitest/autorun"
require "benchmark/ips"
module ActiveSupportRange
def active_support_cover?(value)
if value.is_a?(::Range)
is_backwards_op = value.exclude_end? ? :>= : :>
return false if value.begin && value.end && value.begin.public_send(is_backwards_op, value.end)
# 1...10 covers 1..9 but it does not cover 1..10.
# 1..10 covers 1...11 but it does not cover 1...12.
operator = exclude_end? && !value.exclude_end? ? :< : :<=
value_max = !exclude_end? && value.exclude_end? ? value.max : value.last
cover?(value.first) && (self.end.nil? || value_max.public_send(operator, last))
else
cover?
end
end
end
class BugTest < Minitest::Test
def test_range_cover
Range.prepend(ActiveSupportRange)
range = (1..10000)
Benchmark.ips do |x|
x.report("Ruby's Range#cover?") do
range.cover?((100..20))
end
x.report("ActiveSupport's Range#cover?") do
range.active_support_cover?((100..20))
end
x.compare!
end
end
end
```
The method signature has changed in Ruby 2.6:
https://bugs.ruby-lang.org/issues/14944
Rails 7.0 requires Ruby 2.7+ now and these checks/monkey patches
shouldn't be needed.
These methods have changed in Ruby 2.5 to be more akin to grep:
https://bugs.ruby-lang.org/issues/11286
Using classes seems to be faster (and a bit more expressive) than iterating over
the collection items:
```
Warming up --------------------------------------
#all? with class 504.000 i/100ms
#all? with proc 189.000 i/100ms
Calculating -------------------------------------
#all? with class 4.960k (± 1.6%) i/s - 25.200k in 5.082049s
#all? with proc 1.874k (± 2.8%) i/s - 9.450k in 5.047866s
Comparison:
#all? with class: 4959.9 i/s
#all? with proc: 1873.8 i/s - 2.65x (± 0.00) slower
```
Benchmark script:
```ruby
require "minitest/autorun"
require "benchmark/ips"
class BugTest < Minitest::Test
def test_enumerators_with_classes
arr = (1..10000).to_a << nil
assert_equal arr.all?(Integer), arr.all? { |v| v.is_a?(Integer) }
Benchmark.ips do |x|
x.report("#all? with class") do
arr.all?(Integer)
end
x.report("#all? with proc") do
arr.all? { |v| v.is_a?(Integer) }
end
x.compare!
end
end
end
```
"active_support/core_ext/string/conversions" was added on 3f1cdb85b8 for `constantize`.
Also, "active_support/core_ext/string/conversions" does not define `constantize` anymore.
"active_support/core_ext/enumerable" was added on ea290e77e6 for `index_by`.
Moved the require to 'lib/active_record/associations/collection_association.rb' where it's still used
Both usages of `index_by` and `constantize` at this file were removed at 52f8e4b9da (diff-7c2226b7a4aa7f86f7d9e5e16b10686056c02efc725a60e378b5f9d7e52e8403)
Followup on https://github.com/rails/rails/pull/41258#discussion_r570441592
This makes sure that newly generated applications get their
`ApplicationRecord` set to `primary_abstract_class`. Existing applications
can opt-in to this if they're using multiple databases or have changed
their `ApplicationRecord` to a class with a different name.
Co-authored-by: John Crepezzi <john.crepezzi@gmail.com>
There is presently no clean way of telling a caller of `perform_later`
the reason why a job failed to enqueue. When the job is enqueued
successfully, the job object itself is returned, but when the job can
not be enqueued, only `false` is returned. This does not allow callers
to distinguish between classes of failures.
One important class of failures is when the job backend experiences a
network partition when communicating with its underlying datastore. It
is entirely possible for that network partition to recover and as such,
code attempting to enqueue a job may wish to take action to reenqueue
that job after a brief delay. This is distinguished from the class of
failures where due a business rule defined in a callback in the
application, a job fails to enqueue and should not be retried.
This PR changes the following:
- Allows a block to be passed to the `perform_later` method. After the
`enqueue` method is executed, but before the result is returned, the
job will be yielded to the block. This allows the code invoking the
`perform_later` method to inspect the job object, even in failure
scenarios.
- Adds an exception `EnqueueError` which job adapters can raise if they
detect a problem specific to their underlying implementation or
infrastructure during the enqueue process.
- Adds two properties to the job base class: `successfully_enqueued` and
`enqueue_error`. `enqueue_error` will be populated by the `enqueue`
method if it rescues an `EnqueueError` raised by the job backend.
`successfully_enqueued` will be true if the job is not rejected by
callbacks and does not cause the job backend to raise an
`EnqueueError` and will be `false` otherwise.
This will allow developers to do something like the following:
MyJob.perform_later do |job|
unless job.successfully_enqueued?
if job.enqueue_error&.message == "Redis was unavailable"
# invoke some code that will retry the job after a delay
end
end
end
Both methods were introduced in Ruby 2.5 and are faster than scanning
unicode graphemes with String#scan
```
Warming up --------------------------------------
scan(/X/) 43.127k i/100ms
grapheme_clusters 103.348k i/100ms
Calculating -------------------------------------
scan(/X/) 427.853k (± 2.4%) i/s - 2.156M in 5.042967s
grapheme_clusters 1.045M (± 0.8%) i/s - 5.271M in 5.042360s
Comparison:
grapheme_clusters: 1045353.5 i/s
scan(/X/): 427852.8 i/s - 2.44x (± 0.00) slower
```
Benchmark script:
```ruby
require "minitest/autorun"
require "benchmark/ips"
class BugTest < Minitest::Test
def test_grapheme_clusters
string = [0x0924, 0x094D, 0x0930].pack("U*") # "त्र"
# string = [0x000D, 0x000A].pack("U*") # cr lf
# string = "こにちわ"
assert string.scan(/\X/) == string.grapheme_clusters
Benchmark.ips do |x|
x.report("scan(/\X/)") do
string.scan(/\X/)
end
x.report("grapheme_clusters") do
string.grapheme_clusters
end
x.compare!
end
end
end
```
String#grapheme_clusters had a bug with CRLF which was fixed in Ruby
2.6: https://bugs.ruby-lang.org/issues/15337
Now that Rails requires Ruby 2.7+, it shouldn't be an issue.
* Add Enumerable#in_order_of
* Explain behavior further
* Use Enumerable#in_order_of
* Use Ruby 2.7 #filter_map
* Update activesupport/lib/active_support/core_ext/enumerable.rb
Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
* No need for explaining variable
* Add CHANGELOG entry
Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>