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 +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
|