atr 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +347 -0
- data/Rakefile +2 -0
- data/atr.gemspec +36 -0
- data/bin/atr_server +24 -0
- data/lib/atr.rb +43 -0
- data/lib/atr/config.rb +25 -0
- data/lib/atr/errors.rb +4 -0
- data/lib/atr/event.rb +24 -0
- data/lib/atr/publishable.rb +95 -0
- data/lib/atr/publisher.rb +11 -0
- data/lib/atr/railtie.rb +29 -0
- data/lib/atr/reactor.rb +119 -0
- data/lib/atr/redis.rb +18 -0
- data/lib/atr/registry.rb +15 -0
- data/lib/atr/request_authenticator.rb +17 -0
- data/lib/atr/request_scope.rb +17 -0
- data/lib/atr/server.rb +49 -0
- data/lib/atr/version.rb +3 -0
- data/spec/atr/event_spec.rb +23 -0
- data/spec/atr/publishable_spec.rb +62 -0
- data/spec/atr/publisher_spec.rb +43 -0
- data/spec/atr/redis_spec.rb +13 -0
- data/spec/atr/registry_spec.rb +27 -0
- data/spec/atr/request_authenticator_spec.rb +20 -0
- data/spec/atr/request_scope_spec.rb +21 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/db/setup.rb +21 -0
- data/spec/support/models.rb +1 -0
- data/spec/support/models/post.rb +8 -0
- data/spec/test.db +0 -0
- metadata +287 -0
data/bin/atr_server
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
require 'atr'
|
5
|
+
require './config/environment.rb'
|
6
|
+
|
7
|
+
class AtrServer < ::Thor
|
8
|
+
class_option :server_host, :default => "127.0.0.1"
|
9
|
+
class_option :server_port, :default => 7777
|
10
|
+
|
11
|
+
desc "start", "Start ATR"
|
12
|
+
def start
|
13
|
+
puts "Starting ATR SERVER"
|
14
|
+
::Dir.glob(::Rails.root.join('app', 'models', "**", "*.rb")).each{ |file| load file }
|
15
|
+
|
16
|
+
::Atr::Server.supervise_as :websocket_server
|
17
|
+
|
18
|
+
::ActiveRecord::Base.clear_active_connections!
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
::AtrServer.start
|
23
|
+
|
24
|
+
sleep
|
data/lib/atr.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require "atr/version"
|
2
|
+
|
3
|
+
require "celluloid"
|
4
|
+
require "reel"
|
5
|
+
require "active_support"
|
6
|
+
require 'active_support/concern'
|
7
|
+
require "active_attr"
|
8
|
+
|
9
|
+
require "atr/config"
|
10
|
+
require "atr/event"
|
11
|
+
require "atr/reactor"
|
12
|
+
require "atr/server"
|
13
|
+
require "atr/redis"
|
14
|
+
require "atr/request_authenticator"
|
15
|
+
require "atr/request_scope"
|
16
|
+
require "atr/publishable"
|
17
|
+
require "atr/publisher"
|
18
|
+
require "atr/registry"
|
19
|
+
|
20
|
+
module Atr
|
21
|
+
class << self
|
22
|
+
attr_accessor :configuration
|
23
|
+
alias_method :config, :configuration
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.publish_event(event)
|
27
|
+
::Celluloid::Actor[:atr_publisher].publish_event(event)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.channels
|
31
|
+
::Atr::Registry.channels
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.configure
|
35
|
+
self.configuration ||= ::Atr::Config.new
|
36
|
+
|
37
|
+
yield(configuration)
|
38
|
+
|
39
|
+
::ActiveSupport.run_load_hooks(:atr, self)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
require 'atr/railtie' if defined?(Rails)
|
data/lib/atr/config.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'active_support/ordered_options'
|
2
|
+
|
3
|
+
module Atr
|
4
|
+
class Config < ::ActiveSupport::OrderedOptions
|
5
|
+
def initialize(options = {})
|
6
|
+
super
|
7
|
+
end
|
8
|
+
|
9
|
+
def authenticate?
|
10
|
+
has_key?(:authenticate_with)
|
11
|
+
end
|
12
|
+
|
13
|
+
def scope?
|
14
|
+
has_key?(:scope_with)
|
15
|
+
end
|
16
|
+
|
17
|
+
def event_serializer?
|
18
|
+
has_key?(:event_serializer)
|
19
|
+
end
|
20
|
+
|
21
|
+
def serialize_events_with?
|
22
|
+
has_key?(:serialize_events_with)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/atr/errors.rb
ADDED
data/lib/atr/event.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
module Atr
|
3
|
+
class Event
|
4
|
+
include ::ActiveAttr::Model
|
5
|
+
include ::ActiveModel::AttributeMethods
|
6
|
+
|
7
|
+
attribute :id
|
8
|
+
attribute :name
|
9
|
+
attribute :occured_at
|
10
|
+
attribute :record
|
11
|
+
attribute :record_type
|
12
|
+
attribute :routing_key
|
13
|
+
|
14
|
+
def initialize(routing_key, name, record)
|
15
|
+
self[:routing_key] = routing_key
|
16
|
+
self[:name] = name
|
17
|
+
self[:record] = record
|
18
|
+
self[:id] = ::SecureRandom.hex
|
19
|
+
self[:record_type] = record.class.name
|
20
|
+
self[:occured_at] ||= ::DateTime.now
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Atr
|
2
|
+
module Publishable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
PUBLISHABLE_ACTIONS = ["updated", "created", "destroyed"]
|
6
|
+
|
7
|
+
included do
|
8
|
+
include ::ActiveModel::Dirty
|
9
|
+
|
10
|
+
after_create :publish_created_event
|
11
|
+
after_update :publish_updated_event
|
12
|
+
after_destroy :publish_destroyed_event
|
13
|
+
|
14
|
+
class << self
|
15
|
+
attr_accessor :publication_scopes
|
16
|
+
end
|
17
|
+
|
18
|
+
self.publication_scopes ||= []
|
19
|
+
self.publishable_actions ||= []
|
20
|
+
|
21
|
+
::Atr::Publishable::PUBLISHABLE_ACTIONS.each do |action|
|
22
|
+
::Atr::Registry.channels << "#{routing_key}.#{action}" unless ::Atr::Registry.channels.include?("#{routing_key}.#{action}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def publish_updated_event
|
29
|
+
routing_key = self.class.build_routing_key_for_record_action(self, "updated")
|
30
|
+
event_name = self.class.resource_action_routing_key("updated")
|
31
|
+
record_updated_event = ::Atr::Event.new(routing_key, event_name, self)
|
32
|
+
|
33
|
+
::Atr.publish_event(record_updated_event)
|
34
|
+
end
|
35
|
+
|
36
|
+
def publish_created_event
|
37
|
+
routing_key = self.class.build_routing_key_for_record_action(self, "created")
|
38
|
+
event_name = self.class.resource_action_routing_key("created")
|
39
|
+
record_created_event = ::Atr::Event.new(routing_key, event_name, self)
|
40
|
+
|
41
|
+
::Atr.publish_event(record_created_event)
|
42
|
+
end
|
43
|
+
|
44
|
+
def publish_destroyed_event
|
45
|
+
routing_key = self.class.build_routing_key_for_record_action(self, "destroyed")
|
46
|
+
event_name = self.class.resource_action_routing_key("destroyed")
|
47
|
+
record_destroyed_event = ::Atr::Event.new(routing_key, event_name, self)
|
48
|
+
|
49
|
+
::Atr.publish_event(record_destroyed_event)
|
50
|
+
end
|
51
|
+
|
52
|
+
module ClassMethods
|
53
|
+
def publishable_actions(*actions)
|
54
|
+
@publishable_actions = actions
|
55
|
+
end
|
56
|
+
|
57
|
+
def routing_key
|
58
|
+
resource_routing_keys.join(".")
|
59
|
+
end
|
60
|
+
|
61
|
+
def resource_routing_keys
|
62
|
+
name.split("::").map(&:underscore)
|
63
|
+
end
|
64
|
+
|
65
|
+
def resource_action_routing_keys(action_routing_key)
|
66
|
+
[resource_routing_keys, action_routing_key]
|
67
|
+
end
|
68
|
+
|
69
|
+
def resource_action_routing_key(action_routing_key)
|
70
|
+
resource_action_routing_keys(action_routing_key).join(".")
|
71
|
+
end
|
72
|
+
|
73
|
+
def build_routing_key_for_record_action(record, action_routing_key)
|
74
|
+
publication_scope_routing_keys = build_publication_scope_for_record(record)
|
75
|
+
[publication_scope_routing_keys, resource_routing_keys, action_routing_key].flatten.join(".")
|
76
|
+
end
|
77
|
+
|
78
|
+
def build_publication_scope_for_record(record)
|
79
|
+
publication_scopes.map do |arg|
|
80
|
+
key = arg.to_s.split("_id").first
|
81
|
+
value = record.__send__(arg)
|
82
|
+
[key, value]
|
83
|
+
end.try(:flatten)
|
84
|
+
end
|
85
|
+
|
86
|
+
def publication_scope(*args)
|
87
|
+
self.publication_scopes = args
|
88
|
+
end
|
89
|
+
|
90
|
+
def scope_publication?
|
91
|
+
publication_scopes.present?
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/lib/atr/railtie.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rails/railtie'
|
2
|
+
|
3
|
+
module Atr
|
4
|
+
class Railtie < ::Rails::Railtie
|
5
|
+
config.after_initialize do
|
6
|
+
::Atr::Redis.connect unless ::Atr::Redis.connected?
|
7
|
+
|
8
|
+
::Atr::Publisher.supervise_as :atr_publisher
|
9
|
+
end
|
10
|
+
|
11
|
+
::ActiveSupport.on_load(:atr) do
|
12
|
+
puts "ATR LOADED"
|
13
|
+
end
|
14
|
+
|
15
|
+
#todo: make redis configurable
|
16
|
+
def self.load_config_yml
|
17
|
+
config_file = ::YAML.load_file(config_yml_filepath)
|
18
|
+
return unless config_file.is_a?(Hash)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.config_yml_exists?
|
22
|
+
::File.exists? config_yml_filepath
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.config_yml_filepath
|
26
|
+
::Rails.root.join('config', 'atr.yml')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/atr/reactor.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'reel'
|
2
|
+
|
3
|
+
module Atr
|
4
|
+
class Reactor
|
5
|
+
include Celluloid
|
6
|
+
include Celluloid::IO
|
7
|
+
include Celluloid::Logger
|
8
|
+
|
9
|
+
attr_accessor :websocket
|
10
|
+
attr_accessor :routing_key_scope
|
11
|
+
attr_accessor :subscribers
|
12
|
+
|
13
|
+
def initialize(websocket, routing_key_scope = nil)
|
14
|
+
info "Streaming changes"
|
15
|
+
|
16
|
+
@routing_key_scope = routing_key_scope
|
17
|
+
@websocket = websocket
|
18
|
+
|
19
|
+
@subscribers = ::Atr::Registry.scoped_channels(routing_key_scope).map do |channel|
|
20
|
+
async.start_subscriber(channel)
|
21
|
+
end
|
22
|
+
|
23
|
+
async.run
|
24
|
+
end
|
25
|
+
|
26
|
+
def dispatch_message(message)
|
27
|
+
puts message.inspect
|
28
|
+
end
|
29
|
+
|
30
|
+
def run
|
31
|
+
while message = @websocket.read
|
32
|
+
if message == "unsubscribe"
|
33
|
+
unsubscribe_all
|
34
|
+
else
|
35
|
+
dispatch_message(message)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
#todo: decide between starting individually or subscribing all at once and remove one of the methods
|
41
|
+
def start_subscribers
|
42
|
+
::Atr::Redis.connect unless ::Atr::Redis.connected?
|
43
|
+
|
44
|
+
::Atr::Redis.connection.subscribe(::Atr::Registry.scoped_channels(routing_key_scope)) do |on|
|
45
|
+
on.subscribe do |channel, subscriptions|
|
46
|
+
puts "Subscribed to ##{channel} (#{subscriptions} subscriptions)"
|
47
|
+
end
|
48
|
+
|
49
|
+
on.unsubscribe do |channel, subscriptions|
|
50
|
+
::ActiveRecord::Base.clear_active_connections!
|
51
|
+
terminate
|
52
|
+
end
|
53
|
+
|
54
|
+
on.message do |channel, message|
|
55
|
+
shutdown if message == "exit"
|
56
|
+
|
57
|
+
event = Marshal.load(message)
|
58
|
+
|
59
|
+
if ::Atr.config.event_serializer?
|
60
|
+
|
61
|
+
websocket << ::Atr.config.event_serializer.new(event).to_json
|
62
|
+
else
|
63
|
+
websocket << event.to_json
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
rescue Reel::SocketError
|
68
|
+
info "Client disconnected"
|
69
|
+
::ActiveRecord::Base.clear_active_connections!
|
70
|
+
terminate
|
71
|
+
end
|
72
|
+
|
73
|
+
def shutdown
|
74
|
+
::Atr::Redis.connection.unsubscribe
|
75
|
+
::ActiveRecord::Base.clear_active_connections!
|
76
|
+
terminate
|
77
|
+
end
|
78
|
+
|
79
|
+
def start_subscriber(channel)
|
80
|
+
::Atr::Redis.connect unless ::Atr::Redis.connected?
|
81
|
+
|
82
|
+
::Atr::Redis.connection.subscribe(channel) do |on|
|
83
|
+
on.subscribe do |channel, subscriptions|
|
84
|
+
puts "Subscribed to ##{channel} (#{subscriptions} subscriptions)"
|
85
|
+
end
|
86
|
+
|
87
|
+
on.unsubscribe do |channel, subscriptions|
|
88
|
+
puts "Unsubscribed from ##{channel} (#{subscriptions} subscriptions)"
|
89
|
+
::ActiveRecord::Base.clear_active_connections!
|
90
|
+
terminate
|
91
|
+
end
|
92
|
+
|
93
|
+
on.message do |channel, message|
|
94
|
+
shutdown if message == "exit"
|
95
|
+
|
96
|
+
event = Marshal.load(message)
|
97
|
+
|
98
|
+
if ::Atr.config.event_serializer?
|
99
|
+
puts "FOUND SERIUALIZER"
|
100
|
+
puts ::Atr.config.event_serializer.inspect
|
101
|
+
puts ::Atr.config.event_serializer.new(event).to_json
|
102
|
+
websocket << ::Atr.config.event_serializer.new(event).to_json
|
103
|
+
else
|
104
|
+
websocket << event.to_json
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def unsubscribe_all
|
111
|
+
::Atr::Registry.scoped_channels(routing_key_scope).map do |channel|
|
112
|
+
::Atr::Redis.connection.unsubscribe(channel)
|
113
|
+
end
|
114
|
+
|
115
|
+
info "clearing connections"
|
116
|
+
terminate
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
data/lib/atr/redis.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'celluloid/redis'
|
3
|
+
require 'redis/connection/celluloid'
|
4
|
+
|
5
|
+
module Atr
|
6
|
+
class Redis
|
7
|
+
class << self
|
8
|
+
@connected ||= false
|
9
|
+
attr_accessor :connected, :connection
|
10
|
+
|
11
|
+
alias_method :connected?, :connected
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.connect(options={})
|
15
|
+
@connection = ::Redis.new(:driver => :celluloid)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/atr/registry.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module Atr
|
2
|
+
class Registry
|
3
|
+
include ActiveSupport::Configurable
|
4
|
+
|
5
|
+
class << self
|
6
|
+
attr_accessor :channels
|
7
|
+
end
|
8
|
+
|
9
|
+
@channels = []
|
10
|
+
|
11
|
+
def self.scoped_channels(routing_key)
|
12
|
+
channels.map{ |channel| "#{routing_key}.#{channel}" }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Atr
|
2
|
+
class RequestAuthenticator
|
3
|
+
attr_accessor :request
|
4
|
+
|
5
|
+
def initialize(request)
|
6
|
+
@request = request
|
7
|
+
end
|
8
|
+
|
9
|
+
def matches?
|
10
|
+
false
|
11
|
+
end
|
12
|
+
|
13
|
+
def params
|
14
|
+
::Hash[request.query_string.split("&").map{|seg| seg.split("=") }].with_indifferent_access
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Atr
|
2
|
+
class RequestScope
|
3
|
+
attr_accessor :request
|
4
|
+
|
5
|
+
def initialize(request)
|
6
|
+
@request = request
|
7
|
+
end
|
8
|
+
|
9
|
+
def matches?
|
10
|
+
false
|
11
|
+
end
|
12
|
+
|
13
|
+
def params
|
14
|
+
::Hash[request.query_string.split("&").map{|seg| seg.split("=") }].with_indifferent_access
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|