methodmissing-mysqlplus_adapter 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.textile ADDED
@@ -0,0 +1,97 @@
1
+ h1. Mysqlplus ActiveRecord Adapter
2
+
3
+ h2. Installation
4
+
5
+ Grab mysqlplus ....
6
+
7
+ git clone git://github.com/oldmoe/mysqlplus.git
8
+ cd mysqlplus
9
+ rake
10
+
11
+ ... and mysqlplus_adapter ...
12
+
13
+ sudo gem install methodmissing-mysqlplus_adapter -s http://gems.github.com
14
+
15
+ ... then update config/database.yml
16
+
17
+ production:
18
+ adapter: mysqlplus
19
+ database: myapp_production
20
+ username: root
21
+ password:
22
+ host: localhost
23
+ pool: 10
24
+
25
+ h2. Why Bother ?
26
+
27
+ The mysqlplus gem ( a fork of mysql-2.8.4pre ) exposes an asynchronous API that lends itself very well to decoupling the stock Mysql protocol into full duplex in a multi-threaded environment.
28
+
29
+ The ever popular "mysql ruby":http://www.tmtm.org/en/mysql/ruby/ do *not* schedule Threads and thus lock the whole MRI interpreter for any database I/O from within a given process.
30
+
31
+ Rails since release 2.2 support connection pooling and Thread safety ( at the framework level mind you, plugins, gems and user code aside ) through :
32
+
33
+ config.threadsafe!
34
+
35
+ This configuration hook removes the global Dispatcher lock and yields one connection from the pool per request / response cycle.
36
+
37
+ You'll need mysqplus and this adapter to get around the MRI lock with mysql ruby.
38
+
39
+ h2. Configuration Options
40
+
41
+ An additional connection specification element, *warmup* is available that attempts to establish the pooled connections in advance.This may be useful for high traffic environments where it makes sense to setup connections when bouncing the App and not let initial incoming requests be hogged with Mysql connection overheads.
42
+
43
+ production:
44
+ adapter: mysqlplus
45
+ database: myapp_production
46
+ username: root
47
+ password:
48
+ host: localhost
49
+ pool: 10
50
+ warmup: true
51
+
52
+ h3. Deferrable Results
53
+
54
+ Deferred results simulate lazy loading in a background Thread, through another Mysql connection, other than the one the current Thread has acquired.This type of workflow assumes a decent Connection Pool size of 5 to 10 connections.
55
+
56
+ # Immediate yield control back to the current Thread as the query is pushed to the background.
57
+ # Yields an instance of ActiveRecord::Deferrable::Result
58
+ #
59
+ Post.find( :first, :defer => true )
60
+
61
+ A deferred result blocks when any method's invoked on the result set right away.
62
+
63
+ Post.find( :first, :defer => true ).title
64
+
65
+ This concept is quite useful in an MVC context, allowing the controller to fetch results, defer fetching them to the background and reference them in the view, allowing an undefined period / time slice during which rendering, template setup etc. may occur.
66
+
67
+ class PostsController
68
+
69
+ def index
70
+ # No blocking, executes in the background, yields a deferrable result.
71
+ #
72
+ @posts = Posts.published.find(:all, :defer => true )
73
+ end
74
+
75
+ end
76
+
77
+ Since ActiveRecord 2.1 preloading favors multiple efficient queries to cumbersome and mostly slow JOINs.Those secondary queries can easily be pushed down different connections.
78
+
79
+ # Use 3 connections from the pool : 1 x Post, 1 x Comment and 1 x Vote
80
+ #
81
+ Post.find(:all, :limit => 10, :include => [:comments, :votes], :defer => true )
82
+
83
+ h2. Garbage Collection
84
+
85
+ There's some experimental GC patches "available":http://github.com/oldmoe/mysqlplus/tree/with_async_validation - the mysql ruby gem forces GC every 20 queries, that's a guaranteed GC cycle every 5th request for a request with a 4 query overhead.
86
+
87
+ h2. Stability
88
+
89
+ In (pre)-production use at a handful of sites and the test suite is designed to run against the existing ActiveRecord suite.
90
+
91
+ h2. TODO
92
+
93
+ * Experiment with turning off query_with_result for certain queries.
94
+
95
+ * Deferred inserts / updates - *dangerous* INSERT DELAYED for Innodb
96
+
97
+ * 1.9 compatibility testing
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 1
3
+ :major: 1
4
+ :minor: 0
@@ -0,0 +1,112 @@
1
+ begin
2
+ require_library_or_gem('mysqlplus')
3
+ rescue LoadError
4
+ $stderr.puts <<-EOS
5
+ The mysqlplus gem is required!
6
+ 'git clone git@github.com:oldmoe/mysqlplus.git && cd mysqlplus && rake'
7
+ There's some experimental GC patches available @ http://github.com/oldmoe/mysqlplus/tree/with_async_validation - the mysql gem forces GC every 20 queries, that's a guaranteed GC cycle every 5th request for a request with a 4 query overhead.
8
+ EOS
9
+ exit
10
+ end
11
+
12
+ begin
13
+ require_library_or_gem('fastthread')
14
+ rescue => LoadError
15
+ $stderr.puts "'gem install fastthread' for better performance"
16
+ end
17
+
18
+ [ "mysqlplus_adapter/connection_pool",
19
+ "mysql_adapter",
20
+ "mysqlplus_adapter/deferrable/result",
21
+ "mysqlplus_adapter/deferrable/macro" ].each{|l| require "active_record/connection_adapters/#{l}" }
22
+
23
+ module ActiveRecord
24
+ module ConnectionAdapters
25
+ class MysqlplusAdapter < ActiveRecord::ConnectionAdapters::MysqlAdapter
26
+
27
+ DEFERRABLE_SQL = /^(INSERT|UPDATE|ALTER|DROP|SELECT|DELETE|RENAME|REPLACE|TRUNCATE)/i.freeze
28
+
29
+ # Accessor for the raw connection socket for integration with EventMachine etc.
30
+ #
31
+ def socket
32
+ @connection.socket
33
+ end
34
+
35
+ def execute(sql, name = nil, skip_logging = false) #:nodoc:
36
+ if skip_logging
37
+ @connection.c_async_query( sql )
38
+ else
39
+ log("(Socket #{socket.to_s}) #{sql}",name) do
40
+ @connection.c_async_query( sql )
41
+ end
42
+ end
43
+ end
44
+
45
+ # Determine if a given SQL snippet is deferrable to another Thread.
46
+ #
47
+ def deferrable?( sql )
48
+ !open_transactions? &&
49
+ initialized? &&
50
+ deferrable_sql?( sql )
51
+ end
52
+
53
+ # Determine if the raw SQL can be deferred.This excludes changing users,
54
+ # retrieving column information, per connection configuration etc.
55
+ #
56
+ def deferrable_sql?( sql )
57
+ sql =~ DEFERRABLE_SQL
58
+ end
59
+
60
+ # Only support deferring connections once the framework has been initialized.
61
+ #
62
+ def initialized?
63
+ Object.const_defined?( 'Rails' ) && ::Rails.initialized?
64
+ end
65
+
66
+ # Are there any open transactions ?
67
+ #
68
+ def open_transactions?
69
+ open_transactions != 0
70
+ end
71
+
72
+ private
73
+
74
+ def configure_connection #:nodoc:
75
+ super
76
+ @connection.disable_gc = true if disable_gc?
77
+ end
78
+
79
+ # See http://github.com/oldmoe/mysqlplus/tree/with_async_validation
80
+ def disable_gc? #:nodoc:
81
+ @connection.respond_to?( :disable_gc= )
82
+ end
83
+
84
+ end
85
+ end
86
+ end
87
+
88
+ module ActiveRecord
89
+ class << Base
90
+
91
+ def mysqlplus_connection(config)
92
+ config = config.symbolize_keys
93
+ host = config[:host]
94
+ port = config[:port]
95
+ socket = config[:socket]
96
+ username = config[:username] ? config[:username].to_s : 'root'
97
+ password = config[:password].to_s
98
+
99
+ if config.has_key?(:database)
100
+ database = config[:database]
101
+ else
102
+ raise ArgumentError, "No database specified. Missing argument: database."
103
+ end
104
+
105
+ mysql = Mysql.init
106
+ mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey]
107
+
108
+ ConnectionAdapters::MysqlplusAdapter.new(mysql, logger, [host, username, password, database, port, socket], config)
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,40 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ class ConnectionPool
4
+
5
+ attr_reader :connections,
6
+ :checked_out,
7
+ :reserved_connections
8
+
9
+ def initialize(spec)
10
+ @spec = spec
11
+ # The cache of reserved connections mapped to threads
12
+ @reserved_connections = {}
13
+ # The mutex used to synchronize pool access
14
+ @connection_mutex = Monitor.new
15
+ @queue = @connection_mutex.new_cond
16
+ # default 5 second timeout
17
+ @timeout = spec.config[:wait_timeout] || 5
18
+ # default max pool size to 5
19
+ @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
20
+ @connections = []
21
+ @checked_out = []
22
+ # warmup hook
23
+ warmup! if spec.config[:warmup]
24
+ end
25
+
26
+ private
27
+
28
+ # Establish ( warmup ) all connections for this pool in advance.
29
+ #
30
+ def warmup!
31
+ @connection_mutex.synchronize do
32
+ 1.upto(@size) do
33
+ @connections << new_connection
34
+ end
35
+ end
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,99 @@
1
+ module ActiveRecord
2
+ module Deferrable
3
+ module Macro
4
+
5
+ class << self
6
+
7
+ def install!
8
+ ActiveRecord::Base.send :extend, SingletonMethods
9
+ ar_eigenclass::VALID_FIND_OPTIONS << :defer
10
+ alias_deferred :find, :find_by_sql, :preload_associations
11
+ end
12
+
13
+ private
14
+
15
+ def ar_eigenclass #:nodoc:
16
+ @@ar_eigneclass ||= (class << ActiveRecord::Base; self; end)
17
+ end
18
+
19
+ def alias_deferred( *method_signatures ) #:nodoc:
20
+ method_signatures.each do |method_signature|
21
+ ar_eigenclass.alias_method_chain method_signature, :defer
22
+ end
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+
29
+ module SingletonMethods
30
+
31
+ # !! EXPERIMENTAL !!
32
+ # Since ActiveRecord 2.1, multiple lightweight queries is preferred to expensive
33
+ # JOINS when eager loading related models.In some use cases it's more performant
34
+ # to distribute those over multiple connections versus dispatching them on the same
35
+ # connection.
36
+ #
37
+ # ....
38
+ # Record.find(:first, :include => [:other. :another], :defer => true)
39
+ # ....
40
+ #
41
+ def preload_associations_with_defer(records, associations, preload_options={})
42
+ if preload_options.key?(:defer)
43
+ ActiveRecord::Deferrable::Result.new do
44
+ preload_associations_without_defer(records, associations, preload_options)
45
+ end
46
+ else
47
+ preload_associations_without_defer(records, associations, preload_options)
48
+ end
49
+ end
50
+
51
+ # Execute raw SQL in another Thread.Blocks only when the result is immediately
52
+ # referenced.
53
+ # ...
54
+ # Record.find_by_sql( "SELECT SLEEP (1)", true )
55
+ # ...
56
+ #
57
+ def find_by_sql_with_defer( sql, defer = false )
58
+ if defer
59
+ ActiveRecord::Deferrable::Result.new do
60
+ find_by_sql_without_defer( sql )
61
+ end
62
+ else
63
+ find_by_sql_without_defer( sql )
64
+ end
65
+ end
66
+
67
+ # Executes a query in another background Thread.Blocks only when the result is
68
+ # immediately referenced.
69
+ # ...
70
+ # Record.find( :first, :conditions => ['records.some_id >= ?', 100], :defer => true )
71
+ # ...
72
+ #
73
+ def find_with_defer( *args )
74
+ options = args.dup.extract_options!
75
+ if options.key?(:defer)
76
+ with_deferred_scope do
77
+ ActiveRecord::Deferrable::Result.new do
78
+ find_without_defer(*args)
79
+ end
80
+ end
81
+ else
82
+ find_without_defer(*args)
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ # Deferred finder scope
89
+ #
90
+ def with_deferred_scope( &block ) #:nodoc:
91
+ with_scope( { :find => { :defer => true } }, :merge, &block )
92
+ end
93
+
94
+ end
95
+
96
+ end
97
+ end
98
+
99
+ ActiveRecord::Deferrable::Macro.install!
@@ -0,0 +1,46 @@
1
+ module ActiveRecord
2
+ module Deferrable
3
+ class Result < ActiveSupport::BasicObject
4
+
5
+ # Represents a Lazy Loaded resultset.
6
+ # Any method calls would block if the result hasn't yet been processed in a background
7
+ # Thread.
8
+ #
9
+ def initialize( &deferrable )
10
+ defer!( deferrable )
11
+ end
12
+
13
+ # Calls a given procedure in a background Thread, on another
14
+ # connection from the pool.Guarantees that said connection is checked
15
+ # back in on completion.
16
+ #
17
+ def defer!( deferrable )
18
+ @result = Thread.new( deferrable ) do |deferrable|
19
+ begin
20
+ deferrable.call
21
+ rescue => exception
22
+ exception
23
+ ensure
24
+ ::ActiveRecord::Base.connection_pool.release_connection
25
+ end
26
+ end
27
+ self
28
+ end
29
+
30
+ # Delegates to the background Thread.
31
+ #
32
+ def method_missing(*args, &block)
33
+ @_result ||= @result.value
34
+ validate!
35
+ @_result.send(*args, &block)
36
+ end
37
+
38
+ # Re-raise any Exceptions from the background Thread.
39
+ #
40
+ def validate!
41
+ raise @_result if @_result.is_a?( Exception )
42
+ end
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,30 @@
1
+ print "Using mysqplus\n"
2
+ require "#{File.dirname(__FILE__)}/../../helper"
3
+ require_dependency "#{AR_TEST_SUITE}/models/course"
4
+ require 'logger'
5
+
6
+ ActiveRecord::Base.logger = Logger.new(StringIO.new)
7
+
8
+ # GRANT ALL PRIVILEGES ON activerecord_unittest.* to 'rails'@'localhost';
9
+ # GRANT ALL PRIVILEGES ON activerecord_unittest2.* to 'rails'@'localhost';
10
+
11
+ ActiveRecord::Base.configurations = {
12
+ 'arunit' => {
13
+ :adapter => 'mysqlplus',
14
+ :username => 'rails',
15
+ :encoding => 'utf8',
16
+ :database => 'activerecord_unittest',
17
+ :pool => 5,
18
+ :warmup => false
19
+ },
20
+ 'arunit2' => {
21
+ :adapter => 'mysqlplus',
22
+ :username => 'rails',
23
+ :database => 'activerecord_unittest2',
24
+ :pool => 5,
25
+ :warmup => false
26
+ }
27
+ }
28
+
29
+ ActiveRecord::Base.establish_connection 'arunit'
30
+ Course.establish_connection 'arunit2'
@@ -0,0 +1,15 @@
1
+ require "#{File.dirname(__FILE__)}/helper"
2
+ Mysqlplus::Test.prepare!
3
+
4
+ class DeferrableTest < Test::Unit::TestCase
5
+
6
+ def test_should_be_able_to_find_records_in_a_background_thread
7
+ assert_equal MysqlUser.find(:first, :defer => true), MysqlUser.find(:first)
8
+ assert_instance_of MysqlUser, MysqlUser.find(:first, :defer => true)
9
+ end
10
+
11
+ def test_should_be_able_to_find_records_by_sql_background_thread
12
+ assert_equal MysqlUser.find_by_sql("SELECT * FROM mysql.user WHERE User = 'root'", true), MysqlUser.find(:all, :conditions => ['user.User = ?', 'root'])
13
+ end
14
+
15
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,90 @@
1
+ require 'rubygems'
2
+ require 'active_support'
3
+ require 'activerecord'
4
+ ActiveRecord.load_all!
5
+
6
+ module Mysqlplus
7
+ class Test
8
+
9
+ MODELS_DIR = "#{File.dirname(__FILE__)}/models".freeze
10
+
11
+ class << self
12
+
13
+ def prepare!
14
+ require 'test/unit'
15
+ connect!
16
+ require_models()
17
+ end
18
+
19
+ def setup!
20
+ setup_constants!
21
+ setup_config!
22
+ end
23
+
24
+ def mysqlplus_connection
25
+ "#{File.dirname(__FILE__)}/connections/mysqlplus"
26
+ end
27
+
28
+ def active_record_test_files
29
+ returning([]) do |files|
30
+ files << glob( "#{AR_TEST_SUITE}/cases/**/*_test{,_mysqlplus}.rb" )
31
+ end.sort
32
+ end
33
+
34
+ def test_files
35
+ glob( "#{File.dirname(__FILE__)}/*_test.rb" )
36
+ end
37
+
38
+ private
39
+
40
+ def connect!
41
+ ::ActiveRecord::Base.establish_connection( :adapter => 'mysqlplus',
42
+ :username => 'root',
43
+ :database => 'mysql',
44
+ :pool => 5,
45
+ :warmup => true )
46
+ end
47
+
48
+ def require_models
49
+ Dir.entries( MODELS_DIR ).grep(/.rb/).each do |model|
50
+ require_model( model )
51
+ end
52
+ end
53
+
54
+ def require_model( model )
55
+ require "#{MODELS_DIR}/#{model}"
56
+ end
57
+
58
+ def setup_constants!
59
+ set_constant( 'MYSQL_DB_USER' ){ 'rails' }
60
+ set_constant( 'AR_TEST_SUITE' ) do
61
+ ENV['AR_TEST_SUITE'] || find_active_record_test_suite()
62
+ end
63
+ end
64
+
65
+ def setup_config!
66
+ unless Object.const_defined?( 'MIGRATIONS_ROOT' )
67
+ require "#{::AR_TEST_SUITE}/config"
68
+ end
69
+ end
70
+
71
+ def set_constant( constant )
72
+ Object.const_set(constant, yield ) unless Object.const_defined?( constant )
73
+ end
74
+
75
+ def find_active_record_test_suite
76
+ returning( ($:).grep( /activerecord/ ).last.split('/') ) do |ar_ts|
77
+ ar_ts.pop
78
+ ar_ts << 'test'
79
+ end.join('/')
80
+ end
81
+
82
+ def glob( pattern )
83
+ Dir.glob( pattern )
84
+ end
85
+
86
+ end
87
+ end
88
+ end
89
+
90
+ Mysqlplus::Test.setup!
@@ -0,0 +1,3 @@
1
+ class MysqlUser < ActiveRecord::Base
2
+ set_table_name 'user'
3
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: methodmissing-mysqlplus_adapter
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - "Lourens Naud\xC3\xA9"
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-02-07 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: ActiveRecord Mysqlplus Adapter
17
+ email: lourens@methodmissing.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - README.textile
26
+ - VERSION.yml
27
+ - lib/active_record
28
+ - lib/active_record/connection_adapters
29
+ - lib/active_record/connection_adapters/mysqlplus_adapter
30
+ - lib/active_record/connection_adapters/mysqlplus_adapter/connection_pool.rb
31
+ - lib/active_record/connection_adapters/mysqlplus_adapter/deferrable
32
+ - lib/active_record/connection_adapters/mysqlplus_adapter/deferrable/macro.rb
33
+ - lib/active_record/connection_adapters/mysqlplus_adapter/deferrable/result.rb
34
+ - lib/active_record/connection_adapters/mysqlplus_adapter.rb
35
+ - test/connections
36
+ - test/connections/mysqlplus
37
+ - test/connections/mysqlplus/connection.rb
38
+ - test/deferrable_test.rb
39
+ - test/helper.rb
40
+ - test/models
41
+ - test/models/mysql_user.rb
42
+ has_rdoc: true
43
+ homepage: http://github.com/methodmissing/mysqplus_adapter
44
+ post_install_message:
45
+ rdoc_options:
46
+ - --inline-source
47
+ - --charset=UTF-8
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ version:
62
+ requirements: []
63
+
64
+ rubyforge_project:
65
+ rubygems_version: 1.2.0
66
+ signing_key:
67
+ specification_version: 2
68
+ summary: ActiveRecord Mysqlplus Adapter
69
+ test_files: []
70
+