em-breakout 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ .rvmrc
2
+ .bundle
3
+ *.gem
4
+ Gemfile.lock
5
+ pkg/*
6
+ features/support/breakout.yml
7
+ features/support/em-breakout.output
8
+ features/support/stats.html
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'http://rubygems.org'
2
+ gemspec
3
+ gem 'rspec', '2.5.0'
4
+ gem 'cucumber', '>=0.10.0'
5
+ gem 'daemons'
6
+ gem 'breakout', :git => "git://github.com/steve9001/breakout.git"
7
+ gem 'ruby-prof'
data/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # EM-Breakout
2
+
3
+ Breakout is a light framework for routing messages among web browsers and workers using WebSockets.
4
+
5
+ EM-Breakout uses [EM-WebSocket](https://github.com/igrigorik/em-websocket) to implement a standalone server process that accepts connections from both browsers and workers.
6
+ Whenever a browser sends a message, it will be put on a queue to be read by the next available worker.
7
+ A simple API lets workers send messages to browsers, disconnect a browser, and be notified when a browser connects or disconnects.
8
+
9
+ The [breakout](https://github.com/steve9001/breakout) gem provides a module to help create workers along with some example workers and JavaScripts.
10
+
11
+ ## Getting started
12
+
13
+ Clone the repository and change to the directory. Use [bundler](http://gembundler.com) to install the dependencies, and run cucumber. If that is successful, you can change into the examples directory and run the server script (possibly with 'bundle exec').
14
+
15
+ Your em-breakout server is now ready to accept connections. Visit the [breakout](https://github.com/steve9001/breakout) page and pick up from there!
16
+
17
+ ## Copyright
18
+
19
+ Copyright (c) 2011 Steve Masterman. See LICENSE for details.
data/_LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Steve Masterman
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "em-breakout/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "em-breakout"
7
+ s.version = EventMachine::Breakout::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Steve Masterman"]
10
+ s.email = ["steve@vermonster.com"]
11
+ s.homepage = "https://github.com/steve9001/em-breakout"
12
+ s.summary = %q{Breakout routes messages among web browsers and workers using WebSockets.}
13
+ s.description = %q{Breakout routes messages among web browsers and workers using WebSockets.}
14
+
15
+ s.rubyforge_project = "em-breakout"
16
+
17
+ s.add_dependency 'json', '>=1.4.6'
18
+ s.add_dependency 'em-websocket', '>=0.2.1'
19
+
20
+ s.files = `git ls-files`.split("\n")
21
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
+ s.require_paths = ["lib"]
23
+ end
24
+
25
+
26
+
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+
5
+ $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
6
+ require 'em-breakout'
7
+
8
+ bdebug = ENV['BDEBUG']
9
+
10
+ EventMachine::Breakout.start_server(:bdebug => bdebug)
@@ -0,0 +1,101 @@
1
+ Feature: server api
2
+
3
+ A worker can send commands to the server
4
+
5
+ Scenario: disconnect
6
+ Given worker 1 opens a url for work
7
+ And browser 1 opens a url
8
+ Then browser 1 should be connected
9
+ When worker 1 sends disconnect for browser 1
10
+ Then browser 1 should not be connected
11
+ And worker 1 should be connected
12
+
13
+ Scenario: done work
14
+ Given worker 1 opens a url for work
15
+ And browser 1 opens a url
16
+ When browser 1 sends "one"
17
+ And browser 1 sends "two"
18
+ And worker 1 receives a payload
19
+ Then worker 1 should not have a payload
20
+ When worker 1 sends done_work
21
+ And worker 1 receives a payload
22
+ Then worker 1's payload message should be "two"
23
+
24
+ Scenario: done work no requeue
25
+ Given worker 1 opens a url for work
26
+ And browser 1 opens a url
27
+ When browser 1 sends "one"
28
+ And browser 1 sends "two"
29
+ And worker 1 receives a payload
30
+ Then worker 1 should not have a payload
31
+ When worker 1 sends done_work no requeue
32
+ Then worker 1 should not have a payload
33
+ When worker 1 sends done_work
34
+ And worker 1 receives a payload
35
+ Then worker 1's payload message should be "two"
36
+
37
+ Scenario: done work no requeue
38
+ Given worker 1 opens a url for work
39
+ And worker 2 opens a url for work
40
+ When browser 1 opens a url
41
+ And browser 1 sends "one"
42
+ And worker 1 receives a payload
43
+ And worker 1 sends done_work
44
+ And browser 1 sends "two"
45
+ And browser 1 sends "three"
46
+ And worker 2 receives a payload
47
+ Then worker 1 should not have a payload
48
+ When worker 2 sends done_work no requeue
49
+ And worker 1 receives a payload
50
+ Then worker 1's payload message should be "three"
51
+
52
+ Scenario: done work disconnected browser
53
+ Given worker 1 opens a url for work
54
+ And worker 2 opens a url for work
55
+ And browser 1 opens a url with notify
56
+ And worker 1 receives a payload
57
+ Then worker 1's payload message should be "/open"
58
+ And browser 1 disconnects
59
+ Then worker 2 should not have a payload
60
+ When worker 1 sends done_work
61
+ And worker 2 receives a payload
62
+ Then worker 2's payload message should be "/close"
63
+
64
+ Scenario: done work disconnected browser
65
+ Given worker 1 opens a url for work
66
+ And browser 1 opens a url with notify
67
+ When browser 1 sends "one"
68
+ And browser 1 sends "two"
69
+ And worker 1 receives a payload
70
+ Then worker 1's payload message should be "/open"
71
+ And browser 1 disconnects
72
+ When worker 1 sends done_work
73
+ And worker 1 receives a payload
74
+ Then worker 1's payload message should be "/close"
75
+
76
+ Scenario: done work disconnected browser
77
+ Given worker 1 opens a url for work
78
+ And browser 1 opens a url with notify
79
+ When browser 1 sends "one"
80
+ And browser 1 sends "two"
81
+ And worker 1 receives a payload
82
+ Then worker 1's payload message should be "/open"
83
+ When worker 1 sends done_work
84
+ And worker 1 receives a payload
85
+ And browser 1 disconnects
86
+ When worker 1 sends done_work
87
+ And worker 1 receives a payload
88
+ Then worker 1's payload message should be "/close"
89
+
90
+ Scenario: done work disconnected browser no requeue
91
+ Given worker 1 opens a url for work
92
+ And browser 1 opens a url with notify
93
+ And worker 1 receives a payload
94
+ Then worker 1's payload message should be "/open"
95
+ When browser 1 disconnects
96
+ And worker 2 opens a url for work
97
+ Then worker 1 should not have a payload
98
+ And worker 2 should not have a payload
99
+ When worker 1 sends done_work no requeue
100
+ And worker 2 receives a payload
101
+ Then worker 2's payload message should be "/close"
@@ -0,0 +1,38 @@
1
+ Feature: server api disconnects on bad message
2
+
3
+ Workers must send a json hash
4
+ Send messages requires
5
+
6
+ Background:
7
+ Given worker 1 opens a url
8
+ Then worker 1 should be connected
9
+
10
+ Scenario: not json
11
+ When worker 1 sends_eval "nil"
12
+ When worker 1 receives a payload
13
+ Then worker 1's payload should be "message must be JSON encoded"
14
+ Then worker 1 should not be connected
15
+
16
+ Scenario: not dictionary
17
+ When worker 1 sends_eval "Array.new"
18
+ When worker 1 receives a payload
19
+ Then worker 1's payload should be "message must be dictionary"
20
+ Then worker 1 should not be connected
21
+
22
+ Scenario: send_messages not hash
23
+ When worker 1 sends_eval "{ :send_messages => Array.new }"
24
+ When worker 1 receives a payload
25
+ Then worker 1's payload should be "send_messages must be dictionary"
26
+ Then worker 1 should not be connected
27
+
28
+ Scenario: send_messages keys must be strings
29
+ When worker 1 sends_eval "{ :send_messages => { 3 => nil } }"
30
+ When worker 1 receives a payload
31
+ Then worker 1's payload should be "send_messages keys must be strings and values must be arrays"
32
+ Then worker 1 should not be connected
33
+
34
+ Scenario: send_messages values must be arrays
35
+ When worker 1 sends_eval "{ :send_messages => { '3' => nil } }"
36
+ When worker 1 receives a payload
37
+ Then worker 1's payload should be "send_messages keys must be strings and values must be arrays"
38
+ Then worker 1 should not be connected
@@ -0,0 +1,54 @@
1
+ Feature: worker and browser connect
2
+
3
+ Workers can connect, disconnect and reconnect with worker url
4
+ Browsers can connect, disconnect with browser url
5
+
6
+ Scenario: worker connects
7
+ When worker 1 opens a url
8
+ Then worker 1 should be connected
9
+
10
+ Scenario: browser connects to unknown grid
11
+ When browser 1 opens a url
12
+ Then browser 1 should not be connected
13
+
14
+ Scenario: worker and browser connect
15
+ When worker 1 opens a url
16
+ And browser 1 opens a url
17
+ Then browser 1 should be connected
18
+ And worker 1 should be connected
19
+
20
+ Scenario: worker reconnects
21
+ When worker 1 opens a url
22
+ Then worker 1 should be connected
23
+ When worker 1 disconnects
24
+ And worker 1 opens a url
25
+ Then worker 1 should be connected
26
+
27
+ Scenario: browser disconnected when worker d/c'd
28
+ When worker 1 opens a url
29
+ And browser 1 opens a url
30
+ Then browser 1 should be connected
31
+ When worker 1 disconnects
32
+ Then browser 1 should not be connected
33
+
34
+ Scenario: queued browser work is removed when browser d/c's
35
+ When worker 1 opens a url
36
+ And browser 1 opens a url
37
+ And browser 1 sends "one"
38
+ And browser 1 disconnects
39
+ When worker 1 sends done_work
40
+ Then worker 1 should not have a payload
41
+ When browser 2 opens a url
42
+ And browser 2 sends "two"
43
+ And worker 1 receives a payload
44
+ Then worker 1's payload message should be "two"
45
+
46
+ Scenario: browser disconnected when workers d/c'd
47
+ When worker 1 opens a url
48
+ And browser 1 opens a url
49
+ And worker 2 opens a url
50
+ Then browser 1 should be connected
51
+ When worker 1 disconnects
52
+ Then browser 1 should be connected
53
+ When worker 2 disconnects
54
+ Then browser 1 should not be connected
@@ -0,0 +1,19 @@
1
+ Feature: notify
2
+ In order to know when browsers connect and disconnect
3
+ The url can include notify=true
4
+ The server will queue a message "from" the browser
5
+ The message is either "/open" or "/close"
6
+
7
+ Scenario: notify on connect and disconnect
8
+ Given worker 1 opens a url for work
9
+ When browser 1 opens a url with notify
10
+ And worker 1 receives a payload
11
+ Then worker 1's payload route should be "test"
12
+ Then worker 1's payload bid should be "1"
13
+ And worker 1's payload message should be "/open"
14
+ Given worker 1 sends done_work
15
+ When browser 1 disconnects
16
+ And worker 1 receives a payload
17
+ Then worker 1's payload route should be "test"
18
+ Then worker 1's payload bid should be "1"
19
+ Then worker 1's payload message should be "/close"
@@ -0,0 +1,8 @@
1
+ Feature: test performance
2
+ see how well it holds up
3
+
4
+ Scenario: performance
5
+ #When 500 grids connect
6
+ #When 500 workers connect
7
+ #When 500 browsers connect
8
+ #When a browser sends 10 messages
@@ -0,0 +1,21 @@
1
+ Feature: two-way asynchronous message passing
2
+
3
+ A worker connects to the em-breakout server and receives messages sent from the server
4
+ The messages always originate from a specific browser identified by bid
5
+ The worker interacts with browsers through a server api, which includes
6
+ Sending a message to one or more browsers identified by bid
7
+ Disconnecting one or more browsers from the server
8
+ The api also includes a the done_work command to inform the server that the browser message is done being processed
9
+
10
+ Scenario: browser pings worker
11
+ When worker 1 opens a url for work
12
+ And browser 1 opens a url for "ping"
13
+ When browser 1 sends "ping"
14
+ And browser 1 sends "ping 2"
15
+ And worker 1 receives a payload
16
+ Then worker 1's payload route should be "ping"
17
+ And worker 1's payload message should be "ping"
18
+ When worker 1 sends message "pong" to browser 1
19
+ And browser 1 receives a payload
20
+ Then browser 1's payload should be "pong"
21
+
@@ -0,0 +1,47 @@
1
+ Feature: security
2
+ In order to control access to my workers
3
+ As a browser url publisher
4
+ I want my urls to be tamper-proof
5
+
6
+ Scenario: unknown grid
7
+ When browser 1 opens a url
8
+ And browser 1 receives a payload
9
+ Then browser 1's payload should be "unknown grid"
10
+ And browser 1 should be disconnected
11
+
12
+ Scenario: bid in use
13
+ Given worker 1 opens a url
14
+ When browser 1 opens a url with bid "foo"
15
+ And browser 2 opens a url with bid "foo"
16
+ And browser 2 receives a payload
17
+ Then browser 2's payload should be "bid in use"
18
+ And browser 2 should be disconnected
19
+ And browser 1 should be connected
20
+
21
+ Scenario: expired link
22
+ Given worker 1 opens a url
23
+ When browser 1 opens an expired url
24
+ And browser 1 receives a payload
25
+ Then browser 1's payload should be "expired"
26
+
27
+ Scenario: Tamper with route
28
+ Given worker 1 opens a url
29
+ When browser 1 opens a tampered url
30
+ And browser 1 receives a payload
31
+ Then browser 1's payload should be "invalid url"
32
+
33
+ Scenario: invalid worker url
34
+ When worker 1 opens a url without grid
35
+ And worker 1 receives a payload
36
+ Then worker 1's payload should be "invalid grid"
37
+ And worker 1 should be disconnected
38
+
39
+ Scenario: grid is taken / wrong grid key
40
+ When worker 1 opens a url
41
+ And the grid_key config is changed
42
+ When worker 2 opens a url
43
+ And worker 2 receives a payload
44
+ Then worker 2's payload should be "invalid grid_key"
45
+ And worker 2 should be disconnected
46
+ And worker 1 should be connected
47
+
@@ -0,0 +1,17 @@
1
+ Feature: two workers will not get messages from the same browser at the same time
2
+ In order to keep it simple
3
+ Messages from a single browser will be queued up
4
+ And the next message will not be sent to any worker
5
+ Until the worker receiving the previous message sends done_work
6
+
7
+ Scenario: browser sends two messages
8
+ Given worker 1 opens a url for work
9
+ And worker 2 opens a url for work
10
+ When browser 1 opens a url
11
+ And browser 1 sends "one"
12
+ And browser 1 sends "two"
13
+ And worker 1 receives a payload
14
+ Then worker 2 should not have a payload
15
+ When worker 1 sends done_work
16
+ And worker 2 receives a payload
17
+ Then worker 2's payload message should be "two"
@@ -0,0 +1,155 @@
1
+ When /^worker (\d+) opens a url$/ do |id|
2
+ @worker_by_id[id] = Breakout::Socket.new(Breakout.worker_url)
3
+ end
4
+
5
+ When /^browser (\d+) opens a url for "([^"]*)"$/ do |id, route|
6
+ @browser_by_id[id] = Breakout::Socket.new(Breakout.browser_url(route, :bid => id))
7
+ end
8
+
9
+ When /^browser (\d+) opens a url with bid "([^"]*)"$/ do |id, bid|
10
+ @browser_by_id[id] = Breakout::Socket.new(Breakout.browser_url("test", :bid => bid))
11
+ end
12
+
13
+ When /^browser (\d+) opens a url with notify$/ do |id|
14
+ @browser_by_id[id] = Breakout::Socket.new(Breakout.browser_url("test", :bid => id, :notify => true))
15
+ end
16
+
17
+ When /^worker (\d+) opens a url for work$/ do |id|
18
+ @worker_by_id[id] = Breakout::Socket.new(Breakout.worker_url)
19
+ @worker_by_id[id].send :done_work => true
20
+ end
21
+
22
+ When /^browser (\d+) opens a url$/ do |id|
23
+ When %|browser #{id} opens a url for "test"|
24
+ end
25
+
26
+ When /^browser (\d+) sends "([^"]*)"$/ do |id, msg|
27
+ @browser_by_id[id].send(msg)
28
+ end
29
+
30
+ When /^worker (\d+) receives a payload$/ do |id|
31
+ Timeout::timeout(0.1) { @worker_payload_by_id[id] = @worker_by_id[id].receive }
32
+ end
33
+
34
+ Then /^worker (\d+) should not have a payload$/ do |id|
35
+ ->() do
36
+ When %|worker #{id} receives a payload|
37
+ end.should raise_error(Timeout::Error)
38
+ end
39
+
40
+ Then /^worker (\d+)'s payload route should be "([^"]*)"$/ do |id, route|
41
+ @worker_payload_by_id[id].split("\n", 3)[0].should == route
42
+ end
43
+
44
+ Then /^worker (\d+)'s payload message should be "([^"]*)"$/ do |id, msg|
45
+ @worker_payload_by_id[id].split("\n", 3)[2].should == msg
46
+ end
47
+
48
+ Then /^worker (\d+)'s payload bid should be "([^"]*)"$/ do |id, msg|
49
+ @worker_payload_by_id[id].split("\n", 3)[1].should == msg
50
+ end
51
+
52
+ Then /^worker (\d+)'s payload should be "([^"]*)"$/ do |id, msg|
53
+ @worker_payload_by_id[id].should == msg
54
+ end
55
+
56
+ When /^worker (\d+) sends done_work$/ do |id|
57
+ @worker_by_id[id].send :done_work => true
58
+ end
59
+
60
+ When /^worker (\d+) sends done_work no requeue$/ do |id|
61
+ @worker_by_id[id].send :done_work => false
62
+ end
63
+
64
+ When /^worker (\d+) sends disconnect for browser (\d+)$/ do |id, bid|
65
+ @worker_by_id[id].send :disconnect => bid
66
+ end
67
+
68
+ When /^worker (\d+) sends message "([^"]*)" to browser (\d+)$/ do |id, msg, bid|
69
+ @worker_by_id[id].send :send_messages => { msg => [ bid ] }
70
+ end
71
+
72
+ When /^browser (\d+) receives a payload$/ do |id|
73
+ Timeout::timeout(1) { @browser_payload_by_id[id] = @browser_by_id[id].receive }
74
+ end
75
+
76
+ Then /^browser (\d+)'s payload should be "([^"]*)"$/ do |id, msg|
77
+ @browser_payload_by_id[id].should == msg
78
+ end
79
+
80
+ Then /^worker (\d+) should be connected$/ do |id|
81
+ ->() do
82
+ Timeout::timeout(0.1) { @worker_by_id[id].receive }
83
+ end.should raise_error(Timeout::Error)
84
+ end
85
+
86
+ Then /^browser (\d+) should be connected$/ do |id|
87
+ ->() do
88
+ Timeout::timeout(0.1) { @browser_by_id[id].receive }
89
+ end.should raise_error(Timeout::Error)
90
+ end
91
+
92
+ Then /^browser (\d+) should not be connected$/ do |id|
93
+ ->() do
94
+ Timeout::timeout(0.1) { @browser_by_id[id].receive }
95
+ end.should_not raise_error(Timeout::Error)
96
+ end
97
+
98
+ Then /^worker (\d+) should not be connected$/ do |arg1|
99
+ ->() do
100
+ Timeout::timeout(0.1) { @worker_by_id[id].receive }
101
+ end.should_not raise_error(Timeout::Error)
102
+ end
103
+
104
+ When /^browser (\d+) disconnects$/ do |id|
105
+ @browser_by_id[id].close
106
+ end
107
+
108
+ When /^worker (\d+) disconnects$/ do |id|
109
+ @worker_by_id[id].close
110
+ end
111
+
112
+ Then /^browser (\d+) should be disconnected$/ do |id|
113
+ Then "browser #{id} should not be connected"
114
+ end
115
+
116
+ Then /^worker (\d+) should be disconnected$/ do |id|
117
+ Then "worker #{id} should not be connected"
118
+ end
119
+
120
+ When /^browser (\d+) opens an expired url$/ do |id|
121
+ url = Breakout.browser_url('test', :e => (Time.now - 1).to_i)
122
+ @browser_by_id[id] = Breakout::Socket.new(url)
123
+ end
124
+
125
+ When /^browser (\d+) opens a tampered url$/ do |id|
126
+ url = Breakout.browser_url('REALROUTE').gsub(/REALROUTE/, 'TAMPEREDROUTE')
127
+ @browser_by_id[id] = Breakout::Socket.new(url)
128
+ end
129
+
130
+ When /^worker (\d+) opens a url without grid$/ do |id|
131
+ url = "ws://#{Breakout::CONFIG[:breakout_host]}:#{Breakout::CONFIG[:worker_port]}/?grid_key=#{Breakout::CONFIG[:grid_key]}"
132
+ @worker_by_id[id] = Breakout::Socket.new(url)
133
+ end
134
+
135
+ When /^worker (\d+) sends_eval "([^"]*)"$/ do |id, rb|
136
+ @worker_by_id[id].send eval(rb)
137
+ end
138
+
139
+ When /^worker (\d+) sends not json$/ do |id|
140
+ @worker_by_id[id].send nil
141
+ end
142
+
143
+ When /^worker (\d+) sends not dictionary$/ do |id|
144
+ @worker_by_id[id].send Array.new
145
+ end
146
+
147
+ When /^the ([\w]*) config is changed$/ do |option|
148
+ option = option.to_sym
149
+ raise option unless Breakout::CONFIG.has_key? option
150
+ Breakout.config(option => Breakout.random_config[option])
151
+ end
152
+
153
+ Given /^the server is restarted$/ do
154
+ restart_server
155
+ end
@@ -0,0 +1,46 @@
1
+ When /^(\d+) workers connect$/ do |total|
2
+ total = [1, total.to_i].max
3
+ i = 0
4
+ while i < total
5
+ i += 1
6
+ @worker_by_id[i] = Breakout::Socket.new(Breakout.worker_url)
7
+ end
8
+ end
9
+
10
+ When /^(\d+) browsers connect$/ do |total|
11
+ total = [1, total.to_i].max
12
+ i = 0
13
+ while i < total
14
+ i += 1
15
+ @browser_by_id[total] = Breakout::Socket.new(Breakout.browser_url('test'))
16
+ end
17
+ end
18
+
19
+ When /^a browser sends (\d+) messages$/ do |total|
20
+ total = [1, total.to_i].max
21
+ i = 0
22
+ @worker_by_id[total] = Breakout::Socket.new(Breakout.worker_url)
23
+ @browser_by_id[total] = Breakout::Socket.new(Breakout.browser_url('test'))
24
+ while i < total
25
+ i += 1
26
+ @browser_by_id[total].send(i)
27
+ end
28
+ end
29
+
30
+ When /^(\d+) grids connect$/ do |total|
31
+ total = [1, total.to_i].max
32
+ i = 0
33
+ while i < total
34
+ i += 1
35
+
36
+ Breakout.config(:grid => i)
37
+ @worker_by_id[i] = Breakout::Socket.new(Breakout.worker_url)
38
+ @browser_by_id[i] = Breakout::Socket.new(Breakout.browser_url("test", :bid => i))
39
+ @browser_by_id[i].send(i)
40
+ @worker_by_id[i].receive
41
+ @worker_by_id[i].send :send_messages => { i => [ i.to_s ] }
42
+ @browser_by_id[i].receive
43
+ end
44
+
45
+ #sleep(10)
46
+ end
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'bundler/setup'
5
+ require 'daemons'
6
+ require File.expand_path("lib/em-breakout")
7
+
8
+ options = {
9
+ :dir_mode => :normal,
10
+ :dir => File.expand_path('../', __FILE__),
11
+ :log_output => :true,
12
+ :app_name => 'em-breakout'
13
+ }
14
+
15
+
16
+ breakout_opts = {
17
+ :worker_port => 8001,
18
+ :browser_port => 8002,
19
+ :debug => ENV['EMDEBUG'],
20
+ :bdebug => ENV['BDEBUG']
21
+ }
22
+
23
+ Daemons.run_proc('em-breakout', options) do
24
+
25
+ if ENV['EMPROFILE']
26
+ require 'ruby-prof'
27
+ result = RubyProf.profile { EventMachine::Breakout.start_server(breakout_opts) }
28
+ printer = RubyProf::GraphHtmlPrinter.new(result)
29
+ printer.print(File.open(File.expand_path('../stats.html', __FILE__),'w'), :min_percent => 5)
30
+ else
31
+ EventMachine::Breakout.start_server(breakout_opts)
32
+ end
33
+
34
+
35
+ end
@@ -0,0 +1,13 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'timeout'
4
+ require 'breakout'
5
+
6
+ breakout_opts = {
7
+ :worker_port => 8001,
8
+ :browser_port => 8002
9
+ }
10
+
11
+ Before do
12
+ Breakout.config(Breakout.random_config.merge(breakout_opts))
13
+ end
@@ -0,0 +1,18 @@
1
+ `rm -f #{File.expand_path('../em-breakout.output', __FILE__)}`
2
+ CONTROL_SCRIPT = File.expand_path('../em_control', __FILE__)
3
+
4
+ module ServerHelper
5
+ def restart_server
6
+ #`#{::CONTROL_SCRIPT} stop ; EMDEBUG=true #{::CONTROL_SCRIPT} start`
7
+ #`#{::CONTROL_SCRIPT} stop ; BDEBUG=true #{::CONTROL_SCRIPT} start`
8
+ `#{::CONTROL_SCRIPT} stop ; #{::CONTROL_SCRIPT} start`
9
+ end
10
+ module_function :restart_server
11
+ end
12
+
13
+ World(ServerHelper)
14
+
15
+ ServerHelper.restart_server
16
+ at_exit do
17
+ `#{CONTROL_SCRIPT} stop`
18
+ end
@@ -0,0 +1,7 @@
1
+ #WebSocket.debug = true
2
+ Before do
3
+ @worker_by_id = {}
4
+ @worker_payload_by_id = Hash.new { |args| [] }
5
+ @browser_by_id = {}
6
+ @browser_payload_by_id = Hash.new { |args| [] }
7
+ end
@@ -0,0 +1,94 @@
1
+ module EventMachine
2
+ module Breakout
3
+ class Browser < Connection
4
+
5
+ attr_accessor :wip #true from when a worker gets sent a message until the worker sends :done_work, false all other times
6
+ attr_reader :bid, #browser id
7
+ :route, #handler to which worker should route this browser's messages
8
+ :notify, #whether to send notification to route handler for browser open/close
9
+ :message_queue #for incoming browser messages waiting to be pulled by a worker
10
+
11
+ def breakout(debug=false)
12
+
13
+ onopen do
14
+ @grid_name = request["Path"].split('?').first.gsub('/','')
15
+ @grid = GRIDS[@grid_name]
16
+ @route = request["Query"]["route"]
17
+ @bid = request["Query"]["bid"]
18
+ @notify = request["Query"]["notify"] == "true" ? true : false
19
+ @e = request["Query"]["e"].to_i
20
+ @gat = request["Query"]["gat"]
21
+
22
+ log(%|grid_name: #{@grid_name}\nroute: #{@route}\nbid: #{@bid}\ne: #{@e}\n| +
23
+ %|gat: #{@gat}\nnotify: #{@notify}|) if debug
24
+
25
+ catch :break do
26
+ if !@grid
27
+ close_websocket "unknown grid"
28
+ throw :break
29
+ end
30
+
31
+ if @grid.browsers.has_key?(@bid)
32
+ close_websocket "bid in use"
33
+ throw :break
34
+ end
35
+
36
+ if @e < Time.now.to_i
37
+ close_websocket "expired"
38
+ throw :break
39
+ end
40
+
41
+ unless @gat == ::Breakout.grid_access_token(@route, @bid, @e, @notify, @grid.grid_key)
42
+ close_websocket "invalid url"
43
+ throw :break
44
+ end
45
+
46
+ @message_queue = []
47
+ @grid.browsers[@bid] = self
48
+ if @notify
49
+ @message_queue << "/open"
50
+ @grid.work_queue[self] = true
51
+ EventMachine.next_tick { @grid.try_work }
52
+ end
53
+ end
54
+ end
55
+
56
+ onmessage do |msg|
57
+ log("msg: #{msg}") if debug
58
+
59
+ unless @is_closing
60
+
61
+ if @message_queue.empty? && !@wip
62
+ @grid.work_queue[self] = true
63
+ EventMachine.next_tick { @grid.try_work }
64
+ end
65
+
66
+ @message_queue << msg
67
+ end
68
+ end
69
+
70
+ onclose do
71
+ log("closing") if debug
72
+
73
+ if @message_queue
74
+
75
+ if @notify
76
+ @grid.disconnected_browsers[@bid] = self
77
+ @message_queue = ["/close"]
78
+ unless @wip
79
+ @grid.work_queue[self] = true
80
+ EventMachine.next_tick { @grid.try_work }
81
+ end
82
+ else
83
+ @grid.work_queue.delete self
84
+ end
85
+ end
86
+ end
87
+
88
+ onerror do |reason|
89
+ log reason.pretty
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,28 @@
1
+ module EventMachine
2
+ module Breakout
3
+ class Connection < EventMachine::WebSocket::Connection
4
+ @@connection_counter = 0
5
+
6
+ attr_reader :grid_name, #public name of worker-browser group
7
+ :grid, #Grid instance
8
+ :is_closing #close_websocket has been invoked
9
+
10
+ def cid
11
+ @cid ||= @@connection_counter += 1
12
+ end
13
+
14
+ def close_websocket(msg=nil)
15
+ unless @is_closing
16
+ @is_closing = true
17
+ send(msg) if msg
18
+ super()
19
+ end
20
+ end
21
+
22
+ def log(msg)
23
+ puts "** #{self.class.name.split('::').last} #{cid} ********** \n#{msg}\n\n\n"
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,10 @@
1
+ class Exception
2
+ def pretty
3
+ begin
4
+ %Q[\n#{self.class} (#{self.message}):\n #{backtrace.join("\n ")}\n]
5
+ rescue Exception => e
6
+ "error in Exception#pretty: #{e}"
7
+ end
8
+ end
9
+ end
10
+
@@ -0,0 +1,44 @@
1
+ module EventMachine
2
+ module Breakout
3
+
4
+ GRIDS = Hash.new # grid_name => grid
5
+
6
+ class Grid
7
+
8
+ attr_reader :grid_key, :workers, :browsers, :disconnected_browsers, :work_queue, :worker_queue
9
+
10
+ def initialize(worker)
11
+ @browsers = Hash.new # bid => browser
12
+ @disconnected_browsers = Hash.new # bid => browser
13
+ @workers = Hash.new # worker => true
14
+ @work_queue = Hash.new # browser => true
15
+ @worker_queue = Hash.new # worker => true
16
+ @grid_key = worker.request["Query"]["grid_key"]
17
+ @name = worker.grid_name
18
+ GRIDS[@name] = self
19
+ end
20
+
21
+ def release
22
+ GRIDS.delete @name
23
+ @browsers.each_value { |browser| browser.close_websocket }
24
+ @worker_queue = {}
25
+ end
26
+
27
+ def try_work
28
+ return if @work_queue.empty?
29
+ return if @worker_queue.empty?
30
+
31
+ browser = @work_queue.shift.first
32
+
33
+ msg = browser.message_queue.shift
34
+ return unless msg
35
+
36
+ worker = @worker_queue.shift.first
37
+
38
+ browser.wip = true
39
+ worker.browser = browser
40
+ worker.send("#{browser.route}\n#{browser.bid}\n#{msg}")
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,29 @@
1
+ # adapted from https://github.com/igrigorik/em-websocket/blob/866290409c35b1557017fac3c12b701af71f8d2d/lib/em-websocket/websocket.rb
2
+ module EventMachine
3
+ module Breakout
4
+
5
+ def self.start_server(opts={})
6
+ browser_port = opts[:browser_port] || 9002
7
+ worker_port = opts[:worker_port] || 9001
8
+ debug = opts[:bdebug]
9
+
10
+ EM.epoll
11
+
12
+ EventMachine::run do
13
+
14
+ trap("TERM") { EventMachine.stop }
15
+ trap("INT") { EventMachine.stop }
16
+
17
+ EventMachine::start_server('0.0.0.0', browser_port, Browser, opts) do |browser|
18
+ browser.breakout debug
19
+ end
20
+
21
+ EventMachine::start_server('0.0.0.0', worker_port, Worker, opts) do |worker|
22
+ worker.breakout debug
23
+ end
24
+
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,5 @@
1
+ module EventMachine
2
+ module Breakout
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,136 @@
1
+ module EventMachine
2
+ module Breakout
3
+ class Worker < Connection
4
+
5
+ attr_accessor :browser
6
+
7
+ def breakout(debug=false)
8
+
9
+ onopen do
10
+ @grid_name = request["Path"].split('?').first.gsub('/','')
11
+ @grid_key = request["Query"]["grid_key"]
12
+
13
+ log(%|grid: #{@grid_name}\ngrid_key: #{@grid_key}|) if debug
14
+
15
+ catch :break do
16
+ unless @grid_name && @grid_name.length > 0 && @grid_key && @grid_key.length > 0
17
+ close_websocket "invalid grid"
18
+ throw :break
19
+ end
20
+
21
+ if @grid = GRIDS[@grid_name]
22
+ unless @grid.grid_key == @grid_key
23
+ close_websocket "invalid grid_key"
24
+ throw :break
25
+ end
26
+ else
27
+ @grid = Grid.new(self)
28
+ end
29
+
30
+ @grid.workers[self] = true
31
+ end
32
+ end
33
+
34
+ onmessage do |msg|
35
+ log("msg: #{msg}") if debug
36
+
37
+ catch :break do
38
+ throw :break if @is_closing
39
+
40
+ begin
41
+ payload = JSON.parse(msg)
42
+ rescue JSON::ParserError
43
+ close_websocket "message must be JSON encoded"
44
+ throw :break
45
+ end
46
+
47
+ unless payload.is_a? Hash
48
+ close_websocket "message must be dictionary"
49
+ throw :break
50
+ end
51
+
52
+ send_messages = payload['send_messages']
53
+ if send_messages
54
+ unless send_messages.is_a? Hash
55
+ close_websocket "send_messages must be dictionary"
56
+ throw :break
57
+ end
58
+
59
+ send_messages.each_pair do |message, bids|
60
+ unless message.is_a?(String) && bids.is_a?(Array)
61
+ close_websocket "send_messages keys must be strings and values must be arrays"
62
+ throw :break
63
+ end
64
+ message_string = "#{message}"
65
+ bids.each do |bid|
66
+ next unless b = @grid.browsers[bid]
67
+ b.send(message_string) unless b.is_closing
68
+ end
69
+ end
70
+ end
71
+
72
+ disconnect = payload['disconnect']
73
+ if disconnect && b = @grid.browsers[disconnect]
74
+ b.close_websocket
75
+ end
76
+
77
+ requeue = payload['done_work']
78
+ unless requeue.nil?
79
+ next_tick = false
80
+ if @browser
81
+ @browser.wip = false
82
+ if @grid.browsers.include?(@browser.bid)
83
+ unless @browser.message_queue.empty? or @browser.is_closing
84
+ @grid.work_queue[@browser] = true
85
+ EventMachine.next_tick { @grid.try_work }
86
+ next_tick = true
87
+ end
88
+ elsif @grid.disconnected_browsers.include?(@browser.bid)
89
+ if @browser.message_queue.any?
90
+ @grid.work_queue[@browser] = true
91
+ unless next_tick
92
+ EventMachine.next_tick { @grid.try_work }
93
+ next_tick = true
94
+ end
95
+ else
96
+ @grid.disconnected_browsers.delete @browser.bid
97
+ end
98
+ end
99
+ @browser = nil
100
+ end
101
+ if requeue
102
+ @grid.worker_queue[self] = true
103
+ EventMachine.next_tick { @grid.try_work } unless next_tick
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ onclose do
110
+ log("closing") if debug
111
+
112
+ catch :break do
113
+ throw :break unless @grid
114
+ throw :break unless @grid.workers.delete self
115
+
116
+ if @grid.workers.empty?
117
+ @grid.release
118
+ throw :break
119
+ end
120
+
121
+ @grid.worker_queue.delete self
122
+ if b = @grid.browsers[@browser]
123
+ b.close_websocket
124
+ end
125
+
126
+ end
127
+ end
128
+
129
+ onerror do |reason|
130
+ log reason.pretty
131
+ end
132
+ end
133
+
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,11 @@
1
+ require 'digest'
2
+ require 'json'
3
+ require 'breakout'
4
+ require 'em-websocket'
5
+ require 'em-breakout/exception'
6
+ require 'em-breakout/connection'
7
+ require 'em-breakout/browser'
8
+ require 'em-breakout/worker'
9
+ require 'em-breakout/grid'
10
+ require 'em-breakout/server'
11
+ require 'em-breakout/version'
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: em-breakout
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Steve Masterman
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-05-01 00:00:00.000000000 -04:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: json
17
+ requirement: &81638730 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: 1.4.6
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *81638730
26
+ - !ruby/object:Gem::Dependency
27
+ name: em-websocket
28
+ requirement: &81638310 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 0.2.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *81638310
37
+ description: Breakout routes messages among web browsers and workers using WebSockets.
38
+ email:
39
+ - steve@vermonster.com
40
+ executables: []
41
+ extensions: []
42
+ extra_rdoc_files: []
43
+ files:
44
+ - .gitignore
45
+ - Gemfile
46
+ - README.md
47
+ - _LICENSE
48
+ - em-breakout.gemspec
49
+ - examples/breakout.rb
50
+ - features/api.feature
51
+ - features/api_format.feature
52
+ - features/connect.feature
53
+ - features/notify.feature
54
+ - features/performance.feature
55
+ - features/ping.feature
56
+ - features/security.feature
57
+ - features/serialize.feature
58
+ - features/step_definitions/common_steps.rb
59
+ - features/step_definitions/performance_steps.rb
60
+ - features/support/em_control
61
+ - features/support/env.rb
62
+ - features/support/server.rb
63
+ - features/support/socket.rb
64
+ - lib/em-breakout.rb
65
+ - lib/em-breakout/browser.rb
66
+ - lib/em-breakout/connection.rb
67
+ - lib/em-breakout/exception.rb
68
+ - lib/em-breakout/grid.rb
69
+ - lib/em-breakout/server.rb
70
+ - lib/em-breakout/version.rb
71
+ - lib/em-breakout/worker.rb
72
+ has_rdoc: true
73
+ homepage: https://github.com/steve9001/em-breakout
74
+ licenses: []
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ! '>='
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ! '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubyforge_project: em-breakout
93
+ rubygems_version: 1.6.2
94
+ signing_key:
95
+ specification_version: 3
96
+ summary: Breakout routes messages among web browsers and workers using WebSockets.
97
+ test_files: []