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.
@@ -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
@@ -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)
@@ -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
@@ -0,0 +1,4 @@
1
+ module Atr
2
+ class AtrError < StandardError; end
3
+ class MustImplementMethodError < AtrError; end
4
+ end
@@ -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
@@ -0,0 +1,11 @@
1
+ module Atr
2
+ class Publisher
3
+ include ::Celluloid
4
+
5
+ def publish_event(event)
6
+ ::ActiveRecord::Base.connection_pool.with_connection do
7
+ ::Atr::Redis.connection.publish(event["routing_key"], Marshal.dump(event))
8
+ end
9
+ end
10
+ end
11
+ end
@@ -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
@@ -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
@@ -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
@@ -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