em-breakout 0.0.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/.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: []