Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: exAspArk/batch-loader
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.0.1
Choose a base ref
...
head repository: exAspArk/batch-loader
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v1.0.2
Choose a head ref
  • 5 commits
  • 7 files changed
  • 1 contributor

Commits on Sep 14, 2017

  1. Verified

    This commit was signed with the committer’s verified signature.
    Copy the full SHA
    9d62c71 View commit details
  2. Verified

    This commit was signed with the committer’s verified signature.
    Copy the full SHA
    3701923 View commit details
  3. Verified

    This commit was signed with the committer’s verified signature.
    Copy the full SHA
    00bbe62 View commit details
  4. Verified

    This commit was signed with the committer’s verified signature.
    Copy the full SHA
    f2ef572 View commit details
  5. Release v1.0.2

    exAspArk committed Sep 14, 2017

    Verified

    This commit was signed with the committer’s verified signature.
    Copy the full SHA
    87bb4c5 View commit details
Showing with 97 additions and 34 deletions.
  1. +8 −1 CHANGELOG.md
  2. +0 −2 Gemfile
  3. +1 −0 batch-loader.gemspec
  4. +22 −30 lib/batch_loader.rb
  5. +1 −1 lib/batch_loader/version.rb
  6. +21 −0 spec/batch_loader_spec.rb
  7. +44 −0 spec/benchmarks/loading.rb
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -8,10 +8,17 @@ one of the following labels: `Added`, `Changed`, `Deprecated`,
to manage the versions of this gem so
that you can set version constraints properly.

#### [Unreleased](https://github.com/exAspArk/batch-loader/compare/v1.0.1...HEAD)
#### [Unreleased](https://github.com/exAspArk/batch-loader/compare/v1.0.2...HEAD)

* WIP

#### [v1.0.2](https://github.com/exAspArk/batch-loader/compare/v1.0.1...v1.0.2) – 2017-09-14

* `Added`: `BatchLoader#inspect` method because of Pry, which [swallows errors](https://github.com/pry/pry/issues/1642).
* `Added`: benchmarks.
* `Fixed`: caching `nil`s for not loaded values only after successful `#batch` execution.
* `Changed`: internal implementation with Ruby `Forwardable`, don't delegate methods like `object_id` and `__send__`.

#### [v1.0.1](https://github.com/exAspArk/batch-loader/compare/v1.0.0...v1.0.1) – 2017-09-03

* `Fixed`: loading `BatchLoader` by requiring `Set`.
2 changes: 0 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem 'coveralls', require: false

# Specify your gem's dependencies in batch-loader.gemspec
1 change: 1 addition & 0 deletions batch-loader.gemspec
Original file line number Diff line number Diff line change
@@ -28,4 +28,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "rspec", "~> 3.0"
spec.add_development_dependency "graphql", "~> 1.6"
spec.add_development_dependency "pry-byebug", "~> 3.4"
spec.add_development_dependency "benchmark-ips", "~> 2.7"
end
52 changes: 22 additions & 30 deletions lib/batch_loader.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
# frozen_string_literal: true

require "set"
require "forwardable"

require "batch_loader/version"
require "batch_loader/executor_proxy"
require "batch_loader/middleware"
require "batch_loader/graphql"

class BatchLoader
extend Forwardable

IMPLEMENTED_INSTANCE_METHODS = %i[object_id __id__ __send__ singleton_method_added batch_loader? respond_to? batch inspect].freeze
REPLACABLE_INSTANCE_METHODS = %i[batch inspect].freeze
LEFT_INSTANCE_METHODS = (IMPLEMENTED_INSTANCE_METHODS - REPLACABLE_INSTANCE_METHODS).freeze

NoBatchError = Class.new(StandardError)

def self.for(item)
@@ -32,7 +40,11 @@ def batch_loader?
end

def respond_to?(method_name)
method_name == :batch_loader? || method_missing(:respond_to?, method_name)
LEFT_INSTANCE_METHODS.include?(method_name) || method_missing(:respond_to?, method_name)
end

def inspect
"#<BatchLoader:0x#{(object_id << 1)}>"
end

private
@@ -62,28 +74,22 @@ def ensure_batched

items = executor_proxy.list_items
loader = ->(item, value) { executor_proxy.load(item: item, value: value) }
items.each { |item| loader.call(item, nil) }

@batch_block.call(items, loader)
items.each do |item|
next if executor_proxy.value_loaded?(item: item)
loader.call(item, nil) # use "nil" for not loaded item after succesfull batching
end
executor_proxy.delete(items: items)
end

def singleton_class
class << self
self
end
class << self ; self ; end
end

def replace_with!(value)
BatchLoader.send(:without_warnings) do
ignore_method_names = %i[singleton_method_added].freeze
singleton_class.class_eval do
(value.methods - ignore_method_names).each do |method_name|
define_method(method_name) do |*args, &block|
value.public_send(method_name, *args, &block)
end
end
end
end
@loaded_value = value
singleton_class.class_eval { def_delegators :@loaded_value, *(value.methods - LEFT_INSTANCE_METHODS) }
end

def purge_cache
@@ -98,19 +104,5 @@ def executor_proxy
end
end

class << self
private

def without_warnings(&block)
warning_level = $VERBOSE
$VERBOSE = nil
block.call
$VERBOSE = warning_level
end
end

without_warnings do
leave_method_names = %i[batch batch_loader? respond_to?].freeze
(instance_methods - leave_method_names).each { |method_name| undef_method(method_name) }
end
(instance_methods - IMPLEMENTED_INSTANCE_METHODS).each { |method_name| undef_method(method_name) }
end
2 changes: 1 addition & 1 deletion lib/batch_loader/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

class BatchLoader
VERSION = "1.0.1"
VERSION = "1.0.2"
end
21 changes: 21 additions & 0 deletions spec/batch_loader_spec.rb
Original file line number Diff line number Diff line change
@@ -86,6 +86,19 @@
end
end

describe '#inspect' do
it 'returns BatchLoader without syncing and delegates #inspect after' do
user = User.save(id: 1)
post = Post.new(user_id: user.id)

batch_loader = post.user_lazy

expect(batch_loader.inspect).to match(/#<BatchLoader:0x\w+>/)
expect(batch_loader.to_s).to match(/#<User:0x\w+>/)
expect(batch_loader.inspect).to match(/#<User:0x\w+ @id=1>/)
end
end

describe '#batch' do
it 'delegates the second batch call to the loaded value' do
user = User.save(id: 1)
@@ -118,5 +131,13 @@
expect(user_lazy).to eq(user)
expect(user_lazy).to eq(user)
end

it 'raises the error if something went wrong in the batch' do
result = BatchLoader.for(1).batch { |ids, loader| raise "Oops" }
# should work event with Pry which currently shallows errors on #inspect call https://github.com/pry/pry/issues/1642
# require 'pry'; binding.pry
expect { result.to_s }.to raise_error("Oops")
expect { result.to_s }.to raise_error("Oops")
end
end
end
44 changes: 44 additions & 0 deletions spec/benchmarks/loading.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

# Usage: ruby spec/benchmarks/loading.rb

require 'benchmark/ips'
require "batch_loader"

require_relative "../fixtures/models"

User.save(id: 1)

batch_loader_with_cache = BatchLoader.for(1).batch do |ids, loader|
User.where(id: ids).each { |user| loader.call(user.id, user) }
end

batch_loader_without_cache = BatchLoader.for(1).batch(cache: false) do |ids, loader|
User.where(id: ids).each { |user| loader.call(user.id, user) }
end

Benchmark.ips do |x|
x.config(time: 5, warmup: 0)
x.report("without BatchLoader") { User.where(id: [1]).first.id }
x.report("with cache") { batch_loader_with_cache.id }
x.report("with purged cache") { batch_loader_with_cache.id ; BatchLoader::Executor.clear_current }
x.report("without cache") { batch_loader_without_cache.id }
x.compare!
end

# Warming up --------------------------------------
# without BatchLoader 1.000 i/100ms
# with cache 1.000 i/100ms
# with purged cache 1.000 i/100ms
# without cache 1.000 i/100ms
# Calculating -------------------------------------
# without BatchLoader 972.844k (±13.5%) i/s - 3.458M
# with cache 991.569k (± 7.7%) i/s - 3.281M
# with purged cache 989.499k (± 8.1%) i/s - 3.570M
# without cache 76.256k (±16.9%) i/s - 345.629k in 4.886612s

# Comparison:
# with cache: 991569.5 i/s
# with purged cache: 989499.4 i/s - same-ish: difference falls within error
# without BatchLoader: 972844.4 i/s - same-ish: difference falls within error
# without cache: 76256.3 i/s - 13.00x slower