atr 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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