action_pubsub 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d4a0328f1cb7e3a3a0155c4b0957cd3dc566eaed
4
+ data.tar.gz: be1336cd5d743520ff2c97f6a6b3034357d197f3
5
+ SHA512:
6
+ metadata.gz: ff793a319ee02b8d4cb58d14a0ce0cf3b6146e1211e6d3d5f20e73c559ff5b46174cc062dd4b49c7e0835072b23bcb13fa71014e076af0d22c254da5776ca6e9
7
+ data.tar.gz: b89adff807fb7cb898ad689df79d6dac365623f31f7e838330e56ea617175e2147e0d7117e75a72b75ff8048f8ec5d11bace7a537156379317b7013b4d5afbf7
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.0
4
+ before_install: gem install bundler -v 1.10.3
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # gem "concurrent-ruby", :path => "~/gems/ruby-concurrency"
4
+ # Specify your gem's dependencies in action_pubsub.gemspec
5
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Jason Ayre
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # ActionPubsub
2
+
3
+ In process, concurrent observers, loosely modeled after rabbitmq
4
+
5
+ ## Example
6
+
7
+ Lets say we have a blog app, and our posts have comments enabled on a case by case basis.
8
+ When someone leaves a new comment, we want to blast out an email to everyone who
9
+ has subscribed to recieve new posts
10
+
11
+ In our comments model:
12
+
13
+ ``` ruby
14
+ module Blogger
15
+ class Comment < ::ActiveRecord::Base
16
+ include ::ActionPubsub::ActiveRecord::Publishable
17
+ publish_as "blogger/comment"
18
+ publishable_actions :created, :updated
19
+ end
20
+ end
21
+ ```
22
+
23
+ Our subscriber:
24
+ ``` ruby
25
+ module Blogger
26
+ class CommentSubscriber < ::ActionPubsub::ActiveRecord::Subscriber
27
+ #this is the "exchange" we want all of the events we are watching for, to be scoped to
28
+ #i.e. blogger/comment/created will end up being the fully qualified path for on :create
29
+ subscribe_to "blogger/comment"
30
+
31
+ self.concurrency = 5
32
+
33
+ on :created, :if => lambda{ |record| record.post.has_comments_enabled? } do
34
+ #on initialize right now subscriber instance will get a resource instance variable
35
+ #populated for free pertaining to the record in focus, i.e. a comment record
36
+ resource.post.commenters.by_new_comment_notifications_enabled.each do |commenter|
37
+ NewCommentNotificationMailer.deliver(resource, commenter)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ ```
43
+
44
+ ### What is the advantage of this pattern?
45
+
46
+ The advantage is it makes your app incredibly reactive. Rather than have to fatten
47
+ up your controller with logic that does not belong, or have some service object that
48
+ does 20 things in sequence, it allows everything to be decoupled, only subscribe to
49
+ the things that are relevant. It also enforces the single responsibility principle, by
50
+ allowing these subscribers to exist in potentially different engines, or areas of your
51
+ application, and do nothing but react to the events occurring within the system.
52
+
53
+ ### Callbacks
54
+
55
+ Sure, we could use callbacks, but do we care about any of that if the record has
56
+ not been commited to the database? (No we should not). Unless you use
57
+ after_commit :on => :create, then your callbacks will attempt to run even if record hasn't been committed,
58
+ unless at some point an error or false was returned.
59
+
60
+ So as a best practice, we only broadcast the creation after it's been commited.
61
+
62
+ This also allows for complex chains of events to occur, with no knowledge of each other,
63
+ but that do their one job, and do it well. If that subscriber happens to create a new record,
64
+ We can then subscribe to that models creation somewhere else, settings up the building blocks of a pipeline.
65
+
66
+ ## Installation
67
+
68
+ Add this line to your application's Gemfile:
69
+
70
+ ```ruby
71
+ gem 'action_pubsub'
72
+ ```
73
+
74
+ And then execute:
75
+
76
+ $ bundle
77
+
78
+ Or install it yourself as:
79
+
80
+ $ gem install action_pubsub
81
+
82
+ ## Usage
83
+
84
+ TODO: Work in progress
85
+
86
+ ## Development
87
+
88
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
89
+
90
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
91
+
92
+ ## Contributing
93
+
94
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/action_pubsub.
95
+
96
+
97
+ ## License
98
+
99
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,42 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'action_pubsub/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "action_pubsub"
8
+ spec.version = ActionPubsub::VERSION
9
+ spec.authors = ["Jason Ayre"]
10
+ spec.email = ["jasonayre@gmail.com"]
11
+
12
+ spec.summary = %q{In process, async, concurrent pubsub}
13
+ spec.description = %q{In process, async, concurrent pubsub}
14
+ spec.homepage = "http://github.com/jasonayre/action_pubsub"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
18
+ # delete this section to allow pushing this gem to any host.
19
+
20
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ spec.bindir = "exe"
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_dependency "active_attr"
26
+ spec.add_dependency "activesupport"
27
+ spec.add_dependency "concurrent-ruby"
28
+ spec.add_development_dependency "bundler", "~> 1.10"
29
+ spec.add_development_dependency 'guard', '~> 2'
30
+ spec.add_development_dependency 'guard-rspec', '~> 4'
31
+ spec.add_development_dependency 'guard-bundler', '~> 2'
32
+ spec.add_development_dependency "rake", "~> 10.0"
33
+ spec.add_development_dependency "rspec"
34
+ spec.add_development_dependency "rspec-pride"
35
+ spec.add_development_dependency 'rspec-its', '~> 1'
36
+ spec.add_development_dependency 'rspec-collection_matchers', '~> 1'
37
+ spec.add_development_dependency 'rb-fsevent'
38
+ spec.add_development_dependency "simplecov"
39
+ spec.add_development_dependency 'terminal-notifier-guard'
40
+ spec.add_development_dependency "pry"
41
+ spec.add_development_dependency "pry-nav"
42
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "action_pubsub"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ require "pry"
11
+ Pry.start
12
+
13
+ # require "irb"
14
+ # IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,81 @@
1
+ require "action_pubsub/version"
2
+ require "active_support/all"
3
+ require "concurrent"
4
+ require "active_attr"
5
+ require "concurrent/lazy_register"
6
+ require "concurrent/actor"
7
+ require "concurrent/agent"
8
+
9
+ module ActionPubsub
10
+ extend ::ActiveSupport::Autoload
11
+
12
+ autoload :ActiveRecord
13
+ autoload :Config
14
+ autoload :Event
15
+ autoload :Exchange
16
+ autoload :ExchangeRegistry
17
+ autoload :Publish
18
+ autoload :Publishable
19
+ autoload :Logging
20
+ autoload :Subscriber
21
+ autoload :Queue
22
+
23
+ @configuration ||= ::ActionPubsub::Config.new
24
+
25
+ def self.event_count
26
+ @event_count ||= ::Concurrent::Agent.new(0)
27
+ end
28
+
29
+ def self.exchange_registry
30
+ @exchange_registry ||= ::ActionPubsub::ExchangeRegistry.new
31
+ end
32
+
33
+ @some_registry = ::Concurrent::LazyRegister.new
34
+
35
+ def self.destination_tuple_from_path(path_string)
36
+ segs = path_string.split("/")
37
+ worker_index = segs.pop
38
+ action = segs.pop
39
+
40
+ [segs.join("/"), action, worker_index]
41
+ end
42
+
43
+ def self.destination_tuple_from_sender_path(path_string)
44
+ segs = path_string.split("/")
45
+ action = segs.pop
46
+ [segs.join("/"), action]
47
+ end
48
+
49
+ def self.symbolize_routing_key(routing_key)
50
+ :"#{routing_key.split('.').join('_')}"
51
+ end
52
+
53
+ def self.publish_event(routing_key, event)
54
+ #need to loop through exchanges and publish to them
55
+ #maybe there is a better way to do this?
56
+ exchange_hash = ::ActionPubsub.exchanges[routing_key].instance_variable_get("@data").value
57
+ queue_names = exchange_hash.keys
58
+
59
+ queue_names.each do |queue_name|
60
+ exchange_registry[routing_key][queue_name] << serialize_event(event)
61
+ end
62
+ end
63
+
64
+ def self.serialize_event(event)
65
+ event
66
+ end
67
+
68
+ def self.deserialize_event(event)
69
+ event
70
+ end
71
+
72
+ class << self
73
+ attr_accessor :configuration
74
+ alias_method :config, :configuration
75
+ alias_method :exchanges, :exchange_registry
76
+
77
+ delegate :register_queue, :to => :exchange_registry
78
+ delegate :register_channel, :to => :exchange_registry
79
+ delegate :register_exchange, :to => :exchange_registry
80
+ end
81
+ end
@@ -0,0 +1,11 @@
1
+ module ActionPubsub
2
+ module ActiveRecord
3
+ extend ::ActiveSupport::Autoload
4
+
5
+ autoload :Events
6
+ autoload :OnChange
7
+ autoload :Publishable
8
+ autoload :Subscriber
9
+ autoload :Subscription
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ module ActionPubsub
2
+ module ActiveRecord
3
+ module Events
4
+ extend ::ActiveSupport::Autoload
5
+
6
+ autoload :Changed
7
+ autoload :Created
8
+ autoload :Destroyed
9
+ autoload :Updated
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,29 @@
1
+ module ActionPubsub
2
+ module ActiveRecord
3
+ module Events
4
+ module Changed
5
+ extend ::ActiveSupport::Concern
6
+
7
+ included do
8
+ after_commit :publish_changed_event, :on => :update
9
+
10
+ routing_key = [exchange_prefix, "changed"].join("/")
11
+ ::ActionPubsub.register_exchange(routing_key)
12
+ end
13
+
14
+ def publish_changed_event
15
+ routing_key = [self.class.exchange_prefix, "changed"].join("/")
16
+
17
+ record_changed_event = ::ActionPubsub::Event.new(
18
+ :topic => routing_key,
19
+ :record => self
20
+ )
21
+
22
+ ::ActiveRecord::Base.connection_pool.with_connection do
23
+ ::ActionPubsub.publish_event(routing_key, record_changed_event)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ module ActionPubsub
2
+ module ActiveRecord
3
+ module Events
4
+ module Created
5
+ extend ::ActiveSupport::Concern
6
+
7
+ included do
8
+ after_commit :publish_created_event, :on => :create
9
+
10
+ routing_key = [exchange_prefix, "created"].join("/")
11
+ ::ActionPubsub.register_exchange(routing_key)
12
+ end
13
+
14
+ def publish_created_event
15
+ routing_key = [self.class.exchange_prefix, "created"].join("/")
16
+
17
+ record_created_event = ::ActionPubsub::Event.new(
18
+ :topic => routing_key,
19
+ :record => self
20
+ )
21
+
22
+ ::ActiveRecord::Base.connection_pool.with_connection do
23
+ ::ActionPubsub.publish_event(routing_key, record_created_event)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ module ActionPubsub
2
+ module ActiveRecord
3
+ module Events
4
+ module Destroyed
5
+ extend ::ActiveSupport::Concern
6
+
7
+ included do
8
+ after_commit :publish_destroyed_event, :on => :create
9
+
10
+ routing_key = [exchange_prefix, "destroyed"].join("/")
11
+ ::ActionPubsub.register_exchange(routing_key)
12
+ end
13
+
14
+ def publish_destroyed_event
15
+ routing_key = [self.class.exchange_prefix, "destroyed"].join("/")
16
+
17
+ record_destroyed_event = ::ActionPubsub::Event.new(
18
+ :topic => routing_key,
19
+ :record => self
20
+ )
21
+
22
+ ::ActiveRecord::Base.connection_pool.with_connection do
23
+ ::ActionPubsub.publish_event(routing_key, record_destroyed_event)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ module ActionPubsub
2
+ module ActiveRecord
3
+ module Events
4
+ module Updated
5
+ extend ::ActiveSupport::Concern
6
+
7
+ included do
8
+ after_commit :publish_updated_event, :on => :update
9
+
10
+ routing_key = [exchange_prefix, "updated"].join("/")
11
+ ::ActionPubsub.register_exchange(routing_key)
12
+ end
13
+
14
+ def publish_updated_event
15
+ routing_key = [self.class.exchange_prefix, "updated"].join("/")
16
+
17
+ record_updated_event = ::ActionPubsub::Event.new(
18
+ :topic => routing_key,
19
+ :record => self
20
+ )
21
+
22
+ ::ActiveRecord::Base.connection_pool.with_connection do
23
+ ::ActionPubsub.publish_event(routing_key, record_updated_event)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,54 @@
1
+ module ActionPubsub
2
+ module ActiveRecord
3
+ module OnChange
4
+ extend ::ActiveSupport::Concern
5
+
6
+ included do
7
+ class << self
8
+ attr_accessor :_on_change_watches
9
+ end
10
+
11
+ @_on_change_watches = {}.with_indifferent_access
12
+ end
13
+
14
+ module ClassMethods
15
+ def on_change(*attribute_names, &block)
16
+ options = attribute_names.extract_options!
17
+
18
+ attribute_names.each do |attribute|
19
+ @_on_change_watches[attribute] = { :block => block }
20
+ @_on_change_watches[attribute].merge!(options)
21
+ end
22
+
23
+ on :changed do |record|
24
+ record.previous_changes.each_pair do |k,vals|
25
+ if self.class.react_to_changed?(record, k, vals)
26
+ old_value, new_value = vals
27
+ self.instance_exec(new_value, old_value, &self.class.watched_attributes[k][:block])
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ def react_to_changed?(resource, attribute_name, value)
34
+ old_value, new_value = value
35
+ return false unless watching_attribute?(attribute_name)
36
+ result = true
37
+ result &&= old_value == watched_attributes[attribute_name][:from] if watched_attributes[attribute_name].key?(:from)
38
+ result &&= new_value == watched_attributes[attribute_name][:to] if watched_attributes[attribute_name].key?(:to)
39
+ result &&= watched_attributes[attribute_name][:if].call(resource) if watched_attributes[attribute_name].key?(:if)
40
+ result &&= !watched_attributes[attribute_name][:unless].call(resource) if watched_attributes[attribute_name].key?(:unless)
41
+ return result
42
+ end
43
+
44
+ def watching_attribute?(attribute_name)
45
+ watched_attributes.key?(attribute_name)
46
+ end
47
+
48
+ def watched_attributes
49
+ self.instance_variable_get(:@_on_change_watches)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,47 @@
1
+ module ActionPubsub
2
+ module ActiveRecord
3
+ module Publishable
4
+ extend ActiveSupport::Concern
5
+
6
+ PUBLISHABLE_EVENTS = {
7
+ :changed => ::ActionPubsub::ActiveRecord::Events::Changed,
8
+ :updated => ::ActionPubsub::ActiveRecord::Events::Updated,
9
+ :created => ::ActionPubsub::ActiveRecord::Events::Created,
10
+ :destroyed => ::ActionPubsub::ActiveRecord::Events::Destroyed
11
+ }
12
+
13
+ included do
14
+ include ::ActiveModel::Dirty unless ancestors.include?(::ActiveModel::Dirty)
15
+
16
+ class_attribute :exchange_prefix
17
+
18
+ class << self
19
+ attr_accessor :_publishable_actions
20
+ end
21
+ end
22
+
23
+ def attributes_hash
24
+ hash = self.as_json
25
+ hash.merge!(:changes => previous_changes) if previous_changes && hash
26
+ hash.symbolize_keys! if hash
27
+ hash
28
+ end
29
+
30
+ private
31
+
32
+ module ClassMethods
33
+ def publish_as(_exchange_prefix)
34
+ self.exchange_prefix = _exchange_prefix
35
+ end
36
+
37
+ def publishable_actions(*actions)
38
+ @_publishable_actions = actions
39
+
40
+ actions.each do |action|
41
+ include PUBLISHABLE_EVENTS[action] unless ancestors.include?(PUBLISHABLE_EVENTS[action])
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,90 @@
1
+ module ActionPubsub
2
+ module ActiveRecord
3
+ class Subscriber
4
+ class_attribute :concurrency,
5
+ :event_triggered_count,
6
+ :event_processed_count,
7
+ :event_failed_count,
8
+ :observed_exchanges,
9
+ :messages_received_count,
10
+ :messages_processed_count,
11
+ :queue,
12
+ :reactions,
13
+ :subscription
14
+
15
+ self.concurrency = 1
16
+
17
+ attr_accessor :resource, :current_event
18
+
19
+ #the indirection here with the "subscription" dynamically created class, is for the sake
20
+ #of making subscribers immutable and not storing instance state.
21
+ #i.e. subscription is the actual actor, which just instantiates this subscriber class
22
+ #and performs the task it needs to
23
+ def self.inherited(subklass)
24
+ subklass.subscription = subklass.const_set("Subscription", ::Class.new(::ActionPubsub::ActiveRecord::Subscription))
25
+ subklass.subscription.subscriber = subklass
26
+ subklass.reactions = {}
27
+ subklass.observed_exchanges = ::Set.new
28
+ subklass.event_triggered_count = ::Concurrent::AtomicFixnum.new(0)
29
+ subklass.event_failed_count = ::Concurrent::AtomicFixnum.new(0)
30
+ subklass.event_processed_count = ::Concurrent::AtomicFixnum.new(0)
31
+ end
32
+
33
+ def self.on(event_name, **options, &block)
34
+ reactions[event_name] = {}.tap do |hash|
35
+ hash[:block] = block
36
+ hash[:conditions] = options.extract!(:if, :unless)
37
+ end
38
+
39
+ register_reaction_to_event(event_name)
40
+ end
41
+
42
+ def self.increment_event_failed_count!
43
+ self.event_failed_count.increment
44
+ end
45
+
46
+ def self.increment_event_processed_count!
47
+ self.event_processed_count.increment
48
+ end
49
+
50
+ def self.increment_event_triggered_count!
51
+ self.event_triggered_count.increment
52
+ end
53
+
54
+ def self.subscribe_to(*exchanges)
55
+ exchanges.each{ |exchange| self.observed_exchanges << exchange }
56
+ end
57
+
58
+ def self.react?(event_name, reaction, record)
59
+ return false if reaction[:block].blank?
60
+ return true if reaction[:conditions].blank?
61
+ result = true
62
+ result &&= !reaction[:conditions][:unless].call(record) if reaction[:conditions].key?(:unless)
63
+ result &&= reaction[:conditions][:if].call(record) if reaction[:conditions].key?(:if)
64
+ return result
65
+ end
66
+
67
+ def self.register_reaction_to_event(event_name)
68
+ observed_exchanges.each do |exchange_prefix|
69
+ target_exchange = [exchange_prefix, event_name].join("/")
70
+ subscriber_key = name.underscore
71
+ queue_key = [target_exchange, subscriber_key].join("/")
72
+
73
+ ::ActionPubsub.register_queue(target_exchange, subscriber_key)
74
+
75
+ self.concurrency.times do |i|
76
+ self.subscription.spawn("#{queue_key}/#{i}") do
77
+ self.subscription.bind_subscription(target_exchange, subscriber_key)
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ ### Instance Methods ###
84
+ def initialize(record, event:nil)
85
+ @resource = record
86
+ @current_event = event if event
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,27 @@
1
+ module ActionPubsub
2
+ module ActiveRecord
3
+ class Subscription < ::Concurrent::Actor::Utils::AdHoc
4
+ class_attribute :subscriber
5
+
6
+ def self.bind_subscription(target_exchange, subscriber_key)
7
+ ::ActionPubsub.exchange_registry[target_exchange][subscriber_key] << :subscribe
8
+ -> message {
9
+ ::ActiveRecord::Base.connection_pool.with_connection do
10
+ message = ::ActionPubsub.deserialize_event(message)
11
+ reaction = self.class.subscriber.reactions[message["action"]]
12
+ record = message["record"]
13
+
14
+ if self.class.subscriber.react?(message["action"], reaction, record)
15
+ self.class.subscriber.increment_event_triggered_count!
16
+
17
+ subscriber_instance = self.class.subscriber.new(record)
18
+ subscriber_instance.instance_exec(record, &reaction[:block])
19
+ end
20
+
21
+ self.class.bind_subscription(target_exchange, subscriber_key)
22
+ end
23
+ }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,22 @@
1
+ require 'active_support/ordered_options'
2
+
3
+ module ActionPubsub
4
+ class Config < ::ActiveSupport::InheritableOptions
5
+ def initialize(*args)
6
+ super(*args)
7
+
8
+ self[:debug] = false
9
+ self[:serializer] = nil
10
+ end
11
+
12
+ def debug=(val)
13
+ ::Concurrent.use_stdlib_logger(Logger::DEBUG) if val
14
+ end
15
+
16
+ def serializer=(val)
17
+ if val && val.ancestors.include?(::ActiveSupport::Concern)
18
+ ::ActionPubsub.include(val)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,31 @@
1
+ require 'active_model'
2
+
3
+ module ActionPubsub
4
+ class Event
5
+ include ::ActiveAttr::Model
6
+ include ::ActiveModel::AttributeMethods
7
+
8
+ attribute :id
9
+ attribute :action
10
+ attribute :context
11
+ attribute :meta
12
+ attribute :name
13
+ attribute :occured_at
14
+ attribute :record
15
+ attribute :subject
16
+ attribute :topic
17
+
18
+ #attributes have to be set for purposes of marshaling
19
+ def initialize(topic:, record:nil, context: nil, **options)
20
+ self[:topic] = topic
21
+ self[:action] = topic.split("/").pop.to_sym
22
+ self[:meta] = options[:meta] || {}
23
+ self[:name] = topic
24
+ self[:record] = record
25
+ self[:id] = ::SecureRandom.hex
26
+ self[:subject] = options[:subject] || record.try(:class).try(:name)
27
+ self[:context] = context if context
28
+ self[:occured_at] ||= ::Time.now.to_i
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,15 @@
1
+ module ActionPubsub
2
+ class ExchangeRegistry < ::Concurrent::LazyRegister
3
+ def register_queue(exchange_name, subscriber_name)
4
+ register_exchange(exchange_name) unless key?(exchange_name)
5
+ exchange_hash = self[exchange_name].instance_variable_get("@data").value
6
+ exchange_keys = exchange_hash.keys
7
+ queue_name = [exchange_name, subscriber_name].join("/")
8
+ self[exchange_name].add(subscriber_name) { ::ActionPubsub::Queue.spawn(queue_name) }
9
+ end
10
+
11
+ def register_exchange(exchange_name)
12
+ add(exchange_name) { ::Concurrent::LazyRegister.new }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ module ActionPubsub
2
+ class Queue < ::Concurrent::Actor::Utils::Balancer
3
+ end
4
+ end
@@ -0,0 +1,7 @@
1
+ module ActionPubsub
2
+ module Serialization
3
+ extend ::ActiveSupport::Autoload
4
+
5
+ autoload :Marshal
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ module ActionPubsub
2
+ module Serialization
3
+ module Marshal
4
+ extend ActiveSupport::Concern
5
+
6
+ module Marshal
7
+ def self.serialize_event(event)
8
+ ::Marshal.dump(super(event))
9
+ end
10
+
11
+ def self.deserialize_event(event)
12
+ ::Marshal.load(super(event))
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,39 @@
1
+ ### NOTE: Don't use this. Only the active record subscriber is working ATM.
2
+ module ActionPubsub
3
+ class Subscriber < ::Concurrent::Actor::Utils::AdHoc
4
+ class_attribute :concurrency, :queue, :exchange_prefix, :watches
5
+ self.concurrency = 1
6
+
7
+ def self.inherited(subklass)
8
+ subklass.watches = {}
9
+ end
10
+
11
+ def self.register_event_watcher(event_name)
12
+ target_exchange = [exchange_prefix, event_name].join("/")
13
+ subscriber_key = name.underscore
14
+ queue_key = [target_exchange, subscriber_key].join("/")
15
+
16
+ ::ActionPubsub.register_queue(target_exchange, subscriber_key)
17
+
18
+ self.concurrency.times do |i|
19
+ spawn("#{queue_key}/#{i}") do
20
+ bind_subscription(target_exchange, subscriber_key)
21
+ end
22
+ end
23
+ end
24
+
25
+ def self.on(event_name, &block)
26
+ watches[event_name] = block
27
+ register_event_watcher(event_name)
28
+ end
29
+
30
+ def self.bind_subscription(target_exchange, subscriber_key)
31
+ ::ActionPubsub.exchange_registry[target_exchange][subscriber_key] << :subscribe
32
+ -> message {
33
+ self.class.watches[message["action"]].call(message["record"])
34
+
35
+ self.class.bind_subscription(target_exchange, subscriber_key)
36
+ }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,3 @@
1
+ module ActionPubsub
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,311 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: action_pubsub
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jason Ayre
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-07-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: active_attr
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: concurrent-ruby
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.10'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.10'
69
+ - !ruby/object:Gem::Dependency
70
+ name: guard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: guard-rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '4'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '4'
97
+ - !ruby/object:Gem::Dependency
98
+ name: guard-bundler
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '10.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '10.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rspec-pride
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rspec-its
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '1'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '1'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rspec-collection_matchers
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '1'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '1'
181
+ - !ruby/object:Gem::Dependency
182
+ name: rb-fsevent
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: simplecov
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: terminal-notifier-guard
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
223
+ - !ruby/object:Gem::Dependency
224
+ name: pry
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: '0'
230
+ type: :development
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: '0'
237
+ - !ruby/object:Gem::Dependency
238
+ name: pry-nav
239
+ requirement: !ruby/object:Gem::Requirement
240
+ requirements:
241
+ - - ">="
242
+ - !ruby/object:Gem::Version
243
+ version: '0'
244
+ type: :development
245
+ prerelease: false
246
+ version_requirements: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - ">="
249
+ - !ruby/object:Gem::Version
250
+ version: '0'
251
+ description: In process, async, concurrent pubsub
252
+ email:
253
+ - jasonayre@gmail.com
254
+ executables: []
255
+ extensions: []
256
+ extra_rdoc_files: []
257
+ files:
258
+ - ".gitignore"
259
+ - ".rspec"
260
+ - ".travis.yml"
261
+ - Gemfile
262
+ - LICENSE.txt
263
+ - README.md
264
+ - Rakefile
265
+ - action_pubsub.gemspec
266
+ - bin/console
267
+ - bin/setup
268
+ - lib/action_pubsub.rb
269
+ - lib/action_pubsub/active_record.rb
270
+ - lib/action_pubsub/active_record/events.rb
271
+ - lib/action_pubsub/active_record/events/changed.rb
272
+ - lib/action_pubsub/active_record/events/created.rb
273
+ - lib/action_pubsub/active_record/events/destroyed.rb
274
+ - lib/action_pubsub/active_record/events/updated.rb
275
+ - lib/action_pubsub/active_record/on_change.rb
276
+ - lib/action_pubsub/active_record/publishable.rb
277
+ - lib/action_pubsub/active_record/subscriber.rb
278
+ - lib/action_pubsub/active_record/subscription.rb
279
+ - lib/action_pubsub/config.rb
280
+ - lib/action_pubsub/event.rb
281
+ - lib/action_pubsub/exchange_registry.rb
282
+ - lib/action_pubsub/queue.rb
283
+ - lib/action_pubsub/serialization.rb
284
+ - lib/action_pubsub/serialization/marshal.rb
285
+ - lib/action_pubsub/subscriber.rb
286
+ - lib/action_pubsub/version.rb
287
+ homepage: http://github.com/jasonayre/action_pubsub
288
+ licenses:
289
+ - MIT
290
+ metadata: {}
291
+ post_install_message:
292
+ rdoc_options: []
293
+ require_paths:
294
+ - lib
295
+ required_ruby_version: !ruby/object:Gem::Requirement
296
+ requirements:
297
+ - - ">="
298
+ - !ruby/object:Gem::Version
299
+ version: '0'
300
+ required_rubygems_version: !ruby/object:Gem::Requirement
301
+ requirements:
302
+ - - ">="
303
+ - !ruby/object:Gem::Version
304
+ version: '0'
305
+ requirements: []
306
+ rubyforge_project:
307
+ rubygems_version: 2.4.5
308
+ signing_key:
309
+ specification_version: 4
310
+ summary: In process, async, concurrent pubsub
311
+ test_files: []