alondra 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +9 -0
- data/Gemfile +22 -0
- data/Gemfile.lock +166 -0
- data/MIT-LICENSE +20 -0
- data/README.md +177 -0
- data/Rakefile +34 -0
- data/alondra.gemspec +23 -0
- data/app/assets/javascripts/alondra-client.js.coffee.erb +71 -0
- data/app/assets/javascripts/moz_websocket.js +5 -0
- data/app/assets/javascripts/vendor/jquery.json-2.2.js +178 -0
- data/app/assets/javascripts/vendor/json2.js +480 -0
- data/app/assets/javascripts/vendor/swfobject.js +4 -0
- data/app/assets/javascripts/vendor/web_socket.js +379 -0
- data/app/assets/swf/WebSocketMain.swf +0 -0
- data/app/helpers/alondra_helper.rb +24 -0
- data/lib/alondra/changes_callbacks.rb +52 -0
- data/lib/alondra/changes_push.rb +23 -0
- data/lib/alondra/channel.rb +84 -0
- data/lib/alondra/command.rb +38 -0
- data/lib/alondra/command_dispatcher.rb +22 -0
- data/lib/alondra/connection.rb +52 -0
- data/lib/alondra/event.rb +74 -0
- data/lib/alondra/event_listener.rb +86 -0
- data/lib/alondra/event_router.rb +27 -0
- data/lib/alondra/listener_callback.rb +37 -0
- data/lib/alondra/message.rb +35 -0
- data/lib/alondra/message_queue.rb +70 -0
- data/lib/alondra/message_queue_client.rb +71 -0
- data/lib/alondra/push_controller.rb +53 -0
- data/lib/alondra/pushing.rb +14 -0
- data/lib/alondra/server.rb +44 -0
- data/lib/alondra/session_parser.rb +57 -0
- data/lib/alondra.rb +80 -0
- data/lib/generators/alondra/USAGE +8 -0
- data/lib/generators/alondra/alondra_generator.rb +7 -0
- data/lib/generators/alondra/templates/alondra +33 -0
- data/lib/tasks/alondra_tasks.rake +4 -0
- data/script/rails +6 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js +9 -0
- data/test/dummy/app/assets/stylesheets/application.css +7 -0
- data/test/dummy/app/controllers/application_controller.rb +14 -0
- data/test/dummy/app/controllers/chats_controller.rb +85 -0
- data/test/dummy/app/controllers/messages_controller.rb +12 -0
- data/test/dummy/app/controllers/sessions_controller.rb +20 -0
- data/test/dummy/app/controllers/users_controller.rb +30 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/helpers/chats_helper.rb +6 -0
- data/test/dummy/app/helpers/error_messages_helper.rb +23 -0
- data/test/dummy/app/helpers/layout_helper.rb +22 -0
- data/test/dummy/app/mailers/.gitkeep +0 -0
- data/test/dummy/app/models/.gitkeep +0 -0
- data/test/dummy/app/models/chat.rb +5 -0
- data/test/dummy/app/models/message.rb +4 -0
- data/test/dummy/app/models/user.rb +37 -0
- data/test/dummy/app/views/chats/_form.html.erb +21 -0
- data/test/dummy/app/views/chats/edit.html.erb +6 -0
- data/test/dummy/app/views/chats/index.html.erb +23 -0
- data/test/dummy/app/views/chats/new.html.erb +5 -0
- data/test/dummy/app/views/chats/show.html.erb +49 -0
- data/test/dummy/app/views/layouts/application.html.erb +19 -0
- data/test/dummy/app/views/sessions/new.html.erb +15 -0
- data/test/dummy/app/views/shared/_message.js.erb +1 -0
- data/test/dummy/app/views/users/_form.html.erb +20 -0
- data/test/dummy/app/views/users/edit.html.erb +3 -0
- data/test/dummy/app/views/users/index.html.erb +25 -0
- data/test/dummy/app/views/users/new.html.erb +5 -0
- data/test/dummy/app/views/users/show.html.erb +15 -0
- data/test/dummy/config/application.rb +42 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +31 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +27 -0
- data/test/dummy/config/environments/production.rb +54 -0
- data/test/dummy/config/environments/test.rb +39 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +10 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +9 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +12 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +19 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/db/migrate/20110719090458_create_chats.rb +9 -0
- data/test/dummy/db/migrate/20110719090538_create_messages.rb +10 -0
- data/test/dummy/db/migrate/20110720193249_create_users.rb +16 -0
- data/test/dummy/db/schema.rb +38 -0
- data/test/dummy/lib/controller_authentication.rb +48 -0
- data/test/dummy/log/.gitkeep +0 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +26 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/public/stylesheets/application.css +75 -0
- data/test/dummy/script/rails +6 -0
- data/test/integration/push_changes_test.rb +41 -0
- data/test/integration/push_messages_test.rb +40 -0
- data/test/models/channel_test.rb +49 -0
- data/test/models/command_test.rb +29 -0
- data/test/models/configuration_test.rb +22 -0
- data/test/models/connection_test.rb +18 -0
- data/test/models/event_listener_test.rb +217 -0
- data/test/models/event_router_test.rb +7 -0
- data/test/models/message_queue_client_test.rb +26 -0
- data/test/models/message_queue_test.rb +70 -0
- data/test/models/pushing_test.rb +34 -0
- data/test/performance/message_queue_performance.rb +66 -0
- data/test/support/factories.rb +19 -0
- data/test/support/integration_helper.rb +18 -0
- data/test/support/integration_test.rb +6 -0
- data/test/support/mocks/bogus_event.rb +15 -0
- data/test/support/mocks/mock_connection.rb +24 -0
- data/test/support/mocks/mock_event_router.rb +12 -0
- data/test/support/mocks/mock_listener.rb +9 -0
- data/test/test_helper.rb +14 -0
- metadata +316 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
module Alondra
|
2
|
+
class Event
|
3
|
+
attr_reader :channel_name
|
4
|
+
attr_reader :type
|
5
|
+
attr_reader :resource
|
6
|
+
attr_reader :resource_type
|
7
|
+
attr_reader :connection
|
8
|
+
|
9
|
+
def initialize(event_hash, from_json = nil, connection = nil)
|
10
|
+
@connection = connection
|
11
|
+
@type = event_hash[:event].to_sym
|
12
|
+
@json_encoded = from_json
|
13
|
+
|
14
|
+
if Hash === event_hash[:resource]
|
15
|
+
@resource = fetch(event_hash[:resource_type], event_hash[:resource])
|
16
|
+
else
|
17
|
+
@resource = event_hash[:resource]
|
18
|
+
end
|
19
|
+
|
20
|
+
@resource_type = event_hash[:resource_type] || resource.class.name
|
21
|
+
|
22
|
+
if event_hash[:channel].present?
|
23
|
+
@channel_name = event_hash[:channel]
|
24
|
+
else
|
25
|
+
channel_type = type == :updated ? :member : :collection
|
26
|
+
Channel.default_name_for(resource, channel_type)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def channel
|
31
|
+
@channel ||= Channel[channel_name]
|
32
|
+
end
|
33
|
+
|
34
|
+
def fire!
|
35
|
+
if connection
|
36
|
+
# We are inside the Alondra Server
|
37
|
+
EM.schedule do
|
38
|
+
MessageQueue.instance.receive self
|
39
|
+
end
|
40
|
+
else
|
41
|
+
MessageQueueClient.push self
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def as_json
|
46
|
+
{
|
47
|
+
:event => type,
|
48
|
+
:resource_type => resource_type,
|
49
|
+
:resource => resource.as_json,
|
50
|
+
:channel => channel_name
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_json
|
55
|
+
@json_encoded ||= ActiveSupport::JSON.encode(as_json)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def fetch(resource_type_name, attributes)
|
61
|
+
attributes.symbolize_keys!
|
62
|
+
resource_class = Kernel.const_get(resource_type_name)
|
63
|
+
|
64
|
+
return attributes unless resource_class < ActiveRecord::Base
|
65
|
+
|
66
|
+
resource = resource_class.new
|
67
|
+
|
68
|
+
filtered_attributes = attributes.delete_if { |k,v| !resource.has_attribute?(k) }
|
69
|
+
|
70
|
+
resource.assign_attributes(filtered_attributes, :without_protection => true)
|
71
|
+
resource
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Alondra
|
2
|
+
class EventListener
|
3
|
+
include Pushing
|
4
|
+
|
5
|
+
attr_accessor :event
|
6
|
+
attr_accessor :resource
|
7
|
+
attr_accessor :channel_name
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def listened_patterns
|
11
|
+
@listened_patterns ||= [default_listened_pattern]
|
12
|
+
end
|
13
|
+
|
14
|
+
def listen_to?(channel_name)
|
15
|
+
listened_patterns.any? { |p| p =~ channel_name }
|
16
|
+
end
|
17
|
+
|
18
|
+
def listen_to(channel_name)
|
19
|
+
unless @custom_pattern_provided
|
20
|
+
listened_patterns.clear
|
21
|
+
@custom_pattern_provided = true
|
22
|
+
end
|
23
|
+
|
24
|
+
if Regexp === channel_name
|
25
|
+
listened_patterns << channel_name
|
26
|
+
else
|
27
|
+
escaped_pattern = Regexp.escape(channel_name)
|
28
|
+
listened_patterns << Regexp.new("^#{escaped_pattern}")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def on(event_type, options = {}, &block)
|
33
|
+
callbacks << ListenerCallback.new(event_type, options, block)
|
34
|
+
end
|
35
|
+
|
36
|
+
def callbacks
|
37
|
+
@callbacks ||= []
|
38
|
+
end
|
39
|
+
|
40
|
+
def default_listened_pattern
|
41
|
+
word = self.name.demodulize
|
42
|
+
word.gsub!(/Listener$/, '')
|
43
|
+
word.gsub!(/::/, '/')
|
44
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1\/\2')
|
45
|
+
word.gsub!(/([a-z\d])([A-Z])/,'\1/\2')
|
46
|
+
word.downcase!
|
47
|
+
Regexp.new("^/#{word}")
|
48
|
+
end
|
49
|
+
|
50
|
+
def inherited(subclass)
|
51
|
+
# In development mode Rails will load the same class many times
|
52
|
+
# Delete it first if we already have parsed it
|
53
|
+
EventRouter.listeners.delete_if { |l| l.name == subclass.name }
|
54
|
+
EventRouter.listeners << subclass
|
55
|
+
end
|
56
|
+
|
57
|
+
def matching_callbacks_for(event)
|
58
|
+
callbacks.find_all { |c| c.matches?(event) }
|
59
|
+
end
|
60
|
+
|
61
|
+
def process(event)
|
62
|
+
matching_callbacks_for(event).each do |callback|
|
63
|
+
new_instance = new(event)
|
64
|
+
begin
|
65
|
+
new_instance.instance_exec(event, &callback.proc)
|
66
|
+
rescue Exception => ex
|
67
|
+
Rails.logger.error 'Error while processing event listener callback'
|
68
|
+
Rails.logger.error ex.message
|
69
|
+
Rails.logger.error ex.stacktrace if ex.respond_to? :stacktrace
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def session
|
76
|
+
@connection.session
|
77
|
+
end
|
78
|
+
|
79
|
+
def initialize(event)
|
80
|
+
@event = event
|
81
|
+
@resource = event.resource
|
82
|
+
@channel_name = event.channel_name
|
83
|
+
@connection = event.connection
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Alondra
|
2
|
+
class EventRouter
|
3
|
+
|
4
|
+
def self.listeners
|
5
|
+
@listeners ||= []
|
6
|
+
end
|
7
|
+
|
8
|
+
def process(event)
|
9
|
+
event.channel.receive(event)
|
10
|
+
|
11
|
+
# Event listeners callback can manipulate AR objects and so can potentially
|
12
|
+
# block the EM reactor thread. To avoid that, we defer them to another thread.
|
13
|
+
EM.defer do
|
14
|
+
|
15
|
+
# Ensure the connection associated with the thread is checked in
|
16
|
+
# after the callbacks are processed
|
17
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
18
|
+
listening_classes = EventRouter.listeners.select do |ob|
|
19
|
+
ob.listen_to?(event.channel_name)
|
20
|
+
end
|
21
|
+
|
22
|
+
listening_classes.each { |listening_class| listening_class.process(event) }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Alondra
|
2
|
+
class ListenerCallback
|
3
|
+
attr_reader :event_type
|
4
|
+
attr_reader :options
|
5
|
+
attr_reader :proc
|
6
|
+
|
7
|
+
CHANNEL_NAME_PATTERN = %r{\d+$}
|
8
|
+
|
9
|
+
def initialize(event_type, options = {}, proc)
|
10
|
+
@event_type = event_type
|
11
|
+
@options = options
|
12
|
+
@proc = proc
|
13
|
+
end
|
14
|
+
|
15
|
+
def matches?(event)
|
16
|
+
return false unless event.type == event_type
|
17
|
+
|
18
|
+
case options[:to]
|
19
|
+
when nil then true
|
20
|
+
when :member then
|
21
|
+
member_channel? event.channel_name
|
22
|
+
when :collection then
|
23
|
+
!member_channel?(event.channel_name)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_proc
|
28
|
+
proc
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def member_channel?(channel_name)
|
34
|
+
channel_name =~ CHANNEL_NAME_PATTERN
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Alondra
|
2
|
+
class Message
|
3
|
+
attr_reader :content
|
4
|
+
attr_reader :channel_names
|
5
|
+
|
6
|
+
def initialize(content, channel_names)
|
7
|
+
@content = content
|
8
|
+
@channel_names = channel_names
|
9
|
+
end
|
10
|
+
|
11
|
+
def enqueue
|
12
|
+
MessageQueueClient.push self
|
13
|
+
end
|
14
|
+
|
15
|
+
def send_to_channels
|
16
|
+
channels.each do |channel|
|
17
|
+
channel.receive self
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def as_json
|
22
|
+
{:message => content, :channel_names => channel_names}
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_json
|
26
|
+
ActiveSupport::JSON.encode(as_json)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def channels
|
32
|
+
channel_names.collect { |name| Channel[name] }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'ffi'
|
3
|
+
require 'em-zeromq'
|
4
|
+
|
5
|
+
module Alondra
|
6
|
+
class MessageQueue
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
def start_listening
|
10
|
+
Rails.logger.info "Starting message queue"
|
11
|
+
|
12
|
+
if @connection
|
13
|
+
Rails.logger.warn 'Push connection to message queue started twice'
|
14
|
+
reset!
|
15
|
+
end
|
16
|
+
|
17
|
+
@connection = context.bind(ZMQ::SUB, Alondra.config.queue_socket, self)
|
18
|
+
@connection.setsockopt ZMQ::SUBSCRIBE, '' # receive all
|
19
|
+
end
|
20
|
+
|
21
|
+
def on_readable(socket, messages)
|
22
|
+
messages.each do |received|
|
23
|
+
begin
|
24
|
+
parse received.copy_out_string
|
25
|
+
rescue Exception => ex
|
26
|
+
Rails.logger.error "Error raised while processing message"
|
27
|
+
Rails.logger.error "#{ex.class}: #{ex.message}"
|
28
|
+
Rails.logger.error ex.backtrace.join("\n") if ex.respond_to? :backtrace
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse(received_string)
|
34
|
+
received_hash = ActiveSupport::JSON.decode(received_string).symbolize_keys
|
35
|
+
|
36
|
+
if received_hash[:event]
|
37
|
+
event = Event.new(received_hash, received_string)
|
38
|
+
receive(event)
|
39
|
+
elsif received_hash[:message]
|
40
|
+
message = Message.new(received_hash[:message], received_hash[:channel_names])
|
41
|
+
message.send_to_channels
|
42
|
+
else
|
43
|
+
Rails.logger.warn "Unrecognized message type #{received_string}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def receive(event)
|
48
|
+
event_router.process(event)
|
49
|
+
end
|
50
|
+
|
51
|
+
def reset!
|
52
|
+
@connection.close_connection()
|
53
|
+
|
54
|
+
@connection = nil
|
55
|
+
@context = nil
|
56
|
+
@push_socket = nil
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def event_router
|
62
|
+
@event_router ||= EventRouter.new
|
63
|
+
end
|
64
|
+
|
65
|
+
def context
|
66
|
+
@context ||= EM::ZeroMQ::Context.new(1)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'ffi-rzmq'
|
3
|
+
require 'em-zeromq'
|
4
|
+
|
5
|
+
module Alondra
|
6
|
+
class MessageQueueClient
|
7
|
+
|
8
|
+
def self.push(message)
|
9
|
+
instance.send_message(message)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.instance
|
13
|
+
if EM.reactor_running?
|
14
|
+
async_instance
|
15
|
+
else
|
16
|
+
sync_instance
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.async_instance
|
21
|
+
@async_instance ||= AsyncMessageQueueClient.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.sync_instance
|
25
|
+
@sync_instance ||= SyncMessageQueueClient.new
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class AsyncMessageQueueClient < MessageQueueClient
|
30
|
+
def send_message(message)
|
31
|
+
EM.schedule do
|
32
|
+
begin
|
33
|
+
push_socket.send_msg(message.to_json)
|
34
|
+
rescue Exception => ex
|
35
|
+
Rails.logger.error "Exception while sending message to message queue: #{ex.message}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def push_socket
|
41
|
+
@push_socket ||= context.connect(ZMQ::PUB, Alondra.config.queue_socket)
|
42
|
+
end
|
43
|
+
|
44
|
+
def context
|
45
|
+
@context ||= EM::ZeroMQ::Context.new(1)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class SyncMessageQueueClient < MessageQueueClient
|
50
|
+
|
51
|
+
def send_message(message)
|
52
|
+
begin
|
53
|
+
push_socket.send_string(message.to_json)
|
54
|
+
rescue Exception => ex
|
55
|
+
Rails.logger.error "Exception while sending message to message queue: #{ex.message}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def push_socket
|
60
|
+
@push_socket ||= begin
|
61
|
+
socket = context.socket(ZMQ::PUB)
|
62
|
+
socket.connect(Alondra.config.queue_socket)
|
63
|
+
socket
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def context
|
68
|
+
@context ||= ZMQ::Context.new(1)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Alondra
|
2
|
+
class PushController
|
3
|
+
include ActiveSupport::Configurable
|
4
|
+
include AbstractController::Logger
|
5
|
+
include AbstractController::Rendering
|
6
|
+
include AbstractController::Helpers
|
7
|
+
include AbstractController::Translation
|
8
|
+
include AbstractController::AssetPaths
|
9
|
+
include AbstractController::ViewPaths
|
10
|
+
|
11
|
+
attr_accessor :channel_names
|
12
|
+
|
13
|
+
def initialize(context, to)
|
14
|
+
@channel_names = Channel.names_for(to)
|
15
|
+
|
16
|
+
self.class.view_paths = ActionController::Base.view_paths
|
17
|
+
copy_instance_variables_from(context)
|
18
|
+
end
|
19
|
+
|
20
|
+
def render_push(options)
|
21
|
+
|
22
|
+
if EM.reactor_thread?
|
23
|
+
Rails.logger.warn('Your are rendering a view from the Event Machine reactor thread')
|
24
|
+
Rails.logger.warn('Rendering a view is a possibly blocking operation, so be careful')
|
25
|
+
end
|
26
|
+
|
27
|
+
message_content = render_to_string(*options)
|
28
|
+
msg = Message.new(message_content, channel_names)
|
29
|
+
msg.enqueue
|
30
|
+
end
|
31
|
+
|
32
|
+
def _prefixes
|
33
|
+
['application']
|
34
|
+
end
|
35
|
+
|
36
|
+
def view_paths
|
37
|
+
@view_paths ||= ApplicationController.send '_view_paths'
|
38
|
+
end
|
39
|
+
|
40
|
+
def action_name
|
41
|
+
'push'
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def copy_instance_variables_from(context)
|
47
|
+
context.instance_variables.each do |var|
|
48
|
+
value = context.instance_variable_get(var)
|
49
|
+
instance_variable_set(var, value)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Alondra
|
2
|
+
|
3
|
+
class PushingException < StandardError; end
|
4
|
+
|
5
|
+
module Pushing
|
6
|
+
def push(*args)
|
7
|
+
raise PushingException.new('You need to specify the channel to push') unless args.last[:to].present?
|
8
|
+
|
9
|
+
to = args.last.delete(:to)
|
10
|
+
controller = PushController.new(self, to)
|
11
|
+
controller.render_push(args)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'em-websocket'
|
2
|
+
|
3
|
+
module Alondra
|
4
|
+
module Server
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def run
|
8
|
+
Rails.logger.info "Server starting on port #{Alondra.config.port}"
|
9
|
+
|
10
|
+
EM::WebSocket.start(:host => '0.0.0.0', :port => Alondra.config.port) do |websocket|
|
11
|
+
|
12
|
+
websocket.onopen do
|
13
|
+
session = SessionParser.parse(websocket)
|
14
|
+
|
15
|
+
Rails.logger.info "client connected."
|
16
|
+
Connection.new(websocket, session)
|
17
|
+
end
|
18
|
+
|
19
|
+
websocket.onclose do
|
20
|
+
Rails.logger.info "Connection closed"
|
21
|
+
Connections[websocket].destroy! if Connections[websocket].present?
|
22
|
+
end
|
23
|
+
|
24
|
+
websocket.onerror do |ex|
|
25
|
+
puts "Error: #{ex.message}"
|
26
|
+
Rails.logger.error "Error: #{ex.message}"
|
27
|
+
Rails.logger.error ex.backtrace.join("\n")
|
28
|
+
Connections[websocket].destroy! if Connections[websocket]
|
29
|
+
end
|
30
|
+
|
31
|
+
websocket.onmessage do |msg|
|
32
|
+
Rails.logger.info "received: #{msg}"
|
33
|
+
CommandDispatcher.dispatch(msg, Connections[websocket])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
EM.error_handler do |error|
|
38
|
+
puts "Error raised during event loop: #{error.message}"
|
39
|
+
Rails.logger.error "Error raised during event loop: #{error.message}"
|
40
|
+
Rails.logger.error error.stacktrace if error.respond_to? :stacktrace
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module Alondra
|
4
|
+
module SessionParser
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def verifier
|
8
|
+
@verifier ||= ActiveSupport::MessageVerifier.new(Rails.application.config.secret_token)
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse(websocket)
|
12
|
+
cookie = websocket.request['cookie'] || websocket.request['Cookie']
|
13
|
+
token = websocket.request['query']['token']
|
14
|
+
|
15
|
+
if token.present?
|
16
|
+
SessionParser.parse_token(token)
|
17
|
+
elsif cookie.present?
|
18
|
+
SessionParser.parse_cookie(cookie)
|
19
|
+
else
|
20
|
+
Hash.new
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse_cookie(cookie)
|
25
|
+
begin
|
26
|
+
cookies = cookie.split(';')
|
27
|
+
session_key = Rails.application.config.session_options[:key]
|
28
|
+
|
29
|
+
encoded_session = cookies.detect{|c| c.include?(session_key)}.gsub("#{session_key}=",'').strip
|
30
|
+
verifier.verify(CGI.unescape(encoded_session))
|
31
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature => ex
|
32
|
+
Rails.logger.error "invalid session cookie: #{cookie}"
|
33
|
+
Hash.new
|
34
|
+
rescue Exception => ex
|
35
|
+
Rails.logger.error "Exception parsing session from cookie: #{ex.message}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def parse_token(token)
|
40
|
+
begin
|
41
|
+
decoded_token = verifier.verify(token)
|
42
|
+
ActiveSupport::JSON.decode(decoded_token)
|
43
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature => ex
|
44
|
+
Rails.logger.error "invalid session token: #{token}"
|
45
|
+
Hash.new
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def session_key
|
50
|
+
Rails.application.config.session_options.key
|
51
|
+
end
|
52
|
+
|
53
|
+
def marshall
|
54
|
+
Rails.application.config.session_options[:coder]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/alondra.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require_relative 'alondra/message'
|
2
|
+
require_relative 'alondra/event'
|
3
|
+
require_relative 'alondra/connection'
|
4
|
+
require_relative 'alondra/channel'
|
5
|
+
require_relative 'alondra/command'
|
6
|
+
require_relative 'alondra/command_dispatcher'
|
7
|
+
require_relative 'alondra/event_router'
|
8
|
+
require_relative 'alondra/message_queue_client'
|
9
|
+
require_relative 'alondra/message_queue'
|
10
|
+
require_relative 'alondra/pushing'
|
11
|
+
require_relative 'alondra/event_listener'
|
12
|
+
require_relative 'alondra/session_parser'
|
13
|
+
require_relative 'alondra/listener_callback'
|
14
|
+
require_relative 'alondra/push_controller'
|
15
|
+
require_relative 'alondra/changes_callbacks'
|
16
|
+
require_relative 'alondra/changes_push'
|
17
|
+
require_relative 'alondra/server'
|
18
|
+
|
19
|
+
module Alondra
|
20
|
+
|
21
|
+
ActiveRecord::Base.extend ChangesPush
|
22
|
+
ActionController::Base.send :include, Pushing
|
23
|
+
|
24
|
+
class Alondra < Rails::Engine
|
25
|
+
|
26
|
+
# Setting default configuration values
|
27
|
+
config.port = Rails.env == 'test' ? 12346 : 12345
|
28
|
+
config.host = 'localhost'
|
29
|
+
config.queue_socket = 'ipc:///tmp/alondra.ipc'
|
30
|
+
|
31
|
+
initializer "enable sessions for flash websockets" do
|
32
|
+
Rails.application.config.session_store :cookie_store, httponly: false
|
33
|
+
end
|
34
|
+
|
35
|
+
initializer "load listeners" do
|
36
|
+
listeners_dir = File.join(Rails.root, 'app', 'listeners')
|
37
|
+
|
38
|
+
Rails.logger.info "Loading event listeners in #{listeners_dir}"
|
39
|
+
Dir[File.join(listeners_dir, '*.rb')].each { |file| require_dependency file }
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.start_server_in_new_thread!
|
43
|
+
Thread.new do
|
44
|
+
start_server!
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.start_server!
|
49
|
+
if EM.reactor_running?
|
50
|
+
EM.schedule do
|
51
|
+
MessageQueue.instance.start_listening
|
52
|
+
Server.run
|
53
|
+
end
|
54
|
+
else
|
55
|
+
Rails.logger.info "starting EM reactor"
|
56
|
+
EM.run do
|
57
|
+
MessageQueue.instance.start_listening
|
58
|
+
Server.run
|
59
|
+
end
|
60
|
+
die_gracefully_on_signal
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.die_gracefully_on_signal
|
65
|
+
Signal.trap("INT") do
|
66
|
+
Rails.logger.warn "INT signal trapped. Shutting down EM reactor"
|
67
|
+
EM.stop
|
68
|
+
end
|
69
|
+
|
70
|
+
Signal.trap("TERM") do
|
71
|
+
Rails.logger.warn "TERM signal trapped. Shutting down EM reactor"
|
72
|
+
EM.stop
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
|
80
|
+
|