promiscuous 0.9.3.1 → 0.10

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -3,119 +3,22 @@ Promiscuous
3
3
 
4
4
  [![Build Status](https://secure.travis-ci.org/crowdtap/promiscuous.png?branch=master)](https://secure.travis-ci.org/crowdtap/promiscuous)
5
5
 
6
- Promiscuous offers an automatic way of propagating your model data across one or
7
- more applications. It supports Mongoid2, Mongoid3 and ActiveRecord.
8
- It uses [RabbitMQ](http://www.rabbitmq.com/).
9
-
10
- Usage
11
- ------
12
-
13
- From a publisher side (app that owns the data), create a Publisher per model.
14
- as shown below.
15
-
16
- From your subscribers side (apps that receive updates from the publisher),
17
- create a Subscriber per model, as shown below.
18
-
19
- Example
20
- --------
21
-
22
- ### In your publisher app
23
-
24
- ```ruby
25
- # initializer
26
- Promiscuous::AMQP.configure(:app => 'crowdtap',
27
- :server_uri => 'amqp://user:password@host:port/vhost')
28
-
29
- # publisher
30
- class ModelPublisher < Promiscuous::Publisher::Mongoid
31
- publish :to => 'crowdtap/model',
32
- :class => :Model,
33
- :attributes => [:field_1, :field_2, :field_3]
34
- end
35
- ```
36
-
37
- ### In your subscriber app
38
-
39
- ```ruby
40
- # initializer
41
- Promiscuous::AMQP.configure(:app => 'sniper',
42
- :server_uri => 'amqp://user:password@host:port/vhost',
43
- :error_handler => some_proc)
44
-
45
- # subscriber
46
- class ModelSubscriber < Promiscuous::Subscriber::Mongoid
47
- subscribe :from => 'crowdtap/model',
48
- :attributes => [:field_1, :field_2, :field_3],
49
- :class => Model, # optional
50
- :foreign_key => :publisher_id # optional
51
- end
52
- ```
53
-
54
- ### Starting the subscriber worker
55
-
56
- rake promiscuous:replicate
57
-
58
- How does it work ?
59
- ------------------
60
-
61
- 1. On the publisher side, Promiscuous hooks into the after_create/update/destroy callbacks.
62
- 2. When a model changes, Promiscuous sends a message to RabbitMQ, to the
63
- 'promiscuous' [topic exchange](http://www.rabbitmq.com/tutorials/tutorial-five-python.html).
64
- 3. RabbitMQ routes the messages to each application through queues.
65
- We use one queue per application (TODO explain why we need one queue).
66
- 4. Subscribers apps are running the promiscuous worker, listening on their own queues,
67
- executing the create/update/destroy on their databases.
68
-
69
- Note that we use a single exchange to preserve the ordering of data updates
70
- across application so that subscribers always see a consistant state of the
71
- system.
72
-
73
- Synching databases
74
- -------------------
75
-
76
- Documents are created if not present when receiving an update on a non existing
77
- document.
78
-
79
- TODO: Explain how to sync databases.
80
-
81
- WARNING/TODO
82
- ------------
83
-
84
- Promiscuous does **not** handle:
85
- - ActiveRecord polymorphism.
86
- - Any of the Mongoid atomic operatiors, such as inc, or add_to_set.
87
- - Association magic. Example:
88
- ```ruby
89
- # This will NOT replicate particiation_ids:
90
- m = Member.first
91
- m.particiations = [Participation.first]
92
- m.save
93
-
94
- # On the other hand, this will:
95
- m = Member.first
96
- m.particiation_ids = [Participation.first.ids]
97
- m.save
98
- ```
99
-
100
- Furthermore, it can be racy. Consider this scenario with two interleaving
101
- requests A and B:
102
-
103
- 1. (A) Update mongo doc X.value = 1
104
- 2. (B) Update mongo doc X.value = 2
105
- 3. (B) Publish 'X.value = 2' to Rabbit
106
- 4. (A) Publish 'X.value = 1' to Rabbit
107
-
108
- At the end of the scenario, on the publisher side, the document X has value
109
- equal to 2, while on the subscriber side, the document has a value of 1. This
110
- will likely not occur in most scenarios BUT BEWARE. We have plans to fix this
111
- issue by using version numbers and mongo's amazing findandmodify.
112
-
113
- Backend: bunny / ruby-amqp
114
- --------------------------
115
-
116
- Your publisher app may not run an eventmachine loop, which is required for
117
- ruby-amqp. Bunny on the other hand allows a non-eventmachine based application
118
- to publish messages to rabbitmq.
6
+ Promiscuous is designed to facilitate designing a
7
+ [service-oriented architecture](http://en.wikipedia.org/wiki/Service-oriented_architecture)
8
+ in Ruby.
9
+
10
+ Promiscuous offers an automatic way of propagating your data across one or more
11
+ applications. It supports Mongoid2, Mongoid3 and ActiveRecord.
12
+ It relies on [RabbitMQ](http://www.rabbitmq.com/) to push data around.
13
+
14
+ Philosophy
15
+ ----------
16
+
17
+ In order for a service-oriented system to be successful, services *must* be
18
+ loosely coupled. The traditional Ruby way of tackling this problem is to
19
+ provide RESTful APIs.
20
+ Sadly, this come to a cost since one must write controllers, integration tests, etc.
21
+ Promiscuous to the rescue
119
22
 
120
23
  Compatibility
121
24
  -------------
@@ -11,12 +11,12 @@ module Promiscuous
11
11
  raise "Please use amqp://user:password@host:port/vhost" if uri.scheme != 'amqp'
12
12
 
13
13
  {
14
- :host => uri.host,
15
- :port => uri.port,
14
+ :host => uri.host,
15
+ :port => uri.port,
16
16
  :scheme => uri.scheme,
17
- :user => uri.user,
18
- :pass => uri.password,
19
- :vhost => uri.path.empty? ? "/" : uri.path,
17
+ :user => uri.user,
18
+ :pass => uri.password,
19
+ :vhost => uri.path.empty? ? "/" : uri.path,
20
20
  }
21
21
  end
22
22
 
@@ -26,7 +26,8 @@ module Promiscuous
26
26
  end
27
27
 
28
28
  def self.disconnect
29
- channel.close
29
+ self.channel.close if self.channel
30
+ self.channel = nil
30
31
  end
31
32
 
32
33
  def self.subscribe(options={}, &block)
@@ -0,0 +1,17 @@
1
+ module Promiscuous::Common::Worker
2
+ extend ActiveSupport::Concern
3
+
4
+ def initialize
5
+ self.stop = false
6
+ end
7
+
8
+ def unit_of_work
9
+ if defined?(Mongoid)
10
+ Mongoid.unit_of_work { yield }
11
+ else
12
+ yield
13
+ end
14
+ end
15
+
16
+ included { attr_accessor :stop }
17
+ end
@@ -2,4 +2,5 @@ module Promiscuous::Common
2
2
  autoload :Options, 'promiscuous/common/options'
3
3
  autoload :Lint, 'promiscuous/common/lint'
4
4
  autoload :ClassHelpers, 'promiscuous/common/class_helpers'
5
+ autoload :Worker, 'promiscuous/common/worker'
5
6
  end
@@ -2,7 +2,7 @@ module Promiscuous::Publisher::AMQP
2
2
  extend ActiveSupport::Concern
3
3
  include Promiscuous::Publisher::Envelope
4
4
 
5
- def amqp_publish
5
+ def publish
6
6
  Promiscuous::AMQP.publish(:key => to, :payload => payload.to_json)
7
7
  end
8
8
 
@@ -0,0 +1,18 @@
1
+ class Promiscuous::Publisher::Error < RuntimeError
2
+ attr_accessor :inner, :instance
3
+
4
+ def initialize(inner, instance)
5
+ super(inner)
6
+ set_backtrace(inner.backtrace)
7
+ self.inner = inner
8
+ self.instance = instance
9
+ end
10
+
11
+ def message
12
+ "#{inner.message} while processing #{instance}"
13
+ end
14
+
15
+ def to_s
16
+ message
17
+ end
18
+ end
@@ -33,7 +33,7 @@ module Promiscuous::Publisher::Model
33
33
  [:create, :update, :destroy].each do |operation|
34
34
  __send__("after_#{operation}", "promiscuous_publish_#{operation}".to_sym)
35
35
  define_method "promiscuous_publish_#{operation}" do
36
- self.class.promiscuous_publisher.new(:instance => self, :operation => operation).amqp_publish
36
+ self.class.promiscuous_publisher.new(:instance => self, :operation => operation).publish
37
37
  end
38
38
  end
39
39
  alias :promiscuous_sync :promiscuous_publish_update
@@ -0,0 +1,52 @@
1
+ module Promiscuous::Publisher::Mongoid::Defer
2
+ extend ActiveSupport::Concern
3
+
4
+ mattr_accessor :klasses
5
+ self.klasses = {}
6
+
7
+ def publish
8
+ super unless should_defer?
9
+ end
10
+
11
+ def should_defer?
12
+ if options.has_key?(:defer)
13
+ options[:defer]
14
+ else
15
+ operation == :update
16
+ end
17
+ end
18
+
19
+ def self.hook_mongoid
20
+ return if @mongoid_hooked
21
+ @mongoid_hooked = true
22
+
23
+ Moped::Query.class_eval do
24
+ alias_method :update_orig, :update
25
+ def update(change, flags = nil)
26
+ if klass = Promiscuous::Publisher::Mongoid::Defer.klasses[@collection.name]
27
+ psp_field = klass.aliased_fields["promiscous_sync_pending"]
28
+ change = change.dup
29
+ change['$set'] ||= {}
30
+ change['$set'].merge!(psp_field => true)
31
+ end
32
+ update_orig(change, flags)
33
+ end
34
+ end
35
+ end
36
+
37
+ included do
38
+ klass.class_eval do
39
+ cattr_accessor :publisher_defer_hooked
40
+ return if self.publisher_defer_hooked
41
+ self.publisher_defer_hooked = true
42
+
43
+ # TODO Make sure we are not overriding a field, although VERY unlikly
44
+ psp_field = :_psp
45
+ field psp_field, :as => :promiscous_sync_pending, :type => Boolean
46
+ index({psp_field => 1}, :background => true, :sparse => true)
47
+
48
+ Promiscuous::Publisher::Mongoid::Defer.hook_mongoid
49
+ Promiscuous::Publisher::Mongoid::Defer.klasses[collection.name] = self
50
+ end
51
+ end
52
+ end
@@ -1,5 +1,6 @@
1
1
  class Promiscuous::Publisher::Mongoid < Promiscuous::Publisher::Base
2
2
  autoload :Embedded, 'promiscuous/publisher/mongoid/embedded'
3
+ autoload :Defer, 'promiscuous/publisher/mongoid/defer'
3
4
 
4
5
  include Promiscuous::Publisher::Class
5
6
  include Promiscuous::Publisher::Attributes
@@ -13,6 +14,11 @@ class Promiscuous::Publisher::Mongoid < Promiscuous::Publisher::Base
13
14
  include Promiscuous::Publisher::Mongoid::Embedded
14
15
  else
15
16
  include Promiscuous::Publisher::Model
17
+ include Promiscuous::Publisher::Mongoid::Defer if mongoid3?
16
18
  end
17
19
  end
20
+
21
+ def self.mongoid3?
22
+ Gem.loaded_specs['mongoid'].version >= Gem::Version.new('3.0.0')
23
+ end
18
24
  end
@@ -0,0 +1,49 @@
1
+ class Promiscuous::Publisher::Worker
2
+ include Promiscuous::Common::Worker
3
+
4
+ def self.poll_delay
5
+ # TODO Configurable globally
6
+ # TODO Configurable per publisher
7
+ 1.second
8
+ end
9
+
10
+ def replicate
11
+ EM.defer proc { self.replicate_once },
12
+ proc { EM::Timer.new(self.class.poll_delay) { replicate } }
13
+ end
14
+
15
+ def replicate_once
16
+ return if self.stop
17
+ begin
18
+ self.unit_of_work do
19
+ Promiscuous::Publisher::Mongoid::Defer.klasses.values.each do |klass|
20
+ replicate_collection(klass)
21
+ end
22
+ end
23
+ rescue Exception => e
24
+ self.stop = true
25
+ unless e.is_a?(Promiscuous::Publisher::Error)
26
+ e = Promiscuous::Publisher::Error.new(e, nil)
27
+ end
28
+ Promiscuous.error "[publish] FATAL #{e}"
29
+ Promiscuous::Config.error_handler.try(:call, e)
30
+ end
31
+ end
32
+
33
+ def replicate_collection(klass)
34
+ return if self.stop
35
+ # TODO Check for indexes and if not there, bail out
36
+ psp_field = klass.aliased_fields["promiscous_sync_pending"]
37
+ while instance = klass.where(psp_field => true).find_and_modify({'$unset' => {psp_field => 1}})
38
+ replicate_instance(instance)
39
+ end
40
+ end
41
+
42
+ def replicate_instance(instance)
43
+ return if self.stop
44
+ instance.class.promiscuous_publisher.new(:instance => instance, :operation => :update, :defer => false).publish
45
+ rescue Exception => e
46
+ # TODO set back the psp field
47
+ raise Promiscuous::Publisher::Error.new(e, instance)
48
+ end
49
+ end
@@ -10,6 +10,8 @@ module Promiscuous::Publisher
10
10
  autoload :Model, 'promiscuous/publisher/model'
11
11
  autoload :Mongoid, 'promiscuous/publisher/mongoid'
12
12
  autoload :Polymorphic, 'promiscuous/publisher/polymorphic'
13
+ autoload :Worker, 'promiscuous/publisher/worker'
14
+ autoload :Error, 'promiscuous/publisher/error'
13
15
 
14
16
  def self.lint(*args)
15
17
  Lint.lint(*args)
@@ -1,18 +1,38 @@
1
1
  namespace :promiscuous do
2
- desc 'Run the subscribers worker'
2
+ # Note This rake task can be loaded without Rails
3
+ desc 'Run the workers'
3
4
  task :replicate => :environment do |t|
4
- require 'promiscuous/worker'
5
5
  require 'eventmachine'
6
6
  require 'em-synchrony'
7
7
 
8
8
  EM.synchrony do
9
- Promiscuous::Loader.load_descriptors :subscribers if defined?(Rails)
10
- Promiscuous::AMQP.disconnect
11
- Promiscuous::Config.backend = :rubyamqp
12
- Promiscuous::AMQP.connect
9
+ trap_signals
10
+ force_backend :rubyamqp
11
+
12
+ Promiscuous::Loader.load_descriptors if defined?(Rails)
13
13
 
14
14
  Promiscuous::Worker.replicate
15
- $stderr.puts "Replicating with #{Promiscuous::Subscriber::AMQP.subscribers.count} subscribers"
15
+
16
+ msg = "Replicating with #{Promiscuous::Subscriber::AMQP.subscribers.count} subscribers" +
17
+ " and #{Promiscuous::Publisher::Mongoid::Defer.klasses.count} publishers"
18
+ Promiscuous.info msg
19
+ $stderr.puts msg
16
20
  end
17
21
  end
22
+
23
+ def trap_signals
24
+ %w(SIGTERM SIGINT).each do |signal|
25
+ Signal.trap(signal) do
26
+ Promiscuous.info "Exiting..."
27
+ Promiscuous::Worker.stop
28
+ EM.stop
29
+ end
30
+ end
31
+ end
32
+
33
+ def force_backend(backend)
34
+ Promiscuous::AMQP.disconnect
35
+ Promiscuous::Config.backend = backend
36
+ Promiscuous::AMQP.connect
37
+ end
18
38
  end
@@ -16,7 +16,7 @@ module Promiscuous::Subscriber::AMQP
16
16
  included { use_option :from }
17
17
 
18
18
  module ClassMethods
19
- def from=(value)
19
+ def from=(_)
20
20
  super
21
21
  old_sub = Promiscuous::Subscriber::AMQP.subscribers[from]
22
22
  raise "The subscriber '#{old_sub}' already listen on '#{from}'" if old_sub
@@ -2,7 +2,7 @@ module Promiscuous::Subscriber::Mongoid::Embedded
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  def fetch
5
- old_value.nil? ? klass.new.tap { |m| m.id = id } : old_value
5
+ (old_value || klass.new).tap { |m| m.id = id }
6
6
  end
7
7
 
8
8
  def old_value
@@ -30,7 +30,6 @@ module Promiscuous::Subscriber::Polymorphic
30
30
 
31
31
  def polymorphic_subscriber_from(payload)
32
32
  type = payload.is_a?(Hash) ? payload['type'] : nil
33
- raise "The payload is missing the type information'" if type.nil?
34
33
  polymorphic_map[type] || self
35
34
  end
36
35
  end
@@ -0,0 +1,31 @@
1
+ class Promiscuous::Subscriber::Worker
2
+ include Promiscuous::Common::Worker
3
+
4
+ def replicate
5
+ Promiscuous::AMQP.subscribe(subscribe_options) do |metadata, payload|
6
+ # Note: This code always runs on the root Fiber,
7
+ # so ordering is always preserved
8
+ begin
9
+ unless self.stop
10
+ Promiscuous.info "[receive] #{payload}"
11
+ self.unit_of_work { Promiscuous::Subscriber.process(JSON.parse(payload)) }
12
+ metadata.ack
13
+ end
14
+ rescue Exception => e
15
+ e = Promiscuous::Subscriber::Error.new(e, payload)
16
+
17
+ # TODO Discuss with Arjun about having an error queue.
18
+ self.stop = true
19
+ Promiscuous::AMQP.disconnect
20
+ Promiscuous.error "[receive] FATAL #{e}"
21
+ Promiscuous::Config.error_handler.try(:call, e)
22
+ end
23
+ end
24
+ end
25
+
26
+ def subscribe_options
27
+ queue_name = "#{Promiscuous::Config.app}.promiscuous"
28
+ bindings = Promiscuous::Subscriber::AMQP.subscribers.keys
29
+ {:queue_name => queue_name, :bindings => bindings}
30
+ end
31
+ end
@@ -12,6 +12,7 @@ module Promiscuous::Subscriber
12
12
  autoload :Polymorphic, 'promiscuous/subscriber/polymorphic'
13
13
  autoload :Upsert, 'promiscuous/subscriber/upsert'
14
14
  autoload :Observer, 'promiscuous/subscriber/observer'
15
+ autoload :Worker, 'promiscuous/subscriber/worker'
15
16
 
16
17
  def self.lint(*args)
17
18
  Lint.lint(*args)
@@ -1,3 +1,3 @@
1
1
  module Promiscuous
2
- VERSION = '0.9.3.1'
2
+ VERSION = '0.10'
3
3
  end
@@ -1,51 +1,15 @@
1
- module Promiscuous
2
- module Worker
3
- mattr_accessor :stop
4
-
5
- def self.replicate
6
- self.stop = false
7
- self.trap_signals unless ENV['TEST_ENV']
8
-
9
- Promiscuous::AMQP.subscribe(subscribe_options) do |metadata, payload|
10
- begin
11
- unless self.stop
12
- Promiscuous.info "[receive] #{payload}"
13
- self.mongoid_wrapper { Promiscuous::Subscriber.process(JSON.parse(payload)) }
14
- metadata.ack
15
- end
16
- rescue Exception => e
17
- e = Promiscuous::Subscriber::Error.new(e, payload)
18
-
19
- self.stop = true
20
- Promiscuous::AMQP.disconnect
21
- Promiscuous.error "[receive] FATAL #{e}"
22
- Promiscuous::Config.error_handler.call(e) if Promiscuous::Config.error_handler
23
- end
24
- end
25
- end
26
-
27
- def self.mongoid_wrapper
28
- if defined?(Mongoid)
29
- Mongoid.unit_of_work { yield }
30
- else
31
- yield
32
- end
33
- end
34
-
35
- def self.trap_signals
36
- %w(SIGTERM SIGINT).each do |signal|
37
- Signal.trap(signal) do
38
- self.stop = true
39
- EM.stop
40
- Promiscuous.info "exiting gracefully"
41
- end
42
- end
43
- end
1
+ module Promiscuous::Worker
2
+ mattr_accessor :workers
3
+ self.workers = []
4
+
5
+ def self.replicate
6
+ self.workers << Promiscuous::Publisher::Worker.new
7
+ self.workers << Promiscuous::Subscriber::Worker.new
8
+ self.workers.each { |w| w.replicate }
9
+ end
44
10
 
45
- def self.subscribe_options
46
- queue_name = "#{Promiscuous::Config.app}.promiscuous"
47
- bindings = Promiscuous::Subscriber::AMQP.subscribers.keys
48
- {:queue_name => queue_name, :bindings => bindings}
49
- end
11
+ def self.stop
12
+ self.workers.each { |w| w.stop = true }
13
+ self.workers.clear
50
14
  end
51
15
  end
data/lib/promiscuous.rb CHANGED
@@ -9,6 +9,7 @@ module Promiscuous
9
9
  autoload :Publisher, 'promiscuous/publisher'
10
10
  autoload :Subscriber, 'promiscuous/subscriber'
11
11
  autoload :Observer, 'promiscuous/observer'
12
+ autoload :Worker, 'promiscuous/worker'
12
13
 
13
14
  class << self
14
15
  def configure(&block)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: promiscuous
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.3.1
4
+ version: '0.10'
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-10-03 00:00:00.000000000 Z
13
+ date: 2012-10-10 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -105,8 +105,8 @@ files:
105
105
  - lib/promiscuous/amqp/rubyamqp.rb
106
106
  - lib/promiscuous/publisher/envelope.rb
107
107
  - lib/promiscuous/publisher/mongoid/embedded.rb
108
+ - lib/promiscuous/publisher/mongoid/defer.rb
108
109
  - lib/promiscuous/publisher/active_record.rb
109
- - lib/promiscuous/publisher/amqp.rb
110
110
  - lib/promiscuous/publisher/attributes.rb
111
111
  - lib/promiscuous/publisher/base.rb
112
112
  - lib/promiscuous/publisher/lint.rb
@@ -115,17 +115,19 @@ files:
115
115
  - lib/promiscuous/publisher/lint/base.rb
116
116
  - lib/promiscuous/publisher/lint/class.rb
117
117
  - lib/promiscuous/publisher/lint/polymorphic.rb
118
- - lib/promiscuous/publisher/mongoid.rb
119
118
  - lib/promiscuous/publisher/polymorphic.rb
120
- - lib/promiscuous/publisher/model.rb
121
119
  - lib/promiscuous/publisher/class.rb
122
120
  - lib/promiscuous/publisher/mock.rb
121
+ - lib/promiscuous/publisher/amqp.rb
122
+ - lib/promiscuous/publisher/error.rb
123
+ - lib/promiscuous/publisher/model.rb
124
+ - lib/promiscuous/publisher/mongoid.rb
125
+ - lib/promiscuous/publisher/worker.rb
123
126
  - lib/promiscuous/railtie/replicate.rake
124
127
  - lib/promiscuous/subscriber/envelope.rb
125
128
  - lib/promiscuous/subscriber/error.rb
126
129
  - lib/promiscuous/subscriber/mongoid/embedded.rb
127
130
  - lib/promiscuous/subscriber/active_record.rb
128
- - lib/promiscuous/subscriber/amqp.rb
129
131
  - lib/promiscuous/subscriber/lint.rb
130
132
  - lib/promiscuous/subscriber/lint/amqp.rb
131
133
  - lib/promiscuous/subscriber/lint/base.rb
@@ -137,23 +139,26 @@ files:
137
139
  - lib/promiscuous/subscriber/upsert.rb
138
140
  - lib/promiscuous/subscriber/model.rb
139
141
  - lib/promiscuous/subscriber/class.rb
140
- - lib/promiscuous/subscriber/polymorphic.rb
141
- - lib/promiscuous/subscriber/attributes.rb
142
142
  - lib/promiscuous/subscriber/observer.rb
143
+ - lib/promiscuous/subscriber/attributes.rb
144
+ - lib/promiscuous/subscriber/amqp.rb
145
+ - lib/promiscuous/subscriber/polymorphic.rb
146
+ - lib/promiscuous/subscriber/worker.rb
143
147
  - lib/promiscuous/config.rb
144
148
  - lib/promiscuous/amqp.rb
145
149
  - lib/promiscuous/common/lint.rb
146
150
  - lib/promiscuous/common/lint/base.rb
147
151
  - lib/promiscuous/common/options.rb
148
152
  - lib/promiscuous/common/class_helpers.rb
153
+ - lib/promiscuous/common/worker.rb
149
154
  - lib/promiscuous/loader.rb
150
- - lib/promiscuous/publisher.rb
151
155
  - lib/promiscuous/railtie.rb
152
- - lib/promiscuous/worker.rb
153
- - lib/promiscuous/common.rb
154
156
  - lib/promiscuous/observer.rb
157
+ - lib/promiscuous/common.rb
158
+ - lib/promiscuous/publisher.rb
155
159
  - lib/promiscuous/subscriber.rb
156
160
  - lib/promiscuous/version.rb
161
+ - lib/promiscuous/worker.rb
157
162
  - lib/promiscuous.rb
158
163
  - README.md
159
164
  homepage: http://github.com/crowdtap/promiscuous