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 +4 -4
- data/README.md +87 -10
- data/lib/multiple_man.rb +1 -0
- data/lib/multiple_man/identity.rb +39 -8
- data/lib/multiple_man/listener.rb +5 -2
- data/lib/multiple_man/model_publisher.rb +1 -1
- data/lib/multiple_man/publish.rb +6 -0
- data/lib/multiple_man/seeder_listener.rb +4 -0
- data/lib/multiple_man/subscribers/model_subscriber.rb +5 -1
- data/lib/multiple_man/version.rb +1 -1
- data/spec/connection_spec.rb +1 -1
- data/spec/identity_spec.rb +35 -11
- data/spec/listener_spec.rb +5 -1
- data/spec/model_publisher_spec.rb +2 -2
- data/spec/subscribers/model_subscriber_spec.rb +15 -2
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 82202196c1530933abce0b808052135a271a9b6b
|
4
|
+
data.tar.gz: 59a804a32f6dcf3810976b983419e6fe3638aaff
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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]
|
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
|
-
- `
|
71
|
-
|
72
|
-
|
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
|
-
|
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
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
data/lib/multiple_man.rb
CHANGED
@@ -1,18 +1,49 @@
|
|
1
1
|
module MultipleMan
|
2
2
|
class Identity
|
3
|
-
def
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
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(
|
54
|
+
connection.queue(queue_name, durable: true, auto_delete: false)
|
52
55
|
end
|
53
56
|
|
54
57
|
def routing_key
|
@@ -25,7 +25,11 @@ module MultipleMan::Subscribers
|
|
25
25
|
private
|
26
26
|
|
27
27
|
def find_model(id)
|
28
|
-
klass.
|
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
|
data/lib/multiple_man/version.rb
CHANGED
data/spec/connection_spec.rb
CHANGED
@@ -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
|
data/spec/identity_spec.rb
CHANGED
@@ -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 "
|
8
|
-
|
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
|
-
|
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
|
data/spec/listener_spec.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
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(:
|
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(:
|
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
|
+
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-
|
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
|