em-synchrony 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +92 -0
- data/Rakefile +20 -0
- data/VERSION +1 -0
- data/lib/em-synchrony.rb +13 -0
- data/lib/em-synchrony/connection_pool.rb +83 -0
- data/lib/em-synchrony/em-http.rb +18 -0
- data/lib/em-synchrony/em-jack.rb +30 -0
- data/lib/em-synchrony/em-multi.rb +37 -0
- data/lib/em-synchrony/em-mysql.rb +18 -0
- data/lib/em-synchrony/em-remcached.rb +67 -0
- data/spec/beanstalk_spec.rb +41 -0
- data/spec/connection_pool_spec.rb +129 -0
- data/spec/helper/all.rb +16 -0
- data/spec/helper/stub-http-server.rb +22 -0
- data/spec/helper/tolerance_matcher.rb +40 -0
- data/spec/http_spec.rb +51 -0
- data/spec/mysql_spec.rb +93 -0
- data/spec/remcached_spec.rb +51 -0
- metadata +100 -0
data/README.md
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# EM-Synchrony
|
2
|
+
|
3
|
+
Collection of convenience classes and patches to common EventMachine clients to
|
4
|
+
make them Fiber aware and friendly. Word of warning: even though fibers have been
|
5
|
+
backported to Ruby 1.8.x, these classes assume Ruby 1.9 (if you're using fibers
|
6
|
+
in production, you should be on 1.9 anyway)
|
7
|
+
|
8
|
+
Features:
|
9
|
+
|
10
|
+
* Fiber aware connection pool with sync/async query support
|
11
|
+
* Multi request interface which accepts any callback enabled client
|
12
|
+
* em-http-request: .get, etc are synchronous, while .aget, etc are async
|
13
|
+
* em-mysqlplus: .query is synchronous, while .aquery is async
|
14
|
+
* remcached: .get, etc, and .multi_* methods are synchronous
|
15
|
+
|
16
|
+
## Example with async em-http client:
|
17
|
+
|
18
|
+
EventMachine.run do
|
19
|
+
Fiber.new {
|
20
|
+
res = EventMachine::HttpRequest.new("http://www.postrank.com").get
|
21
|
+
|
22
|
+
p "Look ma, no callbacks!"
|
23
|
+
p res
|
24
|
+
|
25
|
+
EventMachine.stop
|
26
|
+
}.resume
|
27
|
+
end
|
28
|
+
|
29
|
+
## Example with multi-request async em-http client:
|
30
|
+
|
31
|
+
EventMachine.run do
|
32
|
+
Fiber.new {
|
33
|
+
|
34
|
+
multi = EventMachine::Synchrony::Multi.new
|
35
|
+
multi.add :a, EventMachine::HttpRequest.new("http://www.postrank.com").aget
|
36
|
+
multi.add :b, EventMachine::HttpRequest.new("http://www.postrank.com").apost
|
37
|
+
res = multi.perform
|
38
|
+
|
39
|
+
p "Look ma, no callbacks, and parallel requests!"
|
40
|
+
p res
|
41
|
+
|
42
|
+
EventMachine.stop
|
43
|
+
}.resume
|
44
|
+
end
|
45
|
+
|
46
|
+
## Example connection pool shared by a fiber:
|
47
|
+
|
48
|
+
EventMachine.run do
|
49
|
+
|
50
|
+
db = EventMachine::Synchrony::ConnectionPool.new(size: 2) do
|
51
|
+
EventMachine::MySQL.new(host: "localhost")
|
52
|
+
end
|
53
|
+
|
54
|
+
Fiber.new {
|
55
|
+
start = now
|
56
|
+
|
57
|
+
multi = EventMachine::Synchrony::Multi.new
|
58
|
+
multi.add :a, db.aquery("select sleep(1)")
|
59
|
+
multi.add :b, db.aquery("select sleep(1)")
|
60
|
+
res = multi.perform
|
61
|
+
|
62
|
+
p "Look ma, no callbacks, and parallel requests!"
|
63
|
+
p res
|
64
|
+
|
65
|
+
EventMachine.stop
|
66
|
+
}.resume
|
67
|
+
end
|
68
|
+
|
69
|
+
# License
|
70
|
+
|
71
|
+
(The MIT License)
|
72
|
+
|
73
|
+
Copyright (c) 2010 Ilya Grigorik
|
74
|
+
|
75
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
76
|
+
a copy of this software and associated documentation files (the
|
77
|
+
'Software'), to deal in the Software without restriction, including
|
78
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
79
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
80
|
+
permit persons to whom the Software is furnished to do so, subject to
|
81
|
+
the following conditions:
|
82
|
+
|
83
|
+
The above copyright notice and this permission notice shall be
|
84
|
+
included in all copies or substantial portions of the Software.
|
85
|
+
|
86
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
87
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
88
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
89
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
90
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
91
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
92
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'jeweler'
|
5
|
+
Jeweler::Tasks.new do |gemspec|
|
6
|
+
gemspec.name = "em-synchrony"
|
7
|
+
gemspec.summary = "Fiber aware EventMachine libraries"
|
8
|
+
gemspec.description = gemspec.summary
|
9
|
+
gemspec.email = "ilya@igvita.com"
|
10
|
+
gemspec.homepage = "http://github.com/igrigorik/em-synchrony"
|
11
|
+
gemspec.authors = ["Ilya Grigorik"]
|
12
|
+
gemspec.required_ruby_version = ">= 1.9"
|
13
|
+
gemspec.add_dependency('eventmachine', '>= 0.12.9')
|
14
|
+
gemspec.rubyforge_project = "em-synchrony"
|
15
|
+
end
|
16
|
+
|
17
|
+
Jeweler::GemcutterTasks.new
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
20
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.1
|
data/lib/em-synchrony.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "eventmachine"
|
5
|
+
require "fiber"
|
6
|
+
|
7
|
+
require "em-synchrony/em-multi"
|
8
|
+
require "em-synchrony/em-http"
|
9
|
+
require "em-synchrony/em-mysql"
|
10
|
+
# require "em-synchrony/em-jack"
|
11
|
+
require "em-synchrony/em-remcached"
|
12
|
+
|
13
|
+
require "em-synchrony/connection_pool"
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module Synchrony
|
3
|
+
|
4
|
+
class ConnectionPool
|
5
|
+
def initialize(opts, &block)
|
6
|
+
@reserved = {} # map of in-progress connections
|
7
|
+
@available = [] # pool of free connections
|
8
|
+
@pending = [] # pending reservations (FIFO)
|
9
|
+
|
10
|
+
opts[:size].times do
|
11
|
+
@available.push(block.call) if block_given?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Choose first available connection and pass it to the supplied
|
16
|
+
# block. This will block indefinitely until there is an available
|
17
|
+
# connection to service the request.
|
18
|
+
def execute(async)
|
19
|
+
f = Fiber.current
|
20
|
+
|
21
|
+
begin
|
22
|
+
conn = acquire(f)
|
23
|
+
yield conn
|
24
|
+
ensure
|
25
|
+
release(f) if not async
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# Acquire a lock on a connection and assign it to executing fiber
|
32
|
+
# - if connection is available, pass it back to the calling block
|
33
|
+
# - if pool is full, yield the current fiber until connection is available
|
34
|
+
def acquire(fiber)
|
35
|
+
|
36
|
+
if conn = @available.pop
|
37
|
+
@reserved[fiber.object_id] = conn
|
38
|
+
conn
|
39
|
+
else
|
40
|
+
Fiber.yield @pending.push fiber
|
41
|
+
acquire(fiber)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Release connection assigned to the supplied fiber and
|
46
|
+
# resume any other pending connections (which will
|
47
|
+
# immediately try to run acquire on the pool)
|
48
|
+
def release(fiber)
|
49
|
+
@available.push(@reserved.delete(fiber.object_id))
|
50
|
+
|
51
|
+
if pending = @pending.pop
|
52
|
+
pending.resume
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Allow the pool to behave as the underlying connection
|
57
|
+
#
|
58
|
+
# If the requesting method begins with "a" prefix, then
|
59
|
+
# hijack the callbacks and errbacks to fire a connection
|
60
|
+
# pool release whenever the request is complete. Otherwise
|
61
|
+
# yield the connection within execute method and release
|
62
|
+
# once it is complete (assumption: fiber will yield until
|
63
|
+
# data is available, or request is complete)
|
64
|
+
#
|
65
|
+
def method_missing(method, *args)
|
66
|
+
async = (method[0,1] == "a")
|
67
|
+
|
68
|
+
execute(async) do |conn|
|
69
|
+
df = conn.send(method, *args)
|
70
|
+
|
71
|
+
if async
|
72
|
+
fiber = Fiber.current
|
73
|
+
df.callback { release(fiber) }
|
74
|
+
df.errback { release(fiber) }
|
75
|
+
end
|
76
|
+
|
77
|
+
df
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module EventMachine
|
2
|
+
class HttpRequest
|
3
|
+
%w[get head post delete put].each do |type|
|
4
|
+
class_eval %[
|
5
|
+
alias :a#{type} :#{type}
|
6
|
+
def #{type}(options = {}, &blk)
|
7
|
+
f = Fiber.current
|
8
|
+
|
9
|
+
conn = setup_request(:#{type}, options, &blk)
|
10
|
+
conn.callback { f.resume(conn) }
|
11
|
+
conn.errback { f.resume(conn) }
|
12
|
+
|
13
|
+
Fiber.yield
|
14
|
+
end
|
15
|
+
]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'em-jack'
|
2
|
+
|
3
|
+
# WANT: namespaced under EventMachine.. would be nice :-)
|
4
|
+
# NOTE: no need for "pooling" since Beanstalk supports pipelining
|
5
|
+
module EMJack
|
6
|
+
class Connection
|
7
|
+
|
8
|
+
alias :ause :use
|
9
|
+
def use(tube, &blk)
|
10
|
+
return if @used_tube == tube
|
11
|
+
|
12
|
+
f = Fiber.current
|
13
|
+
|
14
|
+
# WANT: per command errbacks, would be nice, instead of one global
|
15
|
+
# errback = Proc.new {|r| f.resume(r) }
|
16
|
+
|
17
|
+
on_error {|r| f.resume(r)}
|
18
|
+
|
19
|
+
@used_tube = tube
|
20
|
+
@conn.send(:use, tube)
|
21
|
+
|
22
|
+
# WANT: Add conditional on add_deferrable to either accept two procs, or a single block
|
23
|
+
# .. two procs = callback, errback
|
24
|
+
add_deferrable { |r| f.resume(r) }
|
25
|
+
|
26
|
+
Fiber.yield
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module Synchrony
|
3
|
+
class Multi
|
4
|
+
include EventMachine::Deferrable
|
5
|
+
|
6
|
+
attr_reader :requests, :responses
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@requests = []
|
10
|
+
@responses = {:callback => {}, :errback => {}}
|
11
|
+
end
|
12
|
+
|
13
|
+
def add(name, conn)
|
14
|
+
fiber = Fiber.current
|
15
|
+
conn.callback { @responses[:callback][name] = conn; check_progress(fiber) }
|
16
|
+
conn.errback { @responses[:errback][name] = conn; check_progress(fiber) }
|
17
|
+
|
18
|
+
@requests.push(conn)
|
19
|
+
end
|
20
|
+
|
21
|
+
def perform
|
22
|
+
Fiber.yield
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def check_progress(fiber)
|
28
|
+
if (@responses[:callback].size + @responses[:errback].size) == @requests.size
|
29
|
+
succeed
|
30
|
+
|
31
|
+
# continue processing
|
32
|
+
fiber.resume(self)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module EventMachine
|
2
|
+
class MySQL
|
3
|
+
|
4
|
+
alias :aquery :query
|
5
|
+
def query(sql, &blk)
|
6
|
+
f = Fiber.current
|
7
|
+
|
8
|
+
# TODO: blk case does not work. Hmm?
|
9
|
+
cb = Proc.new { |r| f.resume(r) }
|
10
|
+
eb = Proc.new { |r| f.resume(r) }
|
11
|
+
|
12
|
+
@connection.execute(sql, cb, eb)
|
13
|
+
|
14
|
+
Fiber.yield
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require "remcached"
|
2
|
+
|
3
|
+
module Memcached
|
4
|
+
class << self
|
5
|
+
|
6
|
+
def connect(servers)
|
7
|
+
Memcached.servers = servers
|
8
|
+
|
9
|
+
f = Fiber.current
|
10
|
+
@t = EM::PeriodicTimer.new(0.01) do
|
11
|
+
if Memcached.usable?
|
12
|
+
@t.cancel
|
13
|
+
f.resume(self)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
Fiber.yield
|
18
|
+
end
|
19
|
+
|
20
|
+
%w[add get set delete].each do |type|
|
21
|
+
class_eval %[
|
22
|
+
def a#{type}(contents, &callback)
|
23
|
+
df = EventMachine::DefaultDeferrable.new
|
24
|
+
df.callback &callback
|
25
|
+
|
26
|
+
cb = Proc.new { |res| df.succeed(res) }
|
27
|
+
operation Request::#{type.capitalize}, contents, &cb
|
28
|
+
|
29
|
+
df
|
30
|
+
end
|
31
|
+
|
32
|
+
def #{type}(contents, &callback)
|
33
|
+
fiber = Fiber.current
|
34
|
+
|
35
|
+
df = a#{type}(contents, &Proc.new { |res| fiber.resume(res) })
|
36
|
+
df.callback &callback
|
37
|
+
|
38
|
+
Fiber.yield
|
39
|
+
end
|
40
|
+
]
|
41
|
+
end
|
42
|
+
|
43
|
+
%w[add get set delete].each do |type|
|
44
|
+
class_eval %[
|
45
|
+
def amulti_#{type}(contents, &callback)
|
46
|
+
df = EventMachine::DefaultDeferrable.new
|
47
|
+
df.callback &callback
|
48
|
+
|
49
|
+
cb = Proc.new { |res| df.succeed(res) }
|
50
|
+
multi_operation Request::#{type.capitalize}, contents, &cb
|
51
|
+
|
52
|
+
df
|
53
|
+
end
|
54
|
+
|
55
|
+
def multi_#{type}(contents, &callback)
|
56
|
+
fiber = Fiber.current
|
57
|
+
|
58
|
+
df = amulti_#{type}(contents, &Proc.new { |res| fiber.resume(res) })
|
59
|
+
df.callback &callback
|
60
|
+
|
61
|
+
Fiber.yield
|
62
|
+
end
|
63
|
+
]
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec/helper'
|
2
|
+
|
3
|
+
DELAY = 0.25
|
4
|
+
|
5
|
+
describe EMJack do
|
6
|
+
|
7
|
+
it "should fire sequential Beanstalk requests" do
|
8
|
+
EventMachine.run do
|
9
|
+
|
10
|
+
Fiber.new {
|
11
|
+
jack = EMJack::Connection.new
|
12
|
+
|
13
|
+
r = jack.use('mytube')
|
14
|
+
r.should == 'mytube'
|
15
|
+
|
16
|
+
EventMachine.stop
|
17
|
+
}.resume
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should fire multiple requests in parallel" do
|
22
|
+
EventMachine.run do
|
23
|
+
|
24
|
+
Fiber.new {
|
25
|
+
jack = EMJack::Connection.new
|
26
|
+
|
27
|
+
multi = EventMachine::Multi.new
|
28
|
+
multi.add jack.ause('mytube-1')
|
29
|
+
multi.add jack.ause('mytube-2')
|
30
|
+
res = multi.perform
|
31
|
+
|
32
|
+
res.responses.size.should == 2
|
33
|
+
p [:multi, res.responses]
|
34
|
+
|
35
|
+
EventMachine.stop
|
36
|
+
}.resume
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require "spec/helper/all"
|
2
|
+
|
3
|
+
DELAY = 0.25
|
4
|
+
QUERY = "select sleep(#{DELAY})"
|
5
|
+
|
6
|
+
describe EventMachine::Synchrony::ConnectionPool do
|
7
|
+
|
8
|
+
it "should queue requests if pool size is exceeded" do
|
9
|
+
EventMachine.run do
|
10
|
+
|
11
|
+
db = EventMachine::Synchrony::ConnectionPool.new(size: 1) do
|
12
|
+
EventMachine::MySQL.new(host: "localhost")
|
13
|
+
end
|
14
|
+
|
15
|
+
Fiber.new {
|
16
|
+
start = now
|
17
|
+
|
18
|
+
multi = EventMachine::Synchrony::Multi.new
|
19
|
+
multi.add :a, db.aquery(QUERY)
|
20
|
+
multi.add :b, db.aquery(QUERY)
|
21
|
+
res = multi.perform
|
22
|
+
|
23
|
+
(now - start.to_f).should be_within(DELAY * 2 * 0.15).of(DELAY * 2)
|
24
|
+
res.responses[:callback].size.should == 2
|
25
|
+
res.responses[:errback].size.should == 0
|
26
|
+
|
27
|
+
EventMachine.stop
|
28
|
+
}.resume
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should execute multiple async pool requests within same fiber" do
|
33
|
+
EventMachine.run do
|
34
|
+
|
35
|
+
db = EventMachine::Synchrony::ConnectionPool.new(size: 2) do
|
36
|
+
EventMachine::MySQL.new(host: "localhost")
|
37
|
+
end
|
38
|
+
|
39
|
+
Fiber.new {
|
40
|
+
start = now
|
41
|
+
|
42
|
+
multi = EventMachine::Synchrony::Multi.new
|
43
|
+
multi.add :a, db.aquery(QUERY)
|
44
|
+
multi.add :b, db.aquery(QUERY)
|
45
|
+
res = multi.perform
|
46
|
+
|
47
|
+
(now - start.to_f).should be_within(DELAY * 0.15).of(DELAY)
|
48
|
+
res.responses[:callback].size.should == 2
|
49
|
+
res.responses[:errback].size.should == 0
|
50
|
+
|
51
|
+
EventMachine.stop
|
52
|
+
}.resume
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should share connection pool between different fibers" do
|
57
|
+
EventMachine.run do
|
58
|
+
|
59
|
+
db = EventMachine::Synchrony::ConnectionPool.new(size: 2) do
|
60
|
+
EventMachine::MySQL.new(host: "localhost")
|
61
|
+
end
|
62
|
+
|
63
|
+
Fiber.new {
|
64
|
+
start = now
|
65
|
+
results = []
|
66
|
+
|
67
|
+
fiber = Fiber.current
|
68
|
+
2.times do |n|
|
69
|
+
Fiber.new {
|
70
|
+
results.push db.query(QUERY)
|
71
|
+
fiber.transfer if results.size == 2
|
72
|
+
}.resume
|
73
|
+
end
|
74
|
+
|
75
|
+
# wait for workers
|
76
|
+
Fiber.yield
|
77
|
+
|
78
|
+
(now - start.to_f).should be_within(DELAY * 0.15).of(DELAY)
|
79
|
+
results.size.should == 2
|
80
|
+
|
81
|
+
EventMachine.stop
|
82
|
+
}.resume
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should share connection pool between different fibers & async requests" do
|
88
|
+
EventMachine.run do
|
89
|
+
|
90
|
+
db = EventMachine::Synchrony::ConnectionPool.new(size: 5) do
|
91
|
+
EventMachine::MySQL.new(host: "localhost")
|
92
|
+
end
|
93
|
+
|
94
|
+
Fiber.new {
|
95
|
+
start = now
|
96
|
+
results = []
|
97
|
+
|
98
|
+
fiber = Fiber.current
|
99
|
+
2.times do |n|
|
100
|
+
Fiber.new {
|
101
|
+
|
102
|
+
multi = EventMachine::Synchrony::Multi.new
|
103
|
+
multi.add :a, db.aquery(QUERY)
|
104
|
+
multi.add :b, db.aquery(QUERY)
|
105
|
+
results.push multi.perform
|
106
|
+
|
107
|
+
fiber.transfer if results.size == 3
|
108
|
+
}.resume
|
109
|
+
end
|
110
|
+
|
111
|
+
Fiber.new {
|
112
|
+
# execute a synchronous requests
|
113
|
+
results.push db.query(QUERY)
|
114
|
+
fiber.transfer if results.size == 3
|
115
|
+
}.resume
|
116
|
+
|
117
|
+
# wait for workers
|
118
|
+
Fiber.yield
|
119
|
+
|
120
|
+
(now - start.to_f).should be_within(DELAY * 0.15).of(DELAY)
|
121
|
+
results.size.should == 3
|
122
|
+
|
123
|
+
EventMachine.stop
|
124
|
+
}.resume
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
data/spec/helper/all.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'spec'
|
3
|
+
require 'pp'
|
4
|
+
|
5
|
+
require 'em-http'
|
6
|
+
require 'em-mysqlplus'
|
7
|
+
|
8
|
+
require 'lib/em-synchrony'
|
9
|
+
require 'helper/tolerance_matcher'
|
10
|
+
require 'helper/stub-http-server'
|
11
|
+
|
12
|
+
def now(); Time.now.to_f; end
|
13
|
+
|
14
|
+
Spec::Runner.configure do |config|
|
15
|
+
config.include(Sander6::CustomMatchers)
|
16
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class StubServer
|
2
|
+
module Server
|
3
|
+
attr_accessor :response, :delay
|
4
|
+
def receive_data(data)
|
5
|
+
EM.add_timer(@delay) {
|
6
|
+
send_data @response
|
7
|
+
close_connection_after_writing
|
8
|
+
}
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(response, delay = 0, port=8081)
|
13
|
+
@sig = EventMachine::start_server("127.0.0.1", port, Server) { |s|
|
14
|
+
s.response = response
|
15
|
+
s.delay = delay
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
def stop
|
20
|
+
EventMachine.stop_server @sig
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
#
|
2
|
+
# courtesy of sander 6
|
3
|
+
# - http://github.com/sander6/custom_matchers/blob/master/lib/matchers/tolerance_matchers.rb
|
4
|
+
#
|
5
|
+
module Sander6
|
6
|
+
module CustomMatchers
|
7
|
+
|
8
|
+
class ToleranceMatcher
|
9
|
+
def initialize(tolerance)
|
10
|
+
@tolerance = tolerance
|
11
|
+
@target = 0
|
12
|
+
end
|
13
|
+
|
14
|
+
def of(target)
|
15
|
+
@target = target
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def matches?(value)
|
20
|
+
@value = value
|
21
|
+
((@target - @tolerance)..(@target + @tolerance)).include?(@value)
|
22
|
+
end
|
23
|
+
|
24
|
+
def failure_message
|
25
|
+
"Expected #{@value.inspect} to be within #{@tolerance.inspect} of #{@target.inspect}, but it wasn't.\n" +
|
26
|
+
"Difference: #{@value - @target}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def negative_failure_message
|
30
|
+
"Expected #{@value.inspect} not to be within #{@tolerance.inspect} of #{@target.inspect}, but it was.\n" +
|
31
|
+
"Difference: #{@value - @target}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def be_within(value)
|
36
|
+
Sander6::CustomMatchers::ToleranceMatcher.new(value)
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
data/spec/http_spec.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require "spec/helper/all"
|
2
|
+
|
3
|
+
URL = "http://localhost:8081/"
|
4
|
+
DELAY = 0.25
|
5
|
+
|
6
|
+
describe EventMachine::HttpRequest do
|
7
|
+
it "should fire sequential requests" do
|
8
|
+
EventMachine.run do
|
9
|
+
@s = StubServer.new("HTTP/1.0 200 OK\r\nConnection: close\r\n\r\nFoo", DELAY)
|
10
|
+
|
11
|
+
Fiber.new {
|
12
|
+
start = now
|
13
|
+
order = []
|
14
|
+
order.push :get if EventMachine::HttpRequest.new(URL).get
|
15
|
+
order.push :post if EventMachine::HttpRequest.new(URL).post
|
16
|
+
order.push :head if EventMachine::HttpRequest.new(URL).head
|
17
|
+
order.push :post if EventMachine::HttpRequest.new(URL).delete
|
18
|
+
order.push :put if EventMachine::HttpRequest.new(URL).put
|
19
|
+
|
20
|
+
(now - start.to_f).should be_within(DELAY * order.size * 0.15).of(DELAY * order.size)
|
21
|
+
order.should == [:get, :post, :head, :post, :put]
|
22
|
+
|
23
|
+
EventMachine.stop
|
24
|
+
}.resume
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should fire simultaneous requests via Multi interface" do
|
29
|
+
EventMachine.run do
|
30
|
+
@s = StubServer.new("HTTP/1.0 200 OK\r\nConnection: close\r\n\r\nFoo", DELAY)
|
31
|
+
|
32
|
+
Fiber.new {
|
33
|
+
start = now
|
34
|
+
|
35
|
+
multi = EventMachine::Synchrony::Multi.new
|
36
|
+
multi.add :a, EventMachine::HttpRequest.new(URL).aget
|
37
|
+
multi.add :b, EventMachine::HttpRequest.new(URL).apost
|
38
|
+
multi.add :c, EventMachine::HttpRequest.new(URL).ahead
|
39
|
+
multi.add :d, EventMachine::HttpRequest.new(URL).adelete
|
40
|
+
multi.add :e, EventMachine::HttpRequest.new(URL).aput
|
41
|
+
res = multi.perform
|
42
|
+
|
43
|
+
(now - start.to_f).should be_within(DELAY * 0.15).of(DELAY)
|
44
|
+
res.responses[:callback].size.should == 5
|
45
|
+
res.responses[:errback].size.should == 0
|
46
|
+
|
47
|
+
EventMachine.stop
|
48
|
+
}.resume
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/spec/mysql_spec.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
require "spec/helper/all"
|
2
|
+
require "em-mysqlplus"
|
3
|
+
|
4
|
+
DELAY = 0.25
|
5
|
+
QUERY = "select sleep(#{DELAY})"
|
6
|
+
|
7
|
+
describe EventMachine::MySQL do
|
8
|
+
|
9
|
+
it "should fire sequential, synchronous requests" do
|
10
|
+
EventMachine.run do
|
11
|
+
Fiber.new {
|
12
|
+
db = EventMachine::MySQL.new(host: "localhost")
|
13
|
+
start = now
|
14
|
+
res = []
|
15
|
+
|
16
|
+
res.push db.query(QUERY)
|
17
|
+
res.push db.query(QUERY)
|
18
|
+
(now - start.to_f).should be_within(DELAY * res.size * 0.15).of(DELAY * res.size)
|
19
|
+
|
20
|
+
EventMachine.stop
|
21
|
+
}.resume
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should have accept a callback, errback on async queries" do
|
26
|
+
EventMachine.run do
|
27
|
+
db = EventMachine::MySQL.new(host: "localhost")
|
28
|
+
|
29
|
+
res = db.aquery(QUERY)
|
30
|
+
res.errback {|r| fail }
|
31
|
+
res.callback {|r|
|
32
|
+
r.all_hashes.size.should == 1
|
33
|
+
EventMachine.stop
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should fire simultaneous requests via Multi interface" do
|
39
|
+
EventMachine.run do
|
40
|
+
|
41
|
+
db = EventMachine::Synchrony::ConnectionPool.new(size: 2) do
|
42
|
+
EventMachine::MySQL.new(host: "localhost")
|
43
|
+
end
|
44
|
+
|
45
|
+
Fiber.new {
|
46
|
+
start = now
|
47
|
+
|
48
|
+
multi = EventMachine::Synchrony::Multi.new
|
49
|
+
multi.add :a, db.aquery(QUERY)
|
50
|
+
multi.add :b, db.aquery(QUERY)
|
51
|
+
res = multi.perform
|
52
|
+
|
53
|
+
(now - start.to_f).should be_within(DELAY * 0.15).of(DELAY)
|
54
|
+
res.responses[:callback].size.should == 2
|
55
|
+
res.responses[:errback].size.should == 0
|
56
|
+
|
57
|
+
EventMachine.stop
|
58
|
+
}.resume
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should fire sequential and simultaneous MySQL requests" do
|
63
|
+
EventMachine.run do
|
64
|
+
db = EventMachine::Synchrony::ConnectionPool.new(size: 3) do
|
65
|
+
EventMachine::MySQL.new(host: "localhost")
|
66
|
+
end
|
67
|
+
|
68
|
+
Fiber.new {
|
69
|
+
start = now
|
70
|
+
res = []
|
71
|
+
|
72
|
+
res.push db.query(QUERY)
|
73
|
+
res.push db.query(QUERY)
|
74
|
+
(now - start.to_f).should be_within(DELAY * res.size * 0.15).of(DELAY * res.size)
|
75
|
+
|
76
|
+
start = now
|
77
|
+
|
78
|
+
multi = EventMachine::Synchrony::Multi.new
|
79
|
+
multi.add :a, db.aquery(QUERY)
|
80
|
+
multi.add :b, db.aquery(QUERY)
|
81
|
+
multi.add :c, db.aquery(QUERY)
|
82
|
+
res = multi.perform
|
83
|
+
|
84
|
+
(now - start.to_f).should be_within(DELAY * 0.15).of(DELAY)
|
85
|
+
res.responses[:callback].size.should == 3
|
86
|
+
res.responses[:errback].size.should == 0
|
87
|
+
|
88
|
+
EventMachine.stop
|
89
|
+
}.resume
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "spec/helper/all"
|
2
|
+
require "remcached"
|
3
|
+
|
4
|
+
describe Memcached do
|
5
|
+
|
6
|
+
it "should yield until connection is ready" do
|
7
|
+
EventMachine.run do
|
8
|
+
Fiber.new {
|
9
|
+
Memcached.connect %w(localhost)
|
10
|
+
Memcached.usable?.should be_true
|
11
|
+
EventMachine.stop
|
12
|
+
}.resume
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should fire sequential memcached requests" do
|
17
|
+
EventMachine.run do
|
18
|
+
Fiber.new {
|
19
|
+
|
20
|
+
Memcached.connect %w(localhost)
|
21
|
+
Memcached.get(key: 'hai') do |res|
|
22
|
+
res[:value].should match('Not found')
|
23
|
+
end
|
24
|
+
|
25
|
+
Memcached.set(key: 'hai', value: 'win')
|
26
|
+
Memcached.add(key: 'count')
|
27
|
+
Memcached.delete(key: 'hai')
|
28
|
+
|
29
|
+
EventMachine.stop
|
30
|
+
}.resume
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should fire multi memcached requests" do
|
35
|
+
pending "remcached borked? opened a ticket"
|
36
|
+
|
37
|
+
EventMachine.run do
|
38
|
+
Fiber.new {
|
39
|
+
|
40
|
+
Memcached.connect %w(localhost)
|
41
|
+
|
42
|
+
Memcached.multi_get([{:key => 'foo'},{:key => 'bar'}, {:key => 'test'}]) do |res|
|
43
|
+
p res
|
44
|
+
end
|
45
|
+
|
46
|
+
EventMachine.stop
|
47
|
+
}.resume
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: em-synchrony
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 1
|
9
|
+
version: 0.1.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Ilya Grigorik
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-03-18 00:00:00 -04:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: eventmachine
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
- 12
|
30
|
+
- 9
|
31
|
+
version: 0.12.9
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
description: Fiber aware EventMachine libraries
|
35
|
+
email: ilya@igvita.com
|
36
|
+
executables: []
|
37
|
+
|
38
|
+
extensions: []
|
39
|
+
|
40
|
+
extra_rdoc_files:
|
41
|
+
- README.md
|
42
|
+
files:
|
43
|
+
- README.md
|
44
|
+
- Rakefile
|
45
|
+
- VERSION
|
46
|
+
- lib/em-synchrony.rb
|
47
|
+
- lib/em-synchrony/connection_pool.rb
|
48
|
+
- lib/em-synchrony/em-http.rb
|
49
|
+
- lib/em-synchrony/em-jack.rb
|
50
|
+
- lib/em-synchrony/em-multi.rb
|
51
|
+
- lib/em-synchrony/em-mysql.rb
|
52
|
+
- lib/em-synchrony/em-remcached.rb
|
53
|
+
- spec/beanstalk_spec.rb
|
54
|
+
- spec/connection_pool_spec.rb
|
55
|
+
- spec/helper/all.rb
|
56
|
+
- spec/helper/stub-http-server.rb
|
57
|
+
- spec/helper/tolerance_matcher.rb
|
58
|
+
- spec/http_spec.rb
|
59
|
+
- spec/mysql_spec.rb
|
60
|
+
- spec/remcached_spec.rb
|
61
|
+
has_rdoc: true
|
62
|
+
homepage: http://github.com/igrigorik/em-synchrony
|
63
|
+
licenses: []
|
64
|
+
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options:
|
67
|
+
- --charset=UTF-8
|
68
|
+
require_paths:
|
69
|
+
- lib
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
segments:
|
75
|
+
- 1
|
76
|
+
- 9
|
77
|
+
version: "1.9"
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
segments:
|
83
|
+
- 0
|
84
|
+
version: "0"
|
85
|
+
requirements: []
|
86
|
+
|
87
|
+
rubyforge_project: em-synchrony
|
88
|
+
rubygems_version: 1.3.6
|
89
|
+
signing_key:
|
90
|
+
specification_version: 3
|
91
|
+
summary: Fiber aware EventMachine libraries
|
92
|
+
test_files:
|
93
|
+
- spec/beanstalk_spec.rb
|
94
|
+
- spec/connection_pool_spec.rb
|
95
|
+
- spec/helper/all.rb
|
96
|
+
- spec/helper/stub-http-server.rb
|
97
|
+
- spec/helper/tolerance_matcher.rb
|
98
|
+
- spec/http_spec.rb
|
99
|
+
- spec/mysql_spec.rb
|
100
|
+
- spec/remcached_spec.rb
|