atr 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.
- 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
|