alondra 0.0.3
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 +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
|
+
|