em-synchrony 0.1.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.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
|