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

Rails 4 #5

Closed
wants to merge 3 commits into from
Closed

Rails 4 #5

wants to merge 3 commits into from

Conversation

lukkry
Copy link
Contributor

@lukkry lukkry commented Aug 25, 2013

This PR adds support for Rails 4.

I try to highlight the biggest changes and describe how I approached them, it's likely going to be a long PR message, so bear with me ;)

  • ConnectionSpecification lives in a different namespace, so ActiveRecordShards::ConnectionSpecification was introduced.
# lib/active_record_shards.rb
if ActiveRecord::VERSION::STRING >= "4.0.0"
  ActiveRecordShards::ConnectionSpecification =  ActiveRecord::ConnectionAdapters::ConnectionSpecification
else
  ActiveRecordShards::ConnectionSpecification = ActiveRecord::Base::ConnectionSpecification
end
  • ConnectionHandler was slightly redesigned and klass.name is called more often now (it's a key in @class_to_pool => all the methods where it's used are currently overrided and klass.connection_pool_name is used instead). If we want to stick with the current approach we'll have to override many more methods from ConnectionHandler. Therefore, I created ActiveRecordDecorator to avoid it. This class decorates ActiveRecord::Base object and returns connection pool name instead of its name. It allows us to override ConnectionHandler methods without changing or copying any logic:
# lib/active_record_shards/connection_pool.rb
def establish_connection_with_connection_pool_name(owner, spec)
  owner = ActiveRecordDecorator.new(owner)
  establish_connection_without_connection_pool_name(owner, spec)
end
alias_method_chain :establish_connection, :connection_pool_name
  • with_scope is depracated => scoping is used instead
# lib/active_record_shards/connection_switcher.rb
if ActiveRecord::VERSION::MAJOR == 2
  with_scope({:find => {:readonly => true}}, &block)
else
  readonly.scoping(&block)
end
  • ConnectionHandler#connection_pools is depracated, it is fairly simple, so I've just copy-pasted it
# lib/active_record_shards/connection_switcher.rb
def connected_to_shard?
  if ActiveRecord::VERSION::MAJOR == 4
    specs_to_pools = Hash[connection_handler.connection_pool_list.map { |pool| [pool.spec, pool] }]
  else
    specs_to_pools = connection_handler.connection_pools
  end

  specs_to_pools.has_key?(connection_pool_key)
end

Let me know what you think, appreciate any feedback! ;)

@lukkry
Copy link
Contributor Author

lukkry commented Aug 25, 2013

cc @eac @staugaard

@staugaard
Copy link
Contributor

@osheroff

#
class ActiveRecordDecorator < SimpleDelegator
def name
if __getobj__.respond_to? :connection_pool_name
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this object is never used except to decorate ActiveRecord::Base, so it doesn't seem like this check is needed, yeah?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's String when we call connection_handler.establish_connection method with explicit connection_pool_name, e.g. here:
https://github.com/zendesk/active_record_shards/blob/master/lib/active_record_shards/connection_switcher.rb#L185

In other words:

if __getobj__.is_a? String
  # assume it's connection_pool_name already
  __getobj__
else
  # assume it's ActiveRecord::Base object
  __getobj__.connection_pool_name
 end

I guess, ActiveRecordDecorator is a poor name and documentation is not 100% accurate, I'll change it ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ActiveRecordDecorator object should quack only with name method, so sth like OpenStruct might be another possible option

if args[0].respond_to? :connection_pool_name
  args[0] = OpenStruct.new(:name => args[0].connection_pool_name)
elsif args[0].is_a? String
  args[0] = OpenStruct.new(:name => args[0])
end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it relies on fact, that ConnectionHandler calls only name method on klass|owner

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how it may look like with OpenStruct: https://gist.github.com/lukkry/6370855
We can remove ActiveRecordDecorator entirely then.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I prefer the OpenStruct approach. It's more clear that "hey, we're just doing this to fool some methods we don't want to override".

@osheroff
Copy link
Contributor

travis needs to be configured to actually build the appraisal

@osheroff
Copy link
Contributor

Overall these changes look good, although I will say I found the decorator stuff to be pretty hard to read at first.

@lukkry
Copy link
Contributor Author

lukkry commented Aug 28, 2013

Agreed, it's not super obvious. Actually, I've started with 2 separate files, each for a different rails version: connection_pool_rails3.rb with 2 overridden methods and connection_pool_rails4.rb with 5 overridden methods, and then I moved to Decorator .

imo it's not perfect but still better than maintaining different files for different rails version and overriding lots of AR internals

else
args[0].connection_pool_name
end
args[0] = OpenStruct.new(:name => name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since OpenStruct has some performance issues (http://ruby-doc.org/, http://jamesgolick.com/), I must ask: could this be done with a Struct instead, i.e.

args[0] = Struct.new(:name).new(name)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lukkry
Copy link
Contributor Author

lukkry commented Aug 29, 2013

I've just found out that some of the methods like: all, scoped, unscoped dont work on sharded DB (only in Rails 4). Probably, sth similar to this #4. I'll add more tests around it.

@lukkry
Copy link
Contributor Author

lukkry commented Sep 3, 2013

Above errors are not Rails 4 related, See: #6

alias_method_chain :establish_connection, :connection_pool_name
end
module ActiveRecordShards
ConnectionPoolNameDecorator = Struct.new(:name)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found a place where superclass method is called on this object: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#L576
but imo we don't expect this code to be executed, pool_for(klass) always returns a pool. Otherwise we probably should go back to SimpleDelegator

Can someone confirm it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think superclass will be called. Can we just inject this into the Struct definition too?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping @lukkry

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@osheroff, I'm not sure if I get what do you want to inject?

I think that superclass is never called because pool_for method ultimately fetches data from @owner_to_pool variable and if our connection pool names are not there, there is nothing what we can do about it but raise ConnectionNotEstablished. It looks like Rails 4 assumes that there is always a key for ActiveRecord::Base in @owner_to_pool

Loading development environment (Rails 4.0.0)
2.0.0-p247 :001 > ActiveRecord::Base.connection_handler.send(:owner_to_pool).keys
 => ["ActiveRecord::Base"]
2.0.0-p247 :002 > Account.all
  Account Load (0.1ms)  SELECT "accounts".* FROM "accounts"
 => #<ActiveRecord::Relation []>
2.0.0-p247 :003 > ActiveRecord::Base.connection_handler.remove_connection(ActiveRecord::Base)
 => {:adapter=>"sqlite3", :database=>"/Users/lukkry/projects/mango_test/db/development.sqlite3", :pool=>5, :timeout=>5000}
2.0.0-p247 :004 > Account.all
ActiveRecord::ConnectionNotEstablished: ActiveRecord::ConnectionNotEstablished
        from /Users/lukkry/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-4.0.0/lib/active_record/connection_adapters/abstract/connection_pool.rb:546:in `retrieve_connection'
        from /Users/lukkry/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-4.0.0/lib/active_record/connection_handling.rb:79:in `retrieve_connection'
        from /Users/lukkry/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-4.0.0/lib/active_record/connection_handling.rb:53:in `connection'
        from /Users/lukkry/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-4.0.0/lib/active_record/querying.rb:36:in `find_by_sql'
        from /Users/lukkry/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-4.0.0/lib/active_record/relation.rb:585:in `exec_queries'
        from /Users/lukkry/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-4.0.0/lib/active_record/relation.rb:471:in `load'
        from /Users/lukkry/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-4.0.0/lib/active_record/relation.rb:220:in `to_a'
        from /Users/lukkry/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-4.0.0/lib/active_record/relation.rb:573:in `inspect'
        from /Users/lukkry/.rvm/gems/ruby-2.0.0-p247/gems/railties-4.0.0/lib/rails/commands/console.rb:90:in `start'
        from /Users/lukkry/.rvm/gems/ruby-2.0.0-p247/gems/railties-4.0.0/lib/rails/commands/console.rb:9:in `start'
        from /Users/lukkry/.rvm/gems/ruby-2.0.0-p247/gems/railties-4.0.0/lib/rails/commands.rb:64:in `<top (required)>'
        from bin/rails:4:in `require'
        from bin/rails:4:in `<main>'

so it should be safe to assume that there is always a key for our connection pool name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I run tests on master and this line is never executed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: superclass/rails3 yeah I guess so ;>
it wraps self.class into ConnectionPoolNameDecorator all the time and finally fails with:

<"undefined method `connection_pool_name' for ActiveRecordShards::ConnectionPoolNameDecorator:Class">

Don't have any reasonable solution yet.

I'm using this test (it's probably good only for rails 3 & 4):

  it "raises an exception if a connection is not found" do
    ActiveRecord::Base.on_shard(0) do
      ActiveRecord::Base.connection_handler.remove_connection(Ticket)
      assert_raises(ActiveRecord::ConnectionNotEstablished) do
        ActiveRecord::Base.connection_handler.retrieve_connection_pool(Ticket)
        assert_using_database('ars_test_shard0', Ticket)
      end
    end
  end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one possible solution is to override retrive_connection_pool methods, but this is sth what I want to avoid from the beginning ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@osheroff I don't see what role sockets play here. As far as I understand pool_from_any_process_for, it uses only copied memory from a parent process. Is it how you see it or I'm missing sth obvious here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In unix, when a child is forked off from the parent, it inherits its
parent's file descriptors (and thus sockets), and it's very bad to
re-use these in the child process. The rails code is probably ensuring
that children don't re-use their parents' sockets, and I wanted to make
sure that it works as expected.

On Mon, Sep 30, 2013 at 02:07:44PM -0700, Lukasz Krystkowiak wrote:

  • end
    -end

-ActiveRecord::Base.singleton_class.class_eval do

  • def establish_connection_with_connection_pool_name(spec = nil)
  • case spec
  • when ActiveRecord::Base::ConnectionSpecification
  •  connection_handler.establish_connection(connection_pool_name, spec)
    
  • else
  •  establish_connection_without_connection_pool_name(spec)
    
  • end
  • end
  • alias_method_chain :establish_connection, :connection_pool_name
    -end
    +module ActiveRecordShards
  • ConnectionPoolNameDecorator = Struct.new(:name)

@osheroff I don't see what role sockets play here. As far as I understand pool_from_any_process_for, it uses only copied memory from a parent process. Is it how you see it or I'm missing sth obvious here?


Reply to this email directly or view it on GitHub:
https://github.com/zendesk/active_record_shards/pull/5/files#r6671278

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: superclass/rails3 I've overriden retrieve_connection_pool ;( and removed unneeded code (with superclass)

Before

# Rails 4
      def retrieve_connection_pool(klass)
        class_to_pool[klass.name] ||= begin
          until pool = pool_for(klass)
            klass = klass.superclass
            break unless klass <= Base
          end

          class_to_pool[klass.name] = pool
        end
      end

# Rails 3 & 2
      def retrieve_connection_pool(klass)
        pool = @connection_pools[klass.name]
        return pool if pool
        return nil if ActiveRecord::Base == klass
        retrieve_connection_pool klass.superclass
      end

After

ActiveRecord::ConnectionAdapters::ConnectionHandler.class_eval do
  if ActiveRecord::VERSION::MAJOR >= 4
    def retrieve_connection_pool(klass)
      class_to_pool[klass.connection_pool_name] ||= pool_for(klass)
    end
  else
    def retrieve_connection_pool(klass)
      (@class_to_pool || @connection_pools)[klass.connection_pool_name]
    end
  end
end

@lukkry
Copy link
Contributor Author

lukkry commented Sep 14, 2013

this is ready for re-review

# Rails version).
#
# It takes the first argument, ActiveRecord::Base object or
# String (connection_pool_name), converts it in OpenStruct
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove "Open" from that comment :-)

@osheroff
Copy link
Contributor

osheroff commented Nov 5, 2013

hey @lukkry, I didn't see that you had added new commits. Think this is ready to merge (or at least pre-release)?

@lukkry
Copy link
Contributor Author

lukkry commented Nov 5, 2013

I squashed the commits, I think it's ready to try it as pre-release ;)

@lukkry
Copy link
Contributor Author

lukkry commented Nov 7, 2013

@osheroff should I go ahead and merge it?

@osheroff
Copy link
Contributor

osheroff commented Nov 7, 2013

I went ahead and released 3.0.0.beta1 from this branch; try that in a real project or two before merge? I was in flight with testing it on classic when security issues got in the way, can resume next week....

@lukkry
Copy link
Contributor Author

lukkry commented Nov 7, 2013

ok, cool, I'll test it against classic as well

@@ -2,6 +2,8 @@ require 'bundler/setup'
require "appraisal"
require "bump/tasks"

Bundler::GemHelper.install_tasks
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

require 'bundler/gem_tasks' does the same

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is it better?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not much, but I like it better, a little shorter / just a require vs a
method call

On Mon, Jan 20, 2014 at 8:56 AM, Lukasz Krystkowiak <
notifications@github.com> wrote:

In Rakefile:

@@ -2,6 +2,8 @@ require 'bundler/setup'
require "appraisal"
require "bump/tasks"

+Bundler::GemHelper.install_tasks

why is it better?


Reply to this email directly or view it on GitHubhttps://github.com//pull/5/files#r9010711
.

Conflicts:
	Gemfile
	active_record_shards.gemspec
	lib/active_record_shards/default_slave_patches.rb
	test/schema.rb
@grosser
Copy link
Contributor

grosser commented Jan 21, 2014

is this ready to go now ? I want to play with rails 4 :)

@osheroff
Copy link
Contributor

The thing that was, and is blocking this is someone testing it with classic on acceptance.

@grosser
Copy link
Contributor

grosser commented Jan 21, 2014

can do!

@grosser
Copy link
Contributor

grosser commented Jan 21, 2014

@lukkry there should be no need to use your own fork

@lukkry
Copy link
Contributor Author

lukkry commented Jan 21, 2014

@grosser I pushed it all to rails4_support branch in this repo, https://github.com/zendesk/active_record_shards/tree/rails4_support, so we can work with it, instead of the fork.

Also, it would be cool to merge #10 first, and rebase everything on top of it ;)

@grosser
Copy link
Contributor

grosser commented Jan 21, 2014

Tried in on tddium + acceptance and seems to work nicely.

On Tue, Jan 21, 2014 at 12:43 PM, Lukasz Krystkowiak <
notifications@github.com> wrote:

@grosser https://github.com/grosser I pushed it all to rails4_support
branch in this repo,
https://github.com/zendesk/active_record_shards/tree/rails4_support, so
we can work with it, instead of the fork.

Also, it would be cool to merge #10https://github.com/zendesk/active_record_shards/pull/10first, and rebase everything on top of it ;)


Reply to this email directly or view it on GitHubhttps://github.com//pull/5#issuecomment-32961132
.

@grosser
Copy link
Contributor

grosser commented Jan 21, 2014

done, try to rebase!

@lukkry
Copy link
Contributor Author

lukkry commented Jan 21, 2014

I'm closing this in favor of #12

@lukkry lukkry closed this Jan 21, 2014
@lukkry lukkry mentioned this pull request Jan 21, 2014
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants