jugglite 0.0.1.alpha

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,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.DS_Store
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in jugglite.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 andruby
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # Jugglite
2
+
3
+ Jugglite is a replacement for the incredible [Juggernaut](https://github.com/maccman/juggernaut) by Maccman. It uses [Server Sent Events](http://www.html5rocks.com/en/tutorials/eventsource/basics/) to push events from your application to the client's browser.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'jugglite'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install jugglite
18
+
19
+ ## Usage
20
+
21
+ Jugglite comes with a binary. This binary runs a thin server that listens on redis for application messages and passes it along to all connected clients.
22
+
23
+ You can run the binary from any terminal
24
+ `jugglite`
25
+
26
+ TODO: Foreman & multiple processes
27
+
28
+ TODO: Behind nginx so the client connects on one port
29
+
30
+ ## Contributing
31
+
32
+ 1. Fork it
33
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
34
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
35
+ 4. Push to the branch (`git push origin my-new-feature`)
36
+ 5. Create new Pull Request
37
+
38
+ ## License
39
+
40
+ Jugglite is licensed under the [MIT license](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rspec/core/rake_task'
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task :default => :spec
data/bin/jugglite ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ host = ENV['HOST'] || '127.0.0.1'
4
+ port = (ENV['PORT'] || 3000).to_i
5
+ file_descriptors = (ENV['FD_SIZE'] || 202400).to_i
6
+
7
+ require 'jugglite'
8
+
9
+ new_size = EM.set_descriptor_table_size( file_descriptors )
10
+ STDERR.puts "New descriptor-table size is #{new_size}"
11
+ EM.epoll
12
+ EM.kqueue
13
+
14
+ STDERR.puts "Starting server at #{host}:#{port}"
15
+ Thin::Server.start(host, port, Jugglite::App.new)
data/jugglite.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'jugglite/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "jugglite"
8
+ gem.version = Jugglite::VERSION
9
+ gem.authors = ["andruby"]
10
+ gem.email = ["andrew@bedesign.be"]
11
+ gem.description = %q{Lightweight SSE server}
12
+ gem.summary = %q{Server Sent Events server written in rack on top of thin inspired by Juggernaut for real time push}
13
+ gem.homepage = "http://github.com/andruby/jugglite"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency('thin')
21
+ gem.add_dependency('em-hiredis')
22
+ gem.add_development_dependency('redis')
23
+ gem.add_development_dependency('rspec')
24
+ end
@@ -0,0 +1,119 @@
1
+ require 'set'
2
+ require 'em-hiredis'
3
+
4
+ module Jugglite
5
+ # Let's go for plain Rack y'all
6
+ class App
7
+ AsyncResponse = [-1, {}, []].freeze
8
+ Headers = {'Content-Type' => 'text/event-stream;charset=utf-8'}
9
+
10
+ # Options include:
11
+ # +path+ : the URI path to listen to ('/stream')
12
+ # +keepalive_timeout+ : the timeout in seconds between keepalive comments
13
+ def initialize(app = nil, options = {})
14
+ @app = app
15
+ @options = {
16
+ path: '/stream',
17
+ keepalive_timeout: 20
18
+ }.merge(options)
19
+ STDERR.puts "Registered Jugglite to listen to #{@options[:path]}"
20
+ @subscription_map = {}
21
+ EventMachine::next_tick { setup_redis }
22
+ EventMachine::next_tick { setup_keepalive }
23
+ end
24
+
25
+ def call(env)
26
+ if @app.nil? || (env["PATH_INFO"] == @options[:path])
27
+ handle_stream(env)
28
+ else
29
+ @app.call(env)
30
+ end
31
+ end
32
+
33
+ private
34
+ def handle_stream(env)
35
+ request = Rack::Request.new(env)
36
+ connection = SseConnection.new(request)
37
+
38
+ # Get the headers out there, let the client know we're alive...
39
+ EventMachine::next_tick do
40
+ register_connection(connection)
41
+ # Calling thin's Connection.post_process([status, headers, body])
42
+ # This is how you start a response to the client asynchronously
43
+ env['async.callback'].call [200, Headers, connection.body]
44
+ connection.comment("registered to channels: #{channels_for_request(request).to_a.join(', ')}")
45
+ end
46
+
47
+ connection.callback { unregister_connection(connection) }
48
+ connection.errback { unregister_connection(connection) }
49
+
50
+ # Needed for crappy Rack::Lint
51
+ throw :async
52
+
53
+ # Returning a status of -1 keeps the connection open
54
+ # You need to use env['async.callback'].call to send the status, headers & body later
55
+ AsyncResponse
56
+ end
57
+
58
+ def setup_redis
59
+ @redis_channels = Set.new
60
+ @async_redis = EM::Hiredis.connect
61
+ @async_redis.on(:message) do |channel, message|
62
+ expedite_incoming_message(channel, message)
63
+ end
64
+ end
65
+
66
+ def setup_keepalive
67
+ EventMachine::add_periodic_timer(@options[:keepalive_timeout]) do
68
+ count = @subscription_map.count
69
+ @subscription_map.each_key do |connection|
70
+ connection.keepalive(count)
71
+ end
72
+ end
73
+ end
74
+
75
+ def channels_for_request(request)
76
+ # TODO: Sanitize? Check signature?
77
+ channel = Array(request.params["channel"])
78
+ Set.new(channel)
79
+ end
80
+
81
+ def register_connection(connection)
82
+ requested_channels = channels_for_request(connection.request)
83
+ subscribe_to_new_channels(requested_channels - @redis_channels)
84
+ @subscription_map[connection] = requested_channels
85
+ end
86
+
87
+ def unregister_connection(connection)
88
+ @subscription_map.delete(connection)
89
+ end
90
+
91
+ def subscribe_to_new_channels(channels)
92
+ channels.each do |channel|
93
+ puts "Listening to channel: #{channel}"
94
+ @async_redis.subscribe(channel)
95
+ @redis_channels << channel
96
+ end
97
+ end
98
+
99
+ def expedite_incoming_message(channel, message)
100
+ no_connection_listening = true
101
+ # Select upfront and use EM::Iterator
102
+ @subscription_map.each do |connection, channels|
103
+ if channels.include?(channel)
104
+ connection.write message
105
+ no_connection_listening = false
106
+ end
107
+ end
108
+ # We stop listening to a channel whenever a message comes in from a channel
109
+ # which has no associated connections
110
+ # Why? Because clients will disconnect and connect all the time when they load new pages
111
+ # But we don't want to subscribe and unsubscribe from redis all the time.
112
+ if no_connection_listening
113
+ puts "Stop listening to channel: #{channel}"
114
+ @async_redis.unsubscribe(channel)
115
+ @redis_channels.delete(channel)
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,22 @@
1
+ module Jugglite
2
+ class DeferrableBody
3
+ include EventMachine::Deferrable
4
+
5
+ def initialize
6
+ @queue = EM::Queue.new
7
+ end
8
+
9
+ def write(body)
10
+ @queue.push(body)
11
+ end
12
+
13
+ def each &blk
14
+ @body_callback = blk
15
+ processor = proc { |item|
16
+ @body_callback.call(item)
17
+ @queue.pop(&processor)
18
+ }
19
+ @queue.pop(&processor)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,42 @@
1
+ module Jugglite
2
+ class SseConnection
3
+ attr_reader :body, :request
4
+
5
+ def initialize(request)
6
+ @request = request
7
+ @body = DeferrableBody.new
8
+ end
9
+
10
+ def write(message, options = {})
11
+ buffer = ""
12
+ options.each do |k, v|
13
+ buffer << "#{k}: #{v}\n"
14
+ end
15
+ message.each_line do |line|
16
+ buffer << "data: #{line.strip}\n"
17
+ end
18
+ @body.write(buffer << "\n")
19
+ end
20
+
21
+ def keepalive(extra=nil)
22
+ # From http://dev.w3.org/html5/eventsource/#notes
23
+ comment("keepalive #{extra}")
24
+ end
25
+
26
+ def comment(comment)
27
+ @body.write(": #{comment}\n")
28
+ end
29
+
30
+ def close
31
+ @body.succeed
32
+ end
33
+
34
+ def callback(&block)
35
+ @body.callback(&block)
36
+ end
37
+
38
+ def errback(&block)
39
+ @body.errback(&block)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,3 @@
1
+ module Jugglite
2
+ VERSION = "0.0.1.alpha"
3
+ end
data/lib/jugglite.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "thin"
2
+ require "jugglite/version"
3
+ require "jugglite/deferrable_body"
4
+ require "jugglite/sse_connection"
5
+ require "jugglite/app"
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+ require 'net/http'
3
+
4
+ # Execute a GET request for +channel+ that reads the body in chunks
5
+ # until the +regexp+ is received or +timeout+ seconds have passed
6
+ def listen_on_channel_until(channel, regexp, timeout = 5)
7
+ Net::HTTP.start(@host, @port) do |http|
8
+ request = Net::HTTP::Get.new("/?channel=#{@channel}")
9
+
10
+ body = ""
11
+ http.request(request) do |response|
12
+ start_time = Time.now
13
+ response.read_body do |chunk|
14
+ body << chunk
15
+ body.should include(": registered to channels: #{@channel}")
16
+ http.finish
17
+ break
18
+ end
19
+ break
20
+ end
21
+ end
22
+ end
23
+
24
+ describe "Streaming and publishing" do
25
+ before(:all) do
26
+ @host = '127.0.0.1'
27
+ @port = rand(10000)+10000
28
+ @channel = "randomized:test:#{rand(2**32)}"
29
+ @app = nil
30
+ @thread = Thread.new do
31
+ @app = Thin::Server.new(@host, @port, Jugglite::App.new)
32
+ @app.start
33
+ end
34
+ sleep(0.01) until @app && @app.running?
35
+ end
36
+
37
+ after(:all) do
38
+ @app.stop!
39
+ @thread.join
40
+ end
41
+
42
+ it "should keep a GET open" do
43
+ Net::HTTP.start(@host, @port) do |http|
44
+ request = Net::HTTP::Get.new("/?channel=#{@channel}")
45
+
46
+ body = ""
47
+ http.request(request) do |response|
48
+ response.read_body do |chunk|
49
+ body << chunk
50
+ body.should include(": registered to channels: #{@channel}")
51
+ http.finish
52
+ break
53
+ end
54
+ break
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,57 @@
1
+ # Will try to open as many connections as possible
2
+ # at a rate of about 500 new connections per second.
3
+ # Every 2 seconds the script will print out the number of:
4
+ # connections triggered, connections that received headers,
5
+ # connections failed and connections finished.
6
+
7
+ # Optional env variables and their defaults:
8
+ # HOST: the hostname to connect to (127.0.0.1)
9
+ # PORT: the port to connect to (3000)
10
+ # RATE: the number of connections per second to open (1000)
11
+ # FD_SIZE: try and increase the number of open file descriptors (202400)
12
+
13
+ # Note: You'll probably need to increase the open file descriptor limit
14
+ # for your platform to reach the limits.
15
+ # On *nix you can use "ulimit -n XXXXX" to set and "ulimit -n" to read the limit
16
+
17
+ host = ENV['HOST'] || '127.0.0.1'
18
+ port = (ENV['PORT'] || 3000).to_i
19
+ $between_time = 1.0 / (ENV['RATE'] || 2000).to_i
20
+ file_descriptors = (ENV['FD_SIZE'] || 202400).to_i
21
+ $url = "http://#{host}:#{port}/stream"
22
+
23
+ require 'eventmachine'
24
+ require 'redis'
25
+ require 'em-http-request'
26
+
27
+ new_size = EM.set_descriptor_table_size(file_descriptors)
28
+ STDERR.puts "New EventMachine descriptor-table size is #{new_size}"
29
+ EM.epoll
30
+ EM.kqueue
31
+
32
+ $trigger_count = 0
33
+ $headers_count = 0
34
+ $error_count = 0
35
+ $finish_count = 0
36
+
37
+ def connect_to_stream(counter)
38
+ http = EventMachine::HttpRequest.new($url, :inactivity_timeout => 30).get
39
+ http.headers { |hash| $headers_count+= 1 }
40
+ http.callback { |result| $finish_count+= 1 }
41
+ http.errback { |obj| $error_count+=1 }
42
+ end
43
+
44
+ EM.run do
45
+ launch_timer = EventMachine::add_periodic_timer($between_time) do
46
+ connect_to_stream($trigger_count+=1)
47
+ end
48
+
49
+ EM::next_tick { $start_time = Time.now } # Set the start time
50
+
51
+ monitor_timer = EventMachine::add_periodic_timer(2) do
52
+ secs_elapsed = (Time.now - $start_time).round
53
+ print "#{'%4i' % secs_elapsed} triggered: #{'%6i' % $trigger_count}, "
54
+ print "headered: #{'%6i' % $headers_count}, errors: #{'%6i' % $error_count}, "
55
+ puts "finish_count: #{'%6i' % $finish_count}"
56
+ end
57
+ end
@@ -0,0 +1,19 @@
1
+ require_relative '../lib/jugglite'
2
+
3
+ # This file was generated by the `rspec --init` command. Conventionally, all
4
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
5
+ # Require this file using `require "spec_helper"` to ensure that it is only
6
+ # loaded once.
7
+ #
8
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
9
+ RSpec.configure do |config|
10
+ config.treat_symbols_as_metadata_keys_with_true_values = true
11
+ config.run_all_when_everything_filtered = true
12
+ config.filter_run :focus
13
+
14
+ # Run specs in random order to surface order dependencies. If you find an
15
+ # order dependency and want to debug it, you can fix the order by providing
16
+ # the seed, which is printed after each run.
17
+ # --seed 1234
18
+ config.order = 'random'
19
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jugglite
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.alpha
5
+ prerelease: 6
6
+ platform: ruby
7
+ authors:
8
+ - andruby
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: thin
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: em-hiredis
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: redis
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rspec
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ description: Lightweight SSE server
79
+ email:
80
+ - andrew@bedesign.be
81
+ executables:
82
+ - jugglite
83
+ extensions: []
84
+ extra_rdoc_files: []
85
+ files:
86
+ - .gitignore
87
+ - .rspec
88
+ - Gemfile
89
+ - LICENSE.txt
90
+ - README.md
91
+ - Rakefile
92
+ - bin/jugglite
93
+ - jugglite.gemspec
94
+ - lib/jugglite.rb
95
+ - lib/jugglite/app.rb
96
+ - lib/jugglite/deferrable_body.rb
97
+ - lib/jugglite/sse_connection.rb
98
+ - lib/jugglite/version.rb
99
+ - spec/acceptance/stream_and_publish_spec.rb
100
+ - spec/benchmark/max_connections.rb
101
+ - spec/spec_helper.rb
102
+ homepage: http://github.com/andruby/jugglite
103
+ licenses: []
104
+ post_install_message:
105
+ rdoc_options: []
106
+ require_paths:
107
+ - lib
108
+ required_ruby_version: !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ! '>='
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ! '>'
118
+ - !ruby/object:Gem::Version
119
+ version: 1.3.1
120
+ requirements: []
121
+ rubyforge_project:
122
+ rubygems_version: 1.8.23
123
+ signing_key:
124
+ specification_version: 3
125
+ summary: Server Sent Events server written in rack on top of thin inspired by Juggernaut
126
+ for real time push
127
+ test_files:
128
+ - spec/acceptance/stream_and_publish_spec.rb
129
+ - spec/benchmark/max_connections.rb
130
+ - spec/spec_helper.rb
131
+ has_rdoc: