Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sharding support #12

Merged
merged 4 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 0.3.7 - 2024-08-05
* Add `connected_to` to the collection for custom connection handling

## 0.0.1
The first release of the esse-active_record plugin
* Added: Initial implementation of the plugin
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
esse-active_record (0.3.6)
esse-active_record (0.3.7)
activerecord (>= 4.2, < 8)
esse (>= 0.3.0)

Expand Down
2 changes: 1 addition & 1 deletion ci/Gemfile.rails-5.2.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: ..
specs:
esse-active_record (0.3.6)
esse-active_record (0.3.7)
activerecord (>= 4.2, < 8)
esse (>= 0.3.0)

Expand Down
2 changes: 1 addition & 1 deletion ci/Gemfile.rails-6.0.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: ..
specs:
esse-active_record (0.3.6)
esse-active_record (0.3.7)
activerecord (>= 4.2, < 8)
esse (>= 0.3.0)

Expand Down
2 changes: 1 addition & 1 deletion ci/Gemfile.rails-6.1.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: ..
specs:
esse-active_record (0.3.6)
esse-active_record (0.3.7)
activerecord (>= 4.2, < 8)
esse (>= 0.3.0)

Expand Down
2 changes: 1 addition & 1 deletion ci/Gemfile.rails-7.0.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: ..
specs:
esse-active_record (0.3.6)
esse-active_record (0.3.7)
activerecord (>= 4.2, < 8)
esse (>= 0.3.0)

Expand Down
2 changes: 1 addition & 1 deletion ci/Gemfile.rails-7.1.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: ..
specs:
esse-active_record (0.3.6)
esse-active_record (0.3.7)
activerecord (>= 4.2, < 8)
esse (>= 0.3.0)

Expand Down
43 changes: 35 additions & 8 deletions lib/esse/active_record/collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ class Collection
class_attribute :batch_contexts
self.batch_contexts = {}

# Connects to a database or role (ex writing, reading, or another custom role) for the collection query
# @param [Symbol] role The role to connect to
# @param [Symbol] shard The shard to connect to
class_attribute :connect_with
self.connect_with = nil

class << self
def inspect
return super unless self < Esse::ActiveRecord::Collection
Expand All @@ -40,6 +46,7 @@ def inherited(subclass)

subclass.scopes = scopes.dup
subclass.batch_contexts = batch_contexts.dup
subclass.connect_with = connect_with&.dup
end

def scope(name, proc = nil, override: false, &block)
Expand All @@ -57,6 +64,10 @@ def batch_context(name, proc = nil, override: false, &block)

batch_contexts[name.to_sym] = proc
end

def connected_to(**kwargs)
self.connect_with = kwargs
end
end

attr_reader :start, :finish, :batch_size, :params
Expand All @@ -74,23 +85,29 @@ def initialize(start: nil, finish: nil, batch_size: nil, **params)
end

def each
dataset.find_in_batches(**batch_options) do |rows|
kwargs = params.dup
self.class.batch_contexts.each do |name, proc|
kwargs[name] = proc.call(rows, **params)
with_connection do
dataset.find_in_batches(**batch_options) do |rows|
kwargs = params.dup
self.class.batch_contexts.each do |name, proc|
kwargs[name] = proc.call(rows, **params)
end
yield(rows, **kwargs)
end
yield(rows, **kwargs)
end
end

def each_batch_ids
dataset.select(:id).except(:includes, :preload, :eager_load).find_in_batches(**batch_options) do |rows|
yield(rows.map(&:id))
with_connection do
dataset.select(:id).except(:includes, :preload, :eager_load).find_in_batches(**batch_options) do |rows|
yield(rows.map(&:id))
end
end
end

def count
dataset.except(:includes, :preload, :eager_load, :group, :order, :limit, :offset).count
with_connection do
dataset.except(:includes, :preload, :eager_load, :group, :order, :limit, :offset).count
end
end
alias_method :size, :count

Expand Down Expand Up @@ -127,6 +144,16 @@ def inspect

protected

def with_connection
if self.class.connect_with&.any?
::ActiveRecord::Base.connected_to(**self.class.connect_with) do
yield
end
else
yield
end
end

def batch_options
{
batch_size: batch_size
Expand Down
2 changes: 1 addition & 1 deletion lib/esse/active_record/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

module Esse
module ActiveRecord
VERSION = '0.3.6'
VERSION = '0.3.7'
end
end
66 changes: 66 additions & 0 deletions spec/esse/active_record/collection_connected_to_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
require 'spec_helper'

RSpec.describe Esse::ActiveRecord::Collection, '.connected_to' do
let(:collection_class) { Class.new(described_class) }

describe '.connected_to' do
it 'sets the connect_with' do
collection_class.connected_to(role: :reading, shard: :default)
expect(collection_class.connect_with).to eq(role: :reading, shard: :default)
end
end

describe '#each using custom connection', :sharding do
let(:collection_class) do
klass = Class.new(described_class)
klass.base_scope = -> { State }
klass.connected_to(role: :reading)
klass
end

it 'uses the custom connection' do
expect(ActiveRecord::Base).to receive(:connected_to).with(role: :reading).and_call_original
il_state = State.create!(name: 'Illinois', abbr_name: 'IL')

instance = collection_class.new
expect { |b| instance.each(&b) }.to yield_successive_args([il_state])
il_state.destroy
end
end

describe '#each_batch_ids using custom connection', :sharding do
let(:collection_class) do
klass = Class.new(described_class)
klass.base_scope = -> { State }
klass.connected_to(role: :reading)
klass
end

it 'uses the custom connection' do
expect(ActiveRecord::Base).to receive(:connected_to).with(role: :reading).and_call_original
il_state = State.create!(name: 'Illinois', abbr_name: 'IL')

instance = collection_class.new
expect { |b| instance.each_batch_ids(&b) }.to yield_successive_args([il_state.id])
il_state.destroy
end
end

describe '#count using custom connection', :sharding do
let(:collection_class) do
klass = Class.new(described_class)
klass.base_scope = -> { State }
klass.connected_to(role: :reading)
klass
end

it 'uses the custom connection' do
expect(ActiveRecord::Base).to receive(:connected_to).with(role: :reading).and_call_original
il_state = State.create!(name: 'Illinois', abbr_name: 'IL')

instance = collection_class.new
expect(instance.count).to eq(1)
il_state.destroy
end
end
end
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
require 'esse/rspec'

require 'support/config_helpers'
require 'support/sharding_hook'
require 'support/webmock'
require 'support/models'
require 'pry'
Expand Down
33 changes: 29 additions & 4 deletions spec/support/models.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,32 @@
if ENV['VERBOSE']
ActiveRecord::Base.logger = Logger.new($stdout)
end
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')

db_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s
db_path = '/tmp/esse-active_record.db'
db_config = {
adapter: 'sqlite3',
database: db_path,
}

if File.exist?(db_path)
File.delete(db_path)
end

if ::ActiveRecord.gem_version >= Gem::Version.new('6.0.0')
ActiveRecord::Base.configurations = { db_env => { primary: db_config, secondary: db_config } }
ActiveRecord::Base.establish_connection(:primary)
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
connects_to database: { writing: :primary, reading: :secondary }
end
else
ActiveRecord::Base.configurations = { db_env => db_config }
ActiveRecord::Base.establish_connection(db_config)
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
end

ActiveRecord::Schema.define do
self.verbose = false
Expand All @@ -27,7 +52,7 @@
end
end

class Animal < ActiveRecord::Base
class Animal < ApplicationRecord
end

class Dog < Animal
Expand All @@ -36,11 +61,11 @@ class Dog < Animal
class Cat < Animal
end

class State < ActiveRecord::Base
class State < ApplicationRecord
has_many :counties
end

class County < ActiveRecord::Base
class County < ApplicationRecord
belongs_to :state
end

Expand Down
21 changes: 21 additions & 0 deletions spec/support/sharding_hook.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

module ShardingHook
extend ActiveSupport::Concern

included do
around do |example|
if example.metadata[:sharding] &&
!ActiveRecord::Base.respond_to?(:connected_to)
skip 'ActiveRecord::Base.connected_to is not available in this version of Rails'
return
end

example.run
end
end
end

RSpec.configure do |config|
config.include ShardingHook
end