multiple_man 0.4.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9b69cb35c812dfa3b4814a81a6cdca0aaa495afc
4
- data.tar.gz: ea18e5f3984487b6f7278febc96269612b7038b3
3
+ metadata.gz: 82202196c1530933abce0b808052135a271a9b6b
4
+ data.tar.gz: 59a804a32f6dcf3810976b983419e6fe3638aaff
5
5
  SHA512:
6
- metadata.gz: 8c1b1b58da01c48d575aea0ce54eac633e9996fed67a3f8860396596c721f122cdb92175823b2bb528d403fb7fb05e7748c55c14f1ead348fd32e2f6ce79a19c
7
- data.tar.gz: 93b9be1ffd2e8a8427c35791ad87c76ef4e3a3b4f401cf8150b4a048797af2e0a8c4758826c8aa24b1d3fe120c0c34cc12d3e2b5e04b4ed019fdae1f23e3b3f3
6
+ metadata.gz: a865251110618cc5dfbde0a91eb4b3af2b291333f69e794fbac5bf0f801f64c4bcbafd7d2317cda920f12d60c3dde69125e64aeb52a28a74daf16146a9a6a93e
7
+ data.tar.gz: e507296ff7fb57a709331f23b13a208d5d1a3139f46feb42826033471092724334818e559ac9803e4a21a3165f0a779158ec9923b8184d93f32d2f36108e0c7f
data/README.md CHANGED
@@ -49,27 +49,79 @@ calling MultipleMan.configure like so:
49
49
  # Rails application name if you're using rails
50
50
  config.app_name = "MyApp"
51
51
 
52
+ # Specify what should happen when MultipleMan
53
+ # encounters an exception.
54
+ config.on_error do |exception|
55
+ ErrorLogger.log(exception)
56
+ end
57
+
52
58
  # Where you want to log errors to. Should be an instance of Logger
53
59
  # Defaults to the Rails logger (for Rails) or STDOUT otherwise.
54
60
  config.logger = Logger.new(STDOUT)
55
61
  end
56
62
 
63
+ ### A note on errors
64
+
65
+ It's extremely important to specify the `on_error` setting
66
+ in your configuration. ActiveRecord by default swallows
67
+ exceptions encountered in an `after_commit` block, meaning
68
+ that without handling these errors through the configuration,
69
+ they will be silently ignored.
70
+
57
71
  ### Publishing models
58
72
 
59
- To publish a model, include the following in your model definition:
73
+ #### Directly from the model
74
+
75
+ Include this in your model definition:
60
76
 
61
77
  class Widget < ActiveRecord::Base
62
78
  include MultipleMan::Publisher
63
- publish fields: [:id, :name, :type], identifier: :code
79
+ publish fields: [:id, :name, :type]
64
80
  end
65
81
 
82
+ #### In an initializer / config file
83
+
84
+ Add this to an initializer (i.e. `multiple_man.rb`):
85
+
86
+ ```
87
+ MultipleMan.publish Widget, fields: [:id, :name, :type]
88
+ ```
89
+
66
90
  You can use the following options when publishing:
67
91
 
68
92
  - `fields` - An array of all the fields you want to send to the message queue. These
69
93
  can be either ActiveRecord attributes or methods on your model.
70
- - `identifier` - Either a symbol or a proc used by MultipleMan to identify your model.
71
- `id` is used by default and is generally fine, unless you're working in a multi-tenant
72
- environment where ids may be shared between two different models of the same class.
94
+ - `with` - As an alternative to `fields`, you can specify
95
+ an ActiveRecord serializer (or anything that takes your
96
+ record in the constructor and has an `as_json` method) to
97
+ serialize models instead.
98
+ - `as` - If you want the name of the model from the
99
+ perspective of MultipleMan to be different than the model
100
+ name in Rails, specify `as` with the name you want to use.
101
+ Useful for STI.
102
+ - `identify_by` - Specify an array of fields that MultipleMan
103
+ should use to identify your record on the subscriber end.
104
+ `id` is used by default and is generally fine, unless you're working in a multi-tenant environment where ids may
105
+ be shared between two different models of the same class.
106
+ - (DEPRECATED) `identifier` - Either a symbol or a proc used by MultipleMan to identify your model.
107
+
108
+ ### Publishing
109
+
110
+ By default, MultipleMan will publish all of your models whenever you save a model (in an `after_commit` hook). If you need to manually publish models, you can do so with the `multiple_man_publish` method, which acts like a scope on your models, like so:
111
+
112
+ ```
113
+ # Publish all widgets to MultipleMan
114
+ Widget.multiple_man_publish
115
+
116
+ # Publish a subset of widgets to MultipleMan
117
+ Widget.where(published: true).multiple_man_publish
118
+
119
+ # Publish an individual widget
120
+ Widget.first.multiple_man_publish
121
+ ```
122
+
123
+ If you're publishing multiple models, it's best to use the
124
+ version of multiple_man_publish that operates on a collection. By calling the individual version, a channel is opened and closed for each model, which can impact the thoroughput of MultipleMan.
73
125
 
74
126
  ### Subscribing to models
75
127
 
@@ -77,20 +129,45 @@ You can subscribe to a model as follows (in a seperate consumer app):
77
129
 
78
130
  class Widget < ActiveRecord::Base
79
131
  include MultipleMan::Subscriber
132
+ subscribe fields: [:id, :name]
80
133
  end
81
134
 
82
- Currently, there's a few assumptions made of subscriber models:
135
+ You can pass the following options to the `subscribe` call:
136
+
137
+ - `fields` - Specify which fields you want to receive from
138
+ the publisher. If this is blank, then any field that is published where your subscriber model has a corresponding `field=` method will be subscribed to.
139
+
140
+ By default, MultipleMan will attempt to identify which model on the subscriber matches a model sent by the publisher by id. However, if your publisher specifies an `identify_by` array, MultipleMan will locate your record by finding a record where all of those fields match.
83
141
 
84
- - They need to have all of the fields that are published.
85
- - They need an additional field, called `multiple_man_identifier`, that is the
86
- same type as what's passed from the publisher side (just an integer if you're using
87
- id)
142
+ ### DEPRECATED: multiple_man_identifier
143
+
144
+ If your publisher specifies an `identifier` option, you *must* include a column on the subscriber side called `multiple_man_identifier`. MultipleMan will attempt to locate models on the subscriber side by this column.
145
+
146
+ ## Listening for subscriptions
88
147
 
89
148
  Once you've set up your subscribers, you'll need to run a background worker to manage
90
149
  the subscription process. Just run the following:
91
150
 
92
151
  rake multiple_man:worker
93
152
 
153
+ ## Seeding
154
+
155
+ One common problem when using MultipleMan on an existing project is that there's already a lot of data that you want to process using your listeners. MultipleMan provides a mechanism called "seeding" to accomplish this.
156
+
157
+ 1. On the subscriber side, start listening for seed requests with the following rake task:
158
+
159
+ ```
160
+ rake multiple_man:seed
161
+ ```
162
+
163
+ 2. On the publisher side, indicate that your models should be seeded with the following command:
164
+
165
+ ```
166
+ MyModel.multiple_man_publish(:seed)
167
+ ```
168
+
169
+ 3. Stop the seeder rake task when all of your messages have been processed. You can check your RabbitMQ server
170
+
94
171
  ## Contributing
95
172
 
96
173
  1. Fork it
@@ -17,6 +17,7 @@ module MultipleMan
17
17
  require 'multiple_man/seeder_listener'
18
18
  require 'multiple_man/model_populator'
19
19
  require 'multiple_man/identity'
20
+ require 'multiple_man/publish'
20
21
 
21
22
  def self.logger
22
23
  configuration.logger
@@ -1,18 +1,49 @@
1
1
  module MultipleMan
2
2
  class Identity
3
- def initialize(record, identifier = :id)
3
+ def self.build(record, options)
4
+ if options[:identifier].present?
5
+ SingleField.new(record, options[:identifier])
6
+ else
7
+ MultipleField.new(record, options[:identify_by])
8
+ end
9
+ end
10
+
11
+ def initialize(record)
4
12
  self.record = record
5
- self.identifier = identifier || :id
6
13
  end
7
14
 
8
- def value
9
- if identifier.class == Proc
10
- identifier.call(record).to_s
11
- else
12
- record.send(identifier).to_s
15
+ attr_accessor :record
16
+
17
+ class MultipleField < Identity
18
+ def initialize(record, identify_by)
19
+ self.identify_by = identify_by ? [*identify_by] : [:id]
20
+ super(record)
21
+ end
22
+ def value
23
+ Hash[identify_by.map do |field|
24
+ [field, record.send(field)]
25
+ end]
13
26
  end
27
+
28
+ attr_accessor :identify_by
14
29
  end
15
30
 
16
- attr_accessor :record, :identifier
31
+ class SingleField < Identity
32
+ def initialize(record, identifier = :id)
33
+ MultipleMan.logger.warn("Using :identifier in publish is deprecated, please switch to identify_by.")
34
+ self.identifier = identifier || :id
35
+ super(record)
36
+ end
37
+
38
+ def value
39
+ if identifier.class == Proc
40
+ identifier.call(record).to_s
41
+ else
42
+ record.send(identifier).to_s
43
+ end
44
+ end
45
+
46
+ attr_accessor :identifier
47
+ end
17
48
  end
18
49
  end
@@ -18,6 +18,8 @@ module MultipleMan
18
18
  attr_accessor :connection
19
19
  end
20
20
 
21
+ delegate :queue_name, to: :subscription
22
+
21
23
  def initialize(subscription)
22
24
  self.subscription = subscription
23
25
  end
@@ -26,7 +28,7 @@ module MultipleMan
26
28
 
27
29
  def listen
28
30
  MultipleMan.logger.info "Listening for #{subscription.klass} with routing key #{routing_key}."
29
- queue.bind(connection.topic, routing_key: routing_key).subscribe do |delivery_info, meta_data, payload|
31
+ queue.bind(connection.topic, routing_key: routing_key).subscribe(ack: true) do |delivery_info, meta_data, payload|
30
32
  process_message(delivery_info, payload)
31
33
  end
32
34
  end
@@ -40,6 +42,7 @@ module MultipleMan
40
42
  MultipleMan.error(ex)
41
43
  else
42
44
  MultipleMan.logger.debug " Successfully processed!"
45
+ queue.channel.acknowledge(delivery_info.delivery_tag, false)
43
46
  end
44
47
  end
45
48
 
@@ -48,7 +51,7 @@ module MultipleMan
48
51
  end
49
52
 
50
53
  def queue
51
- connection.queue(subscription.queue_name, durable: true, auto_delete: false)
54
+ connection.queue(queue_name, durable: true, auto_delete: false)
52
55
  end
53
56
 
54
57
  def routing_key
@@ -45,7 +45,7 @@ module MultipleMan
45
45
 
46
46
  def record_data(record)
47
47
  {
48
- id: Identity.new(record, identifier).value,
48
+ id: Identity.build(record, options).value,
49
49
  data: serializer(record).as_json
50
50
  }.to_json
51
51
  end
@@ -0,0 +1,6 @@
1
+ module MultipleMan
2
+ def self.publish(klass, options)
3
+ klass.send(:include, MultipleMan::Publisher)
4
+ klass.publish options
5
+ end
6
+ end
@@ -8,5 +8,9 @@ module MultipleMan
8
8
  def operation(delivery_info)
9
9
  "create"
10
10
  end
11
+
12
+ def queue
13
+ connection.queue(subscription.queue_name + ".seed", auto_delete: true)
14
+ end
11
15
  end
12
16
  end
@@ -25,7 +25,11 @@ module MultipleMan::Subscribers
25
25
  private
26
26
 
27
27
  def find_model(id)
28
- klass.find_or_initialize_by(multiple_man_identifier: id)
28
+ klass.where(find_conditions(id)).first || klass.new
29
+ end
30
+
31
+ def find_conditions(id)
32
+ id.kind_of?(Hash) ? id : {multiple_man_identifier: id}
29
33
  end
30
34
 
31
35
  attr_writer :klass
@@ -1,3 +1,3 @@
1
1
  module MultipleMan
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.1"
3
3
  end
@@ -9,7 +9,7 @@ describe MultipleMan::Connection do
9
9
  it "should open a connection and a channel" do
10
10
  mock_bunny.should_receive(:start)
11
11
  Bunny.should_receive(:new).and_return(mock_bunny)
12
- mock_bunny.should_receive(:create_channel)
12
+ mock_bunny.should_receive(:create_channel).any_number_of_times
13
13
 
14
14
  described_class.connect
15
15
  end
@@ -2,18 +2,42 @@ require 'spec_helper'
2
2
 
3
3
  describe MultipleMan::Identity do
4
4
  let(:record) { double(:model, id: 1, foo: 'foo', bar: 'bar' )}
5
- subject { described_class.new(record, identifier).value }
6
5
 
7
- context "proc identifier" do
8
- let(:identifier) { lambda{|record| "#{record.foo}-#{record.id}" } }
9
- it { should == "foo-1" }
10
- end
11
- context "symbol identifier" do
12
- let(:identifier) { :foo }
13
- it { should == "foo" }
14
- end
15
- context "no identifier" do
6
+ context "with identifier" do
7
+ subject { described_class.build(record, identifier: identifier).value }
16
8
  let(:identifier) { :id }
17
- it { should == "1" }
9
+
10
+ context "proc identifier" do
11
+ let(:identifier) { lambda{|record| "#{record.foo}-#{record.id}" } }
12
+ it { should == "foo-1" }
13
+ end
14
+ context "symbol identifier" do
15
+ let(:identifier) { :foo }
16
+ it { should == "foo" }
17
+ end
18
+ context "id identifier" do
19
+ let(:identifier) { :id }
20
+ it { should == "1" }
21
+ end
22
+ it "should log a deprecation notice" do
23
+ MultipleMan.logger.should_receive(:warn)
24
+ subject
25
+ end
26
+ end
27
+
28
+ context "with identify_by" do
29
+ subject { described_class.build(record, identify_by: identify_by).value }
30
+ context "single field" do
31
+ let(:identify_by) { :foo }
32
+ it { should == { foo: 'foo'} }
33
+ end
34
+ context "no identify_by" do
35
+ let(:identify_by) { nil }
36
+ it { should == { id: 1 } }
37
+ end
38
+ context "multiple_fields" do
39
+ let(:identify_by) { [:foo, :bar] }
40
+ it { should == { foo: 'foo', bar: 'bar' } }
41
+ end
18
42
  end
19
43
  end
@@ -37,8 +37,12 @@ describe MultipleMan::Listener do
37
37
  end
38
38
 
39
39
  specify "process_message should send the correct data" do
40
- subscriber = double(MultipleMan::Subscribers::ModelSubscriber, klass: MockClass1, routing_key: "MockClass1.#")
40
+ connection_stub = double(MultipleMan::Connection).as_null_object
41
+ MultipleMan::Listener.stub(:connection).and_return(connection_stub)
42
+ subscriber = double(MultipleMan::Subscribers::ModelSubscriber, klass: MockClass1, routing_key: "MockClass1.#").as_null_object
41
43
  listener = MultipleMan::Listener.new(subscriber)
44
+
45
+ connection_stub.should_receive(:acknowledge)
42
46
  subscriber.should_receive(:create).with({"a" => 1, "b" => 2})
43
47
  listener.process_message(OpenStruct.new(routing_key: "app.MockClass1.create"), '{"a":1,"b":2}')
44
48
  end
@@ -30,7 +30,7 @@ describe MultipleMan::ModelPublisher do
30
30
  describe "publish" do
31
31
  it "should send the jsonified version of the model to the correct routing key" do
32
32
  MultipleMan::AttributeExtractor.any_instance.should_receive(:as_json).and_return({foo: "bar"})
33
- topic_stub.should_receive(:publish).with('{"id":"10","data":{"foo":"bar"}}', routing_key: "app.MockObject.create")
33
+ topic_stub.should_receive(:publish).with('{"id":{"id":10},"data":{"foo":"bar"}}', routing_key: "app.MockObject.create")
34
34
  described_class.new(fields: [:foo]).publish(MockObject.new)
35
35
  end
36
36
 
@@ -55,7 +55,7 @@ describe MultipleMan::ModelPublisher do
55
55
 
56
56
  it "should get its data from the serializer" do
57
57
  obj = MockObject.new
58
- topic_stub.should_receive(:publish).with('{"id":"10","data":{"a":"yes"}}', routing_key: "app.MockObject.create")
58
+ topic_stub.should_receive(:publish).with('{"id":{"id":10},"data":{"a":"yes"}}', routing_key: "app.MockObject.create")
59
59
  subject.publish(obj)
60
60
  end
61
61
  end
@@ -8,7 +8,7 @@ describe MultipleMan::Subscribers::ModelSubscriber do
8
8
  describe "create" do
9
9
  it "should create a new model" do
10
10
  mock_object = MockClass.new
11
- MockClass.stub(:find_or_initialize_by).with(multiple_man_identifier: 5).and_return(mock_object)
11
+ MockClass.stub(:where).and_return([mock_object])
12
12
  mock_populator = double(MultipleMan::ModelPopulator)
13
13
  MultipleMan::ModelPopulator.should_receive(:new).and_return(mock_populator)
14
14
  mock_populator.should_receive(:populate).with({a: 1, b: 2})
@@ -18,10 +18,23 @@ describe MultipleMan::Subscribers::ModelSubscriber do
18
18
  end
19
19
  end
20
20
 
21
+ describe "find_model" do
22
+ it "should find by multiple_man_identifier for a single field" do
23
+ mock_object = double(MockClass).as_null_object
24
+ MockClass.should_receive(:where).with(multiple_man_identifier: 5).and_return([mock_object])
25
+ described_class.new(MockClass, {}).create({id: 5, data:{a: 1, b: 2}})
26
+ end
27
+ it "should find by the hash for multiple fields" do
28
+ mock_object = double(MockClass).as_null_object
29
+ MockClass.should_receive(:where).with(foo: 'bar').and_return([mock_object])
30
+ described_class.new(MockClass, {}).create({id: {foo: 'bar'}, data:{a: 1, b: 2}})
31
+ end
32
+ end
33
+
21
34
  describe "destroy" do
22
35
  it "should destroy the model" do
23
36
  mock_object = MockClass.new
24
- MockClass.should_receive(:find_or_initialize_by).and_return(mock_object)
37
+ MockClass.should_receive(:where).and_return([mock_object])
25
38
  mock_object.should_receive(:destroy!)
26
39
 
27
40
  described_class.new(MockClass, {}).destroy({id: 1})
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: multiple_man
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Brunner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-03-21 00:00:00.000000000 Z
11
+ date: 2014-04-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -117,6 +117,7 @@ files:
117
117
  - lib/multiple_man/mixins/subscriber.rb
118
118
  - lib/multiple_man/model_populator.rb
119
119
  - lib/multiple_man/model_publisher.rb
120
+ - lib/multiple_man/publish.rb
120
121
  - lib/multiple_man/railtie.rb
121
122
  - lib/multiple_man/routing_key.rb
122
123
  - lib/multiple_man/seeder_listener.rb