em-synchrony 0.1.2 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,71 +1,86 @@
1
1
  # EM-Synchrony
2
2
 
3
+ http://www.igvita.com/2010/03/22/untangling-evented-code-with-ruby-fibers
4
+
3
5
  Collection of convenience classes and patches to common EventMachine clients to
4
6
  make them Fiber aware and friendly. Word of warning: even though fibers have been
5
7
  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)
8
+ in production, you should be on 1.9 anyway)
7
9
 
8
10
  Features:
9
11
 
10
12
  * Fiber aware connection pool with sync/async query support
11
13
  * Multi request interface which accepts any callback enabled client
14
+ * Fibered iterator to allow concurrency control & mixing of sync / async
12
15
  * em-http-request: .get, etc are synchronous, while .aget, etc are async
13
16
  * em-mysqlplus: .query is synchronous, while .aquery is async
14
17
  * remcached: .get, etc, and .multi_* methods are synchronous
15
18
 
16
19
  ## Example with async em-http client:
17
20
 
18
- EventMachine.run do
19
- Fiber.new {
20
- res = EventMachine::HttpRequest.new("http://www.postrank.com").get
21
+ EventMachine.synchrony do
22
+ res = EventMachine::HttpRequest.new("http://www.postrank.com").get
21
23
 
22
- p "Look ma, no callbacks!"
23
- p res
24
+ p "Look ma, no callbacks!"
25
+ p res
24
26
 
25
- EventMachine.stop
26
- }.resume
27
+ EventMachine.stop
27
28
  end
28
29
 
29
- ## Example with multi-request async em-http client:
30
+ ## EM Iterator & mixing sync / async code
30
31
 
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
32
+ EM.synchrony do
33
+ concurrency = 2
34
+ urls = ['http://url.1.com', 'http://url2.com']
35
+
36
+ # iterator will execute async blocks until completion, .each, .inject also work!
37
+ results = EM::Synchrony::Iterator.new(urls, concurrency).map do |url, iter|
38
+
39
+ # fire async requests, on completion advance the iterator
40
+ http = EventMachine::HttpRequest.new(url).aget
41
+ http.callback { iter.return(http) }
42
+ end
41
43
 
42
- EventMachine.stop
43
- }.resume
44
+ p results # all completed requests
45
+
46
+ EventMachine.stop
44
47
  end
45
48
 
46
49
  ## Example connection pool shared by a fiber:
47
50
 
48
- EventMachine.run do
49
-
51
+ EventMachine.synchrony do
50
52
  db = EventMachine::Synchrony::ConnectionPool.new(size: 2) do
51
53
  EventMachine::MySQL.new(host: "localhost")
52
54
  end
53
55
 
54
- Fiber.new {
55
- start = now
56
+ start = now
56
57
 
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
58
+ multi = EventMachine::Synchrony::Multi.new
59
+ multi.add :a, db.aquery("select sleep(1)")
60
+ multi.add :b, db.aquery("select sleep(1)")
61
+ res = multi.perform
61
62
 
62
- p "Look ma, no callbacks, and parallel requests!"
63
- p res
63
+ p "Look ma, no callbacks, and parallel MySQL requests!"
64
+ p res
64
65
 
65
- EventMachine.stop
66
- }.resume
66
+ EventMachine.stop
67
67
  end
68
68
 
69
+
70
+ ## Example with multi-request async em-http client:
71
+
72
+ EventMachine.synchrony do
73
+ multi = EventMachine::Synchrony::Multi.new
74
+ multi.add :a, EventMachine::HttpRequest.new("http://www.postrank.com").aget
75
+ multi.add :b, EventMachine::HttpRequest.new("http://www.postrank.com").apost
76
+ res = multi.perform
77
+
78
+ p "Look ma, no callbacks, and parallel HTTP requests!"
79
+ p res
80
+
81
+ EventMachine.stop
82
+ end
83
+
69
84
  # License
70
85
 
71
86
  (The MIT License)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.2
1
+ 0.1.4
data/examples/all.rb ADDED
@@ -0,0 +1,27 @@
1
+ require "lib/em-synchrony"
2
+
3
+ EM.synchrony do
4
+
5
+ # open 4 concurrent MySQL connections
6
+ db = EventMachine::Synchrony::ConnectionPool.new(size: 4) do
7
+ EventMachine::MySQL.new(host: "localhost")
8
+ end
9
+
10
+ # perform 4 http requests in parallel, and collect responses
11
+ multi = EventMachine::Synchrony::Multi.new
12
+ multi.add :page1, EventMachine::HttpRequest.new("http://service.com/page1").aget
13
+ multi.add :page2, EventMachine::HttpRequest.new("http://service.com/page2").aget
14
+ multi.add :page3, EventMachine::HttpRequest.new("http://service.com/page3").aget
15
+ multi.add :page4, EventMachine::HttpRequest.new("http://service.com/page4").aget
16
+ data = multi.perform.responses[:callback].values
17
+
18
+ # insert fetched HTTP data into a mysql database, using at most 2 connections at a time
19
+ # - note that we're writing async code within the callback!
20
+ EM::Synchrony::Iterator.new(data, 2).each do |page, iter|
21
+ db.aquery("INSERT INTO table (data) VALUES(#{page});")
22
+ db.callback { iter.return(http) }
23
+ end
24
+
25
+ puts "All done! Stopping event loop."
26
+ EventMachine.stop
27
+ end
data/lib/em-synchrony.rb CHANGED
@@ -5,9 +5,23 @@ require "eventmachine"
5
5
  require "fiber"
6
6
 
7
7
  require "em-synchrony/em-multi"
8
+ # require "em-synchrony/iterator" # iterators are not release in EM yet
9
+ require "em-synchrony/connection_pool"
10
+
8
11
  require "em-synchrony/em-http"
9
12
  require "em-synchrony/em-mysql"
10
- # require "em-synchrony/em-jack"
11
13
  require "em-synchrony/em-remcached"
12
14
 
13
- require "em-synchrony/connection_pool"
15
+ module EventMachine
16
+
17
+ # A convenience method for wrapping EM.run body within
18
+ # a Ruby Fiber such that async operations can be transparently
19
+ # paused and resumed based on IO scheduling.
20
+ def self.synchrony(blk=nil, tail=nil, &block)
21
+ blk ||= block
22
+ context = Proc.new { Fiber.new { blk.call }.resume }
23
+
24
+ self.run(context, tail)
25
+ end
26
+
27
+ end
@@ -0,0 +1,52 @@
1
+ require "em/iterator"
2
+
3
+ module EventMachine
4
+ module Synchrony
5
+
6
+ class Iterator < EM::Iterator
7
+
8
+ # synchronous iterator which will wait until all the
9
+ # jobs are done before returning. Unfortunately this
10
+ # means that you loose ability to choose concurrency
11
+ # on the fly (see iterator documentation in EM)
12
+ def each(foreach=nil, after=nil, &blk)
13
+ fiber = Fiber.current
14
+
15
+ fe = (foreach || blk)
16
+ cb = Proc.new do
17
+ after.call if after
18
+ fiber.resume
19
+ end
20
+
21
+ Fiber.yield super(fe, cb)
22
+ end
23
+
24
+ def map(&block)
25
+ fiber = Fiber.current
26
+ result = nil
27
+
28
+ after = Proc.new {|res| result = res; fiber.resume }
29
+ super(block, after)
30
+
31
+ Fiber.yield
32
+ result
33
+ end
34
+
35
+ def inject(obj, foreach = nil, after = nil, &block)
36
+ if foreach and after
37
+ super(obj, foreach, after)
38
+ else
39
+ fiber = Fiber.current
40
+ result = nil
41
+
42
+ after = Proc.new {|res| result = res; fiber.resume}
43
+ super(obj, block, after)
44
+
45
+ Fiber.yield
46
+ result
47
+ end
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -1,12 +1,14 @@
1
- require 'spec/helper'
1
+ require 'spec/helper/all'
2
2
 
3
3
  DELAY = 0.25
4
4
 
5
+ __END__
5
6
  describe EMJack do
6
7
 
7
8
  it "should fire sequential Beanstalk requests" do
9
+ pending
10
+
8
11
  EventMachine.run do
9
-
10
12
  Fiber.new {
11
13
  jack = EMJack::Connection.new
12
14
 
@@ -19,8 +21,9 @@ describe EMJack do
19
21
  end
20
22
 
21
23
  it "should fire multiple requests in parallel" do
24
+ pending
25
+
22
26
  EventMachine.run do
23
-
24
27
  Fiber.new {
25
28
  jack = EMJack::Connection.new
26
29
 
data/spec/http_spec.rb CHANGED
@@ -5,47 +5,45 @@ DELAY = 0.25
5
5
 
6
6
  describe EventMachine::HttpRequest do
7
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
8
+ EventMachine.synchrony do
9
+ s = StubServer.new("HTTP/1.0 200 OK\r\nConnection: close\r\n\r\nFoo", DELAY)
10
+
11
+ start = now
12
+ order = []
13
+ order.push :get if EventMachine::HttpRequest.new(URL).get
14
+ order.push :post if EventMachine::HttpRequest.new(URL).post
15
+ order.push :head if EventMachine::HttpRequest.new(URL).head
16
+ order.push :post if EventMachine::HttpRequest.new(URL).delete
17
+ order.push :put if EventMachine::HttpRequest.new(URL).put
18
+
19
+ (now - start.to_f).should be_within(DELAY * order.size * 0.15).of(DELAY * order.size)
20
+ order.should == [:get, :post, :head, :post, :put]
21
+
22
+ s.stop
23
+ EventMachine.stop
25
24
  end
26
25
  end
27
26
 
28
27
  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
28
+ EventMachine.synchrony do
29
+ s = StubServer.new("HTTP/1.0 200 OK\r\nConnection: close\r\n\r\nFoo", DELAY)
30
+
31
+ start = now
32
+
33
+ multi = EventMachine::Synchrony::Multi.new
34
+ multi.add :a, EventMachine::HttpRequest.new(URL).aget
35
+ multi.add :b, EventMachine::HttpRequest.new(URL).apost
36
+ multi.add :c, EventMachine::HttpRequest.new(URL).ahead
37
+ multi.add :d, EventMachine::HttpRequest.new(URL).adelete
38
+ multi.add :e, EventMachine::HttpRequest.new(URL).aput
39
+ res = multi.perform
40
+
41
+ (now - start.to_f).should be_within(DELAY * 0.15).of(DELAY)
42
+ res.responses[:callback].size.should == 5
43
+ res.responses[:errback].size.should == 0
44
+
45
+ s.stop
46
+ EventMachine.stop
49
47
  end
50
48
  end
51
49
  end
@@ -0,0 +1,68 @@
1
+ require "spec/helper/all"
2
+ require "em-synchrony/iterator"
3
+
4
+ describe EventMachine::Synchrony::Iterator do
5
+
6
+ it "should wait until the iterator is done" do
7
+ EM.synchrony do
8
+
9
+ results = []
10
+ i = EM::Synchrony::Iterator.new(1..50, 10).each do |num, iter|
11
+ results.push num
12
+ iter.next
13
+ end
14
+
15
+ results.should == (1..50).to_a
16
+ results.size.should == 50
17
+ EventMachine.stop
18
+ end
19
+ end
20
+
21
+ it "should map values within the iterator" do
22
+ EM.synchrony do
23
+ results = EM::Synchrony::Iterator.new(1..50, 10).map do |num, iter|
24
+ iter.return(num + 1)
25
+ end
26
+
27
+ results.should == (2..51).to_a
28
+ results.size.should == 50
29
+ EventMachine.stop
30
+ end
31
+ end
32
+
33
+ it "should sum values within the iterator" do
34
+ EM.synchrony do
35
+ data = (1..50).to_a
36
+ res = EM::Synchrony::Iterator.new(data, 10).inject(0) do |total, num, iter|
37
+ total += num
38
+ iter.return(total)
39
+ end
40
+
41
+ res.should == data.inject(:+)
42
+ EventMachine.stop
43
+ end
44
+ end
45
+
46
+ it "should fire async http requests in blocks of 2" do
47
+ EM.synchrony do
48
+ num_urls = 4
49
+ concurrency = 2
50
+ delay = 0.25
51
+
52
+ s = StubServer.new("HTTP/1.0 200 OK\r\nConnection: close\r\n\r\nFoo", delay)
53
+ urls = ['http://localhost:8081/'] * num_urls
54
+
55
+ start = now
56
+ results = EM::Synchrony::Iterator.new(urls, concurrency).map do |url, iter|
57
+ http = EventMachine::HttpRequest.new(url).aget
58
+ http.callback { iter.return(http) }
59
+ end
60
+
61
+ results.size.should == 4
62
+ (now - start.to_f).should be_within(delay * 0.15).of(delay * (num_urls / concurrency))
63
+
64
+ EventMachine.stop
65
+ end
66
+ end
67
+
68
+ end
data/spec/mysql_spec.rb CHANGED
@@ -7,23 +7,21 @@ QUERY = "select sleep(#{DELAY})"
7
7
  describe EventMachine::MySQL do
8
8
 
9
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 = []
10
+ EventMachine.synchrony do
11
+ db = EventMachine::MySQL.new(host: "localhost")
12
+ start = now
13
+ res = []
15
14
 
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)
15
+ res.push db.query(QUERY)
16
+ res.push db.query(QUERY)
17
+ (now - start.to_f).should be_within(DELAY * res.size * 0.15).of(DELAY * res.size)
19
18
 
20
- EventMachine.stop
21
- }.resume
19
+ EventMachine.stop
22
20
  end
23
21
  end
24
22
 
25
23
  it "should have accept a callback, errback on async queries" do
26
- EventMachine.run do
24
+ EventMachine.synchrony do
27
25
  db = EventMachine::MySQL.new(host: "localhost")
28
26
 
29
27
  res = db.aquery(QUERY)
@@ -36,58 +34,54 @@ describe EventMachine::MySQL do
36
34
  end
37
35
 
38
36
  it "should fire simultaneous requests via Multi interface" do
39
- EventMachine.run do
37
+ EventMachine.synchrony do
40
38
 
41
39
  db = EventMachine::Synchrony::ConnectionPool.new(size: 2) do
42
40
  EventMachine::MySQL.new(host: "localhost")
43
41
  end
44
42
 
45
- Fiber.new {
46
- start = now
43
+ start = now
47
44
 
48
- multi = EventMachine::Synchrony::Multi.new
49
- multi.add :a, db.aquery(QUERY)
50
- multi.add :b, db.aquery(QUERY)
51
- res = multi.perform
45
+ multi = EventMachine::Synchrony::Multi.new
46
+ multi.add :a, db.aquery(QUERY)
47
+ multi.add :b, db.aquery(QUERY)
48
+ res = multi.perform
52
49
 
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
50
+ (now - start.to_f).should be_within(DELAY * 0.15).of(DELAY)
51
+ res.responses[:callback].size.should == 2
52
+ res.responses[:errback].size.should == 0
56
53
 
57
- EventMachine.stop
58
- }.resume
54
+ EventMachine.stop
59
55
  end
60
56
  end
61
57
 
62
58
  it "should fire sequential and simultaneous MySQL requests" do
63
- EventMachine.run do
59
+ EventMachine.synchrony do
64
60
  db = EventMachine::Synchrony::ConnectionPool.new(size: 3) do
65
61
  EventMachine::MySQL.new(host: "localhost")
66
62
  end
67
63
 
68
- Fiber.new {
69
- start = now
70
- res = []
64
+ start = now
65
+ res = []
71
66
 
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)
67
+ res.push db.query(QUERY)
68
+ res.push db.query(QUERY)
69
+ (now - start.to_f).should be_within(DELAY * res.size * 0.15).of(DELAY * res.size)
75
70
 
76
- start = now
71
+ start = now
77
72
 
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
73
+ multi = EventMachine::Synchrony::Multi.new
74
+ multi.add :a, db.aquery(QUERY)
75
+ multi.add :b, db.aquery(QUERY)
76
+ multi.add :c, db.aquery(QUERY)
77
+ res = multi.perform
83
78
 
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
79
+ (now - start.to_f).should be_within(DELAY * 0.15).of(DELAY)
80
+ res.responses[:callback].size.should == 3
81
+ res.responses[:errback].size.should == 0
87
82
 
88
- EventMachine.stop
89
- }.resume
83
+ EventMachine.stop
90
84
  end
91
85
  end
92
86
 
93
- end
87
+ end
@@ -4,47 +4,38 @@ require "remcached"
4
4
  describe Memcached do
5
5
 
6
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
7
+ EventMachine.synchrony do
8
+ Memcached.connect %w(localhost)
9
+ Memcached.usable?.should be_true
10
+ EventMachine.stop
13
11
  end
14
12
  end
15
13
 
16
14
  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
15
+ EventMachine.synchrony do
16
+
17
+ Memcached.connect %w(localhost)
18
+ Memcached.get(key: 'hai') do |res|
19
+ res[:value].should match('Not found')
20
+ end
21
+
22
+ Memcached.set(key: 'hai', value: 'win')
23
+ Memcached.add(key: 'count')
24
+ Memcached.delete(key: 'hai')
25
+
26
+ EventMachine.stop
31
27
  end
32
28
  end
33
-
29
+
34
30
  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
-
31
+ EventMachine.synchrony do
32
+ pending "patch mult-get"
33
+
34
+ Memcached.connect %w(localhost)
35
+ Memcached.multi_get([{:key => 'foo'},{:key => 'bar'}, {:key => 'test'}]) do |res|
36
+ # TODO
46
37
  EventMachine.stop
47
- }.resume
38
+ end
48
39
  end
49
40
  end
50
41
 
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 1
8
- - 2
9
- version: 0.1.2
8
+ - 4
9
+ version: 0.1.4
10
10
  platform: ruby
11
11
  authors:
12
12
  - Ilya Grigorik
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-03-19 00:00:00 -04:00
17
+ date: 2010-03-22 00:00:00 -04:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -50,12 +50,14 @@ files:
50
50
  - lib/em-synchrony/em-multi.rb
51
51
  - lib/em-synchrony/em-mysql.rb
52
52
  - lib/em-synchrony/em-remcached.rb
53
+ - lib/em-synchrony/iterator.rb
53
54
  - spec/beanstalk_spec.rb
54
55
  - spec/connection_pool_spec.rb
55
56
  - spec/helper/all.rb
56
57
  - spec/helper/stub-http-server.rb
57
58
  - spec/helper/tolerance_matcher.rb
58
59
  - spec/http_spec.rb
60
+ - spec/iterator_spec.rb
59
61
  - spec/mysql_spec.rb
60
62
  - spec/remcached_spec.rb
61
63
  has_rdoc: true
@@ -96,5 +98,7 @@ test_files:
96
98
  - spec/helper/stub-http-server.rb
97
99
  - spec/helper/tolerance_matcher.rb
98
100
  - spec/http_spec.rb
101
+ - spec/iterator_spec.rb
99
102
  - spec/mysql_spec.rb
100
103
  - spec/remcached_spec.rb
104
+ - examples/all.rb