Sunday, April 15, 2007

[PLUGIN RELEASE] ActsAsReadonlyable

Introduction

ActsAsReadonlyable adds support of multiple read-only slave databases to ActiveRecord models. When a model is marked with acts_as_readonlyable, some of AR finders are overridden to run against a slave DB. The supported finders are find, find_by_sql, count_by_sql, find_[all_]by_*, and reload.

Finders can be forced to fall back to a default DB by passing the :readonly flag set to false.

Disclaimer

As our blog post points out, we wrote this plugin in preparation to using slave DBs but we are not going to have those until May 2007. So even though the code is covered with tests (see svn://rubyforge.org/var/svn/acts-as-with-ro/trunk/test/unit/read_write_model_test.rb), it has not been used outside of those. We would have a discovery period in May when the code is likely to be improved so for now you can use is at your own risk. Meanwhile, we would be happy to fix any issue revealed. Drop us a line at rails-trunk [ at ] revolution DOT com.

Using this plugin should not be your first step in application optimization/scaling or even the second one. Before installing it make sure you understand the implication of leveraging multiple DBs (for example, the potential for cross DB joins).

Usage

Add acts_as_readonlyable to your models backed up by slave DBs. If you want to apply ActsAsReadonlyable to all models, add this or similar code at the end of config/environment.rb:


class << ActiveRecord::Base

def read_only_inherited(child)
child.acts_as_readonlyable :read_only
ar_inherited(child)
end

alias_method :ar_inherited, :inherited
alias_method :inherited, :read_only_inherited

end


Example

Sample DB Config


dbs:

database: master_db
host: master-host

read_only:
database: slave_db
host: slave-host


Note: There is no need for more than one read-only database configuration in your database.yml since you can leverage traditional load balancing solutions. If you still want to use database.yml to spread the load, define multiple entries there and use acts_as_readonlyable [:first_read_only, :second_read_only].


Sample Model

class Fruit < ActiveRecord::Base
acts_as_readonlyable :read_only
end


Usage

r = Fruit.find(:first) # executes against the read-only db
r.field = 'value'
r.save! # executes against the read/write db

r.reload # executes against the read-only db
r.reload(:readonly => false) # executes against the read/write db


Installation

As plugin:
script/plugin install svn://rubyforge.org/var/svn/acts-as-with-ro/trunk/vendor/plugins/acts_as_readonlyable


License

ActsAsReadonlyable released under the MIT license.


Support

The plugin RubyForge page is http://rubyforge.org/projects/acts-as-with-ro

16 comments:

Anonymous said...

Is readonlyable a word? I'm not so sure.

Anonymous said...

It's at least as much a word as numericality is.

Aaron Batalion said...

Yes, we know readonlyable is not a word. It was mostly in jest. Are the other 300 acts_as_*able plugins grammatically correct? :) If you have suggestions for another name, feel free to comment below.

Anonymous said...

class Post
is_readonly
end

?

Herval said...

readonlyable! Now that was creativelyable...
why not just acts_as_readonly, or is_readonly? Just to feel like part of the cool pack that says acts_as_crappable?

Unknown said...

Nah, dont change the name. acts_as_readonlyable is completely clear and mildly amusing.

Anonymous said...

acts_as_twitterable? :p

Not sure I understand the comment about cross-DB joins. Are you suggesting that as an alternate solution, or saying use of this plugin could cause cross-DB joins to occur? I think I'm missing something

Val Aleksenko said...

It was just a reminder there is often a price to pay when moving toward the master-slave model. Like a replication lag, so, for example, save & reload might return different results then before, or cross DB-joins that worked before might stop working because one of the DB is now on a slave host and your DB engine does not support such configurtion, etc.

Anonymous said...

Some better name proposals: has_read_slave :read_only
find_in :read_only
for_finds_use :read_only

acts_as_readonlyable doesn't quite communicate for me, as the model doesn't act as readony, you can still save it.

Anyway, still a nice plugin. Thanks and Congrats.

Anonymous said...

You said: "We would have a discovery period in May when the code is likely to be improved so for now you can use is at your own risk."

Any results from the May work? I'm very interested in this plugin and I'd like to know if it's likely to go into a production environment (and hence have a better chance of being maintained across new Rails versions).

Val Aleksenko said...

Matt: yeah, we have been using it quite a lot in production since May and the number of applications converted to that approach increases every month. DBA found it to be the easiest way to boost the DB performance since it requires no effort from the dev team. It is being used in production with 1.1.6 and, lately, with 1.2.2.

Anonymous said...

Another possible approach: mysql-proxy read/write splitting. I've given it an informal test run and it works as advertised.

Anonymous said...

Sorry, this is probably a very stupid question, but I am a little confused with the syntax for the database.yml file. I tried what is documented and a host of other permutations but no luck.

I have configured my database.yml as the following:

development:
adapter: mysql
database: master_development
username: root
password:
host: localhost

slave_db:
adapter: mysql
database: slave_development
username: root
password:
host: localhost

dbs:

database: master_development
host: localhost

read_only:
database: slave_development
host: localhost

But this and a variety of other combinations do not work. Any help would be appreciated. Thanks.

Marc Byrd said...

The page says the plugin was not planned for production until some time later. Is it being used in production now by the originator or anyone else (besides us!)? If so, it would be great if the page could be updated.

Thanks!

Anonymous said...

the plugin only does aggregate functions on the slaves if you're using #count_by_sql, otherwise all counts, sums, mins, maxs, etc get sent off to the master (which theoretically should produce more correct results, but could block the table for updates).

I added this method locally to send all aggregates to the slaves as well:


def calculate(operation, column_name, options = {})
run_on_readonly_db { super(operation, column_name, options) }
end

Anonymous said...

The yml config is nested, in response to an earlier comment.

Here's what it should look like:

http://pastie.org/private/q1uoijsllbii7awy6un5a