multiple_man 0.4.0 → 0.5.1

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