em-synchrony 0.3.0.beta.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
1
  Gemfile.lock
2
2
  .bundle
3
3
  misc
4
+ pkg
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ -I.
data/Gemfile CHANGED
@@ -3,13 +3,15 @@ source :gemcutter
3
3
  gem 'eventmachine', :git => 'git://github.com/eventmachine/eventmachine.git'
4
4
 
5
5
  group :development do
6
- gem 'rspec', '~> 2.0.0'
6
+ gem 'rspec'
7
7
  gem 'em-http-request', :git => 'git://github.com/igrigorik/em-http-request'
8
8
  gem 'remcached'
9
- gem 'em-mongo'
9
+ # gem 'em-mongo', :git => 'https://github.com/bcg/em-mongo.git'
10
+ gem 'activerecord', '>= 3.1.0.rc6'
11
+ gem 'em-mongo', '~> 0.3.6'
10
12
  gem 'bson_ext'
11
- gem 'mysqlplus'
12
- gem 'em-mysqlplus'
13
+ gem 'mysql2'
13
14
  gem 'em-redis', '~> 0.3.0'
15
+ gem 'em-hiredis'
14
16
  gem 'mongo'
15
- end
17
+ end
data/README.md CHANGED
@@ -1,141 +1,171 @@
1
1
  # EM-Synchrony
2
2
 
3
- Collection of convenience classes and primitives to help untangle evented code, plus a number of patched EM clients to make them Fiber aware. To learn more, please see: [Untangling Evented Code with Ruby Fibers](http://www.igvita.com/2010/03/22/untangling-evented-code-with-ruby-fibers). Word of warning: even though Fibers have been backported to Ruby 1.8.x, these classes assume Ruby 1.9.x (if you're using fibers in production, you should be on 1.9.x anyway).
3
+ Collection of convenience classes and primitives to help untangle evented code, plus a number of patched EM clients to make them Fiber aware. To learn more, please see: [Untangling Evented Code with Ruby Fibers](http://www.igvita.com/2010/03/22/untangling-evented-code-with-ruby-fibers).
4
4
 
5
- * Fiber aware connection pool with sync/async query support
6
- * Fiber aware iterator to allow concurrency control & mixing of sync / async
5
+ * Fiber aware ConnectionPool with sync/async query support
6
+ * Fiber aware Iterator to allow concurrency control & mixing of sync / async
7
7
  * Fiber aware async inline support: turns any async function into sync
8
- * Fiber aware multi-request interface for any callback enabled clients
8
+ * Fiber aware Multi-request interface for any callback enabled clients
9
9
  * Fiber aware TCPSocket replacement, powered by EventMachine
10
10
  * Fiber aware Thread, Mutex, ConditionVariable clases
11
11
  * Fiber aware sleep
12
12
 
13
13
  Supported clients:
14
14
 
15
- * em-http-request: .get, etc are synchronous, while .aget, etc are async
16
- * em-memcached & remcached: .get, etc, and .multi_* methods are synchronous
17
- * em-redis: synchronous connect, .a{cmd} are async
18
- * em-mysqlplus: .query is synchronous, while .aquery is async
19
- * em-mongo: .find, .first are synchronous
20
- * mongoid: all functions synchronous, plus Rails compatability
21
- * bitly v2 and v3: synchronous api calls with EM::HttpRequest.
15
+ * [mysql2](http://github.com/igrigorik/em-synchrony/blob/master/spec/mysql2_spec.rb): .query is synchronous, while .aquery is async (see specs)
16
+ * [activerecord](http://github.com/igrigorik/em-synchrony/blob/master/spec/activerecord_spec.rb): require synchrony/activerecord, set your AR adapter to em_mysql2 and you should be good to go
17
+ * [em-http-request](http://github.com/igrigorik/em-synchrony/blob/master/spec/http_spec.rb): .get, etc are synchronous, while .aget, etc are async
18
+ * [em-memcached](http://github.com/igrigorik/em-synchrony/blob/master/spec/memcache_spec.rb) & [remcached](http://github.com/igrigorik/em-synchrony/blob/master/spec/remcached_spec.rb): .get, etc, and .multi_* methods are synchronous
19
+ * [em-mongo](http://github.com/igrigorik/em-synchrony/blob/master/spec/em-mongo_spec.rb): .find, .first are synchronous
20
+ * [mongoid](http://github.com/igrigorik/em-synchrony/blob/master/spec/mongo_spec.rb): all functions synchronous, plus Rails compatibility
21
+ * em-jack: a[method]'s are async, and all regular jack method's are synchronous
22
22
 
23
+ Other clients with native Fiber support:
24
+
25
+ * redis: contains [synchrony code](https://github.com/ezmobius/redis-rb/blob/master/test/synchrony_driver.rb) right within the driver
26
+ * synchrony also supports [em-redis](http://github.com/igrigorik/em-synchrony/blob/master/spec/redis_spec.rb) and em-hiredis (see specs), but unless you specifically need either of those, use the official redis gem
23
27
 
24
28
  ## Fiber-aware Iterator: mixing sync / async code
29
+
25
30
  Allows you to perform each, map, inject on a collection of any asynchronous tasks. To advance the iterator, simply call iter.next, or iter.return(result). The iterator will not exit until you advance through the entire collection. Additionally, you can specify the desired concurrency level! Ex: crawling a web-site, but you want to have at most 5 connections open at any one time.
26
31
 
27
- require "em-synchrony/em-http"
28
- EM.synchrony do
29
- concurrency = 2
30
- urls = ['http://url.1.com', 'http://url2.com']
32
+ ```ruby
33
+ require "em-synchrony"
34
+ require "em-synchrony/em-http"
35
+
36
+ EM.synchrony do
37
+ concurrency = 2
38
+ urls = ['http://url.1.com', 'http://url2.com']
39
+
40
+ # iterator will execute async blocks until completion, .each, .inject also work!
41
+ results = EM::Synchrony::Iterator.new(urls, concurrency).map do |url, iter|
42
+
43
+ # fire async requests, on completion advance the iterator
44
+ http = EventMachine::HttpRequest.new(url).aget
45
+ http.callback { iter.return(http) }
46
+ http.errback { iter.return(http) }
47
+ end
48
+
49
+ p results # all completed requests
50
+ EventMachine.stop
51
+ end
52
+ ```
53
+
54
+ Or, you can use FiberIterator to hide the async nature of em-http:
31
55
 
32
- # iterator will execute async blocks until completion, .each, .inject also work!
33
- results = EM::Synchrony::Iterator.new(urls, concurrency).map do |url, iter|
56
+ ```ruby
57
+ require "em-synchrony"
58
+ require "em-synchrony/em-http"
34
59
 
35
- # fire async requests, on completion advance the iterator
36
- http = EventMachine::HttpRequest.new(url).aget
37
- http.callback { iter.return(http) }
38
- end
60
+ EM.synchrony do
61
+ concurrency = 2
62
+ urls = ['http://url.1.com', 'http://url2.com']
63
+ results = []
39
64
 
40
- p results # all completed requests
41
- EventMachine.stop
65
+ EM::Synchrony::FiberIterator.new(urls, concurrency).each do |url|
66
+ resp = EventMachine::HttpRequest.new(url).get
67
+ results.push resp.response
42
68
  end
43
69
 
70
+ p results # all completed requests
71
+ EventMachine.stop
72
+ end
73
+ ```
74
+
44
75
  ## Fiber-aware ConnectionPool shared by a fiber:
45
76
  Allows you to create a pool of resources which are then shared by one or more fibers. A good example is a collection of long-lived database connections. The pool will automatically block and wake up the fibers as the connections become available.
46
77
 
47
- require "em-synchrony/em-mysqlplus"
48
- EventMachine.synchrony do
49
- db = EventMachine::Synchrony::ConnectionPool.new(size: 2) do
50
- EventMachine::MySQL.new(host: "localhost")
51
- end
78
+ ```ruby
79
+ require "em-synchrony"
80
+ require "em-synchrony/mysql2"
52
81
 
53
- multi = EventMachine::Synchrony::Multi.new
54
- multi.add :a, db.aquery("select sleep(1)")
55
- multi.add :b, db.aquery("select sleep(1)")
56
- res = multi.perform
82
+ EventMachine.synchrony do
83
+ db = EventMachine::Synchrony::ConnectionPool.new(size: 2) do
84
+ Mysql2::EM::Client.new
85
+ end
57
86
 
58
- p "Look ma, no callbacks, and parallel MySQL requests!"
59
- p res
87
+ multi = EventMachine::Synchrony::Multi.new
88
+ multi.add :a, db.aquery("select sleep(1)")
89
+ multi.add :b, db.aquery("select sleep(1)")
90
+ res = multi.perform
60
91
 
61
- EventMachine.stop
62
- end
92
+ p "Look ma, no callbacks, and parallel MySQL requests!"
93
+ p res
94
+
95
+ EventMachine.stop
96
+ end
97
+ ```
63
98
 
64
- ## Fiber-aware Multi inteface: parallel HTTP requests
99
+ ## Fiber-aware Multi interface: parallel HTTP requests
65
100
  Allows you to fire simultaneous requests and wait for all of them to complete (success or error) before advancing. Concurrently fetching many HTTP pages at once is a good example; parallel SQL queries is another. Technically, this functionality can be also achieved by using the Synchrony Iterator shown above.
66
101
 
67
- require "em-synchrony/em-http"
68
- EventMachine.synchrony do
69
- multi = EventMachine::Synchrony::Multi.new
70
- multi.add :a, EventMachine::HttpRequest.new("http://www.postrank.com").aget
71
- multi.add :b, EventMachine::HttpRequest.new("http://www.postrank.com").apost
72
- res = multi.perform
102
+ ```ruby
103
+ require "em-synchrony"
104
+ require "em-synchrony/em-http"
105
+ EventMachine.synchrony do
106
+ multi = EventMachine::Synchrony::Multi.new
107
+ multi.add :a, EventMachine::HttpRequest.new("http://www.postrank.com").aget
108
+ multi.add :b, EventMachine::HttpRequest.new("http://www.postrank.com").apost
109
+ res = multi.perform
73
110
 
74
- p "Look ma, no callbacks, and parallel HTTP requests!"
75
- p res
111
+ p "Look ma, no callbacks, and parallel HTTP requests!"
112
+ p res
76
113
 
77
- EventMachine.stop
78
- end
114
+ EventMachine.stop
115
+ end
116
+ ```
79
117
 
80
118
  ## Fiber-aware & EventMachine backed TCPSocket:
81
119
  This is dangerous territory - you've been warned. You can patch your base TCPSocket class to make any/all libraries depending on TCPSocket be actually powered by EventMachine and Fibers under the hood.
82
120
 
83
- require "lib/em-synchrony"
84
- require "net/http"
121
+ ```ruby
122
+ require "em-synchrony"
123
+ require "lib/em-synchrony"
124
+ require "net/http"
85
125
 
86
- EM.synchrony do
87
- # replace default Socket code to use EventMachine Sockets instead
88
- TCPSocket = EventMachine::Synchrony::TCPSocket
126
+ EM.synchrony do
127
+ # replace default Socket code to use EventMachine Sockets instead
128
+ TCPSocket = EventMachine::Synchrony::TCPSocket
89
129
 
90
- Net::HTTP.get_print 'www.google.com', '/index.html'
91
- EM.stop
92
- end
130
+ Net::HTTP.get_print 'www.google.com', '/index.html'
131
+ EM.stop
132
+ end
133
+ ```
93
134
 
94
135
  ## Inline synchronization & Fiber sleep:
95
136
  Allows you to inline/synchronize any callback interface to behave as if it was a blocking call. Simply pass any callback object to Synchrony.sync and it will do the right thing: the fiber will be resumed once the callback/errback fires. Likewise, use Synchrony.sleep to avoid blocking the main thread if you need to put one of your workers to sleep.
96
137
 
97
- EM.synchrony do
98
- # pass a callback enabled client to sync to automatically resume it when callback fires
99
- result = EM::Synchrony.sync EventMachine::HttpRequest.new('http://www.gooogle.com/').aget
100
- p result
138
+ ```ruby
139
+ require "em-synchrony"
140
+ require "em-synchrony/em-http"
141
+ EM.synchrony do
142
+ # pass a callback enabled client to sync to automatically resume it when callback fires
143
+ result = EM::Synchrony.sync EventMachine::HttpRequest.new('http://www.gooogle.com/').aget
144
+ p result
101
145
 
102
- # pause exection for 2 seconds
103
- EM::Synchrony.sleep(2)
146
+ # pause exection for 2 seconds
147
+ EM::Synchrony.sleep(2)
104
148
 
105
- EM.stop
106
- end
149
+ EM.stop
150
+ end
151
+ ```
107
152
 
108
- ## Patched clients
153
+ ## Async ActiveRecord:
109
154
 
110
- EM-Synchrony ships with a number of patched Eventmachine clients, which allow you to patch your asynchronous drivers on the fly to behave as if they were actually blocking clients:
155
+ Allows you to use async ActiveRecord within Rails and outside of Rails (see [async-rails](https://github.com/igrigorik/async-rails)). If you need to control the connection pool size, use [rack/fiber_pool](https://github.com/mperham/rack-fiber_pool/).
111
156
 
112
- * [em-http-request](http://github.com/igrigorik/em-synchrony/blob/master/spec/http_spec.rb)
113
- * [em-mysqlplus](http://github.com/igrigorik/em-synchrony/blob/master/spec/mysqlplus_spec.rb)
114
- * [em-redis](http://github.com/igrigorik/em-synchrony/blob/master/spec/redis_spec.rb)
115
- * [em-memcached](http://github.com/igrigorik/em-synchrony/blob/master/spec/memcache_spec.rb) & [remcached](http://github.com/igrigorik/em-synchrony/blob/master/spec/remcached_spec.rb)
116
- * [em-mongo](http://github.com/igrigorik/em-synchrony/blob/master/spec/em-mongo_spec.rb) & [mongoid](http://github.com/igrigorik/em-synchrony/blob/master/spec/mongo_spec.rb)
157
+ ```ruby
158
+ require "em-synchrony"
159
+ require "em-synchrony/activerecord"
117
160
 
118
- # License
119
-
120
- (The MIT License)
161
+ ActiveRecord::Base.establish_connection(
162
+ :adapter => 'em_mysql2',
163
+ :database => 'widgets'
164
+ )
121
165
 
122
- Copyright (c) 2010 Ilya Grigorik
166
+ result = Widget.all.to_a
167
+ ```
123
168
 
124
- Permission is hereby granted, free of charge, to any person obtaining
125
- a copy of this software and associated documentation files (the
126
- 'Software'), to deal in the Software without restriction, including
127
- without limitation the rights to use, copy, modify, merge, publish,
128
- distribute, sublicense, and/or sell copies of the Software, and to
129
- permit persons to whom the Software is furnished to do so, subject to
130
- the following conditions:
131
-
132
- The above copyright notice and this permission notice shall be
133
- included in all copies or substantial portions of the Software.
169
+ # License
134
170
 
135
- THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
136
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
137
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
138
- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
139
- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
140
- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
141
- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
171
+ The MIT License - Copyright (c) 2011 Ilya Grigorik
data/Rakefile CHANGED
@@ -1,4 +1,10 @@
1
1
  require 'bundler'
2
+ require 'rspec/core/rake_task'
2
3
 
3
4
  Bundler.setup
4
- Bundler::GemHelper.install_tasks
5
+ Bundler::GemHelper.install_tasks
6
+
7
+ desc "Run all RSpec tests"
8
+ RSpec::Core::RakeTask.new(:spec)
9
+
10
+ task :default => :spec
data/em-synchrony.gemspec CHANGED
@@ -3,7 +3,7 @@ $:.push File.expand_path("../lib", __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "em-synchrony"
6
- s.version = "0.3.0.beta.1"
6
+ s.version = "1.0.0"
7
7
  s.platform = Gem::Platform::RUBY
8
8
  s.authors = ["Ilya Grigorik"]
9
9
  s.email = ["ilya@igvita.com"]
@@ -19,4 +19,4 @@ Gem::Specification.new do |s|
19
19
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
20
20
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
21
  s.require_paths = ["lib"]
22
- end
22
+ end
data/lib/em-synchrony.rb CHANGED
@@ -12,6 +12,7 @@ require "em-synchrony/thread"
12
12
  require "em-synchrony/em-multi"
13
13
  require "em-synchrony/tcpsocket"
14
14
  require "em-synchrony/connection_pool"
15
+ require "em-synchrony/keyboard"
15
16
  require "em-synchrony/iterator" if EventMachine::VERSION > '0.12.10'
16
17
 
17
18
  module EventMachine
@@ -44,18 +45,41 @@ module EventMachine
44
45
  #
45
46
  def self.sync(df)
46
47
  f = Fiber.current
47
- df.callback { |r| f.resume(r) }
48
- df.errback { |r| f.resume(r) }
48
+ xback = proc {|r|
49
+ if f == Fiber.current
50
+ return r
51
+ else
52
+ f.resume r
53
+ end
54
+ }
55
+ df.callback &xback
56
+ df.errback &xback
49
57
 
50
58
  Fiber.yield
51
59
  end
52
60
 
53
61
  # a Fiber-aware sleep function using an EM timer
54
- def self.sleep( secs )
62
+ def self.sleep(secs)
55
63
  fiber = Fiber.current
56
- EM::Timer.new(secs) { fiber.resume }
64
+ EM::Timer.new(secs) { fiber.resume }
57
65
  Fiber.yield
58
66
  end
59
- end
60
67
 
68
+ def self.add_timer(interval, &blk)
69
+ EM.add_timer(interval) do
70
+ Fiber.new { blk.call }.resume
71
+ end
72
+ end
73
+
74
+ def self.add_periodic_timer(interval, &blk)
75
+ EM.add_periodic_timer(interval) do
76
+ Fiber.new { blk.call }.resume
77
+ end
78
+ end
79
+
80
+ # routes to EM::Synchrony::Keyboard
81
+ def self.gets
82
+ EventMachine::Synchrony::Keyboard.new.gets
83
+ end
84
+ end
61
85
  end
@@ -0,0 +1,18 @@
1
+ # encoding: utf-8
2
+
3
+ # AR adapter for using a fibered mysql2 connection with EM
4
+ # This adapter should be used within Thin or Unicorn with the rack-fiber_pool middleware.
5
+ # Just update your database.yml's adapter to be 'em_mysql2'
6
+
7
+ require 'active_record/connection_adapters/abstract_adapter'
8
+ require 'active_record/connection_adapters/mysql2_adapter'
9
+
10
+ module ActiveRecord
11
+ class Base
12
+ def self.em_mysql2_connection(config)
13
+ client = Mysql2::EM::Client.new(config.symbolize_keys)
14
+ options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0]
15
+ ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,132 @@
1
+ # Necessary monkeypatching to make AR fiber-friendly.
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+
6
+ def self.fiber_pools
7
+ @fiber_pools ||= []
8
+ end
9
+ def self.register_fiber_pool(fp)
10
+ fiber_pools << fp
11
+ end
12
+
13
+ class FiberedMonitor
14
+ class Queue
15
+ def initialize
16
+ @queue = []
17
+ end
18
+
19
+ def wait(timeout)
20
+ t = timeout || 5
21
+ fiber = Fiber.current
22
+ x = EM::Timer.new(t) do
23
+ @queue.delete(fiber)
24
+ fiber.resume(false)
25
+ end
26
+ @queue << fiber
27
+ Fiber.yield.tap do
28
+ x.cancel
29
+ end
30
+ end
31
+
32
+ def signal
33
+ fiber = @queue.pop
34
+ fiber.resume(true) if fiber
35
+ end
36
+ end
37
+
38
+ def synchronize
39
+ yield
40
+ end
41
+
42
+ def new_cond
43
+ Queue.new
44
+ end
45
+ end
46
+
47
+ # ActiveRecord's connection pool is based on threads. Since we are working
48
+ # with EM and a single thread, multiple fiber design, we need to provide
49
+ # our own connection pool that keys off of Fiber.current so that different
50
+ # fibers running in the same thread don't try to use the same connection.
51
+ class ConnectionPool
52
+ def initialize(spec)
53
+ @spec = spec
54
+
55
+ # The cache of reserved connections mapped to threads
56
+ @reserved_connections = {}
57
+
58
+ # The mutex used to synchronize pool access
59
+ @connection_mutex = FiberedMonitor.new
60
+ @queue = @connection_mutex.new_cond
61
+
62
+ # default 5 second timeout unless on ruby 1.9
63
+ @timeout = spec.config[:wait_timeout] || 5
64
+
65
+ # default max pool size to 5
66
+ @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
67
+
68
+ @connections = []
69
+ @checked_out = []
70
+ @automatic_reconnect = true
71
+ @tables = {}
72
+
73
+ @columns = Hash.new do |h, table_name|
74
+ h[table_name] = with_connection do |conn|
75
+
76
+ # Fetch a list of columns
77
+ conn.columns(table_name, "#{table_name} Columns").tap do |columns|
78
+
79
+ # set primary key information
80
+ columns.each do |column|
81
+ column.primary = column.name == primary_keys[table_name]
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ @columns_hash = Hash.new do |h, table_name|
88
+ h[table_name] = Hash[columns[table_name].map { |col|
89
+ [col.name, col]
90
+ }]
91
+ end
92
+
93
+ @primary_keys = Hash.new do |h, table_name|
94
+ h[table_name] = with_connection do |conn|
95
+ table_exists?(table_name) ? conn.primary_key(table_name) : 'id'
96
+ end
97
+ end
98
+ end
99
+
100
+ def clear_stale_cached_connections!
101
+ cache = @reserved_connections
102
+ keys = Set.new(cache.keys)
103
+
104
+ ActiveRecord::ConnectionAdapters.fiber_pools.each do |pool|
105
+ pool.busy_fibers.each_pair do |object_id, fiber|
106
+ keys.delete(object_id)
107
+ end
108
+ end
109
+
110
+ keys.each do |key|
111
+ next unless cache.has_key?(key)
112
+ checkin cache[key]
113
+ cache.delete(key)
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def current_connection_id #:nodoc:
120
+ Fiber.current.object_id
121
+ end
122
+
123
+ def checkout_and_verify(c)
124
+ @checked_out << c
125
+ c.run_callbacks :checkout
126
+ c.verify!
127
+ c
128
+ end
129
+ end
130
+
131
+ end
132
+ end