brown 2.2.0 → 2.2.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 +166 -0
- data/bin/brown +23 -17
- data/lib/brown/agent.rb +2 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 80d8b8839069b1aef10dc6a78f8d300b7f0d0191
|
|
4
|
+
data.tar.gz: 76552fb92abda3c6e4362fb8bcc12f2a602c8e14
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 71edb2cda3ba54528d608bc5032e008d501bfd79de97cbb168516ca407c1086ae46fc2a9d21645d6ba5346a03e1534a32261a913338f1326f238a481b1d33b50
|
|
7
|
+
data.tar.gz: e1bc450d4ab6eeb14aae29184676481598c25a43b949b596f28e279a462d6f0924da00cdd26897432fec6cf81692b977fb6af5eb5970994098e82aaa2cc1410b
|
data/README.md
CHANGED
|
@@ -90,6 +90,172 @@ You can pass arguments to the agent method call, by giving them to
|
|
|
90
90
|
`worker.call`.
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
## Agent-wide common variables
|
|
94
|
+
|
|
95
|
+
There is some state you will want to keep across the entire agent. For
|
|
96
|
+
this, Brown provides the concept of "memos". These are persistent objects,
|
|
97
|
+
which you access via a class or instance method. To declare them, you
|
|
98
|
+
simply do:
|
|
99
|
+
|
|
100
|
+
class MemoUser < Brown::Agent
|
|
101
|
+
memo(:foo) { Foo.new }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
The way this works is that the memo defines a method (both class and
|
|
105
|
+
instance) which, the first time you run it, runs the provided block to
|
|
106
|
+
create the memo object. Thereafter, that cached object is provided to the
|
|
107
|
+
caller of the memo method.
|
|
108
|
+
|
|
109
|
+
Because of Brown's multi-threaded nature, memos come with a built-in mutex
|
|
110
|
+
to prevent concurrent usage. That means that every time you want to access
|
|
111
|
+
the memo, you must do so inside a block:
|
|
112
|
+
|
|
113
|
+
class MemoUser < Brown::Agent
|
|
114
|
+
memo(:foo) { Foo.new }
|
|
115
|
+
|
|
116
|
+
every(10) do
|
|
117
|
+
foo do |f|
|
|
118
|
+
f.frob
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
The crucial thing to note here is that you *only have the memo lock inside
|
|
124
|
+
the block*. If you were to capture the memo object into a variable outside
|
|
125
|
+
the block, and then use it (read *or* write) outside the block, Really Bad
|
|
126
|
+
Things can happen. So **don't do that**.
|
|
127
|
+
|
|
128
|
+
When you have multiple memos, it is entirely possible that you can end up
|
|
129
|
+
deadlocking your agent by acquiring the locks for various memos in different
|
|
130
|
+
orders. Those dining philosophers are always getting themselves in a
|
|
131
|
+
muddle. To prevent this problem, it is highly recommended that you always
|
|
132
|
+
acquire the locks for your memos in the order they are written in the class
|
|
133
|
+
definition:
|
|
134
|
+
|
|
135
|
+
class MemoUser < Brown::Agent
|
|
136
|
+
memo(:foo) { Foo.new }
|
|
137
|
+
memo(:bar) { Bar.new }
|
|
138
|
+
|
|
139
|
+
every(5) do
|
|
140
|
+
# Acquiring a single lock is OK
|
|
141
|
+
foo do |f|
|
|
142
|
+
f.brob
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
every(6) do
|
|
147
|
+
# Acquiring a single lock, even if it is later in the
|
|
148
|
+
# list, is fine
|
|
149
|
+
bar do |b|
|
|
150
|
+
b.baznicate(b)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
every(7) do
|
|
155
|
+
# This is the right order to acquire nested locks
|
|
156
|
+
foo do |f|
|
|
157
|
+
bar do |b|
|
|
158
|
+
f.frob(b)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
every(7) do
|
|
164
|
+
# This is THE WRONG WAY AROUND. DO NOT DO THIS!
|
|
165
|
+
# YOU WILL GET DEADLOCKS!
|
|
166
|
+
bar do |b|
|
|
167
|
+
foo do |f|
|
|
168
|
+
b.baznicate(f)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
Another "gotcha" in the world of memos is that the memoised object itself is
|
|
175
|
+
persistent. When you get the lock, and the memo object comes in via the
|
|
176
|
+
argument to your block, that is *a reference* to the memo object. That
|
|
177
|
+
means that reassigning that variable to a new object won't change the value
|
|
178
|
+
of the memo object:
|
|
179
|
+
|
|
180
|
+
class MemoUser < Brown::Agent
|
|
181
|
+
memo(:now) { Time.now }
|
|
182
|
+
|
|
183
|
+
every(5) do
|
|
184
|
+
now do |t|
|
|
185
|
+
puts t
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
every(60) do
|
|
190
|
+
now do |t|
|
|
191
|
+
t = Time.now
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
The above code will *always* print the time as at the first time that `now`
|
|
197
|
+
was called, even though every minute we *think* we're resetting the memo
|
|
198
|
+
value to a new `Time`.
|
|
199
|
+
|
|
200
|
+
The "hack" around this is to use a single-value array to "contain" the
|
|
201
|
+
object that we actually want to periodically replace:
|
|
202
|
+
|
|
203
|
+
class MutableMemoUser < Brown::Agent
|
|
204
|
+
memo(:now) { [Time.now] }
|
|
205
|
+
|
|
206
|
+
every(5) do
|
|
207
|
+
now do |t|
|
|
208
|
+
puts t[0]
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
every(60) do
|
|
213
|
+
now to |t|
|
|
214
|
+
t[0] = Time.now
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
This example code will print the same time for a minute, before changing to
|
|
220
|
+
a new minute.
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
### Thread-safe memos
|
|
224
|
+
|
|
225
|
+
There are some classes which are themselves thread-safe -- usually because
|
|
226
|
+
the class author has gone to some trouble to provide zer own, more
|
|
227
|
+
fine-grained, locking on the data within the object. If you are *quite
|
|
228
|
+
sure* you have such a thread-safe object to memoise, you can use
|
|
229
|
+
{Brown::Agent::ClassMethods.safe_memo} for that purpose:
|
|
230
|
+
|
|
231
|
+
class SafeMemoUser < Brown::Agent
|
|
232
|
+
safe_memo(:foo) { ThreadSafeFoo.new }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
The benefit of this form of memos is that you don't have to access them in a
|
|
236
|
+
block:
|
|
237
|
+
|
|
238
|
+
class SafeMemoUser < Brown::Agent
|
|
239
|
+
safe_memo(:foo) { ThreadSafeFoo.new }
|
|
240
|
+
|
|
241
|
+
every(10) do
|
|
242
|
+
foo.frob
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
Like regular memos, you cannot reassign a thread-safe memo to another
|
|
247
|
+
object:
|
|
248
|
+
|
|
249
|
+
class SafeMemoUser < Brown::Agent
|
|
250
|
+
safe_memo(:foo) { ThreadSafeFoo.new }
|
|
251
|
+
|
|
252
|
+
every(10) do
|
|
253
|
+
# This will explode with a NameError
|
|
254
|
+
foo = ThreadSafeFoo.new
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
|
|
93
259
|
## AMQP publishing / consumption
|
|
94
260
|
|
|
95
261
|
Since message-based communication is a common pattern amongst cooperating
|
data/bin/brown
CHANGED
|
@@ -23,10 +23,10 @@ Brown::Agent.logger = Logger.new($stderr)
|
|
|
23
23
|
Brown::Agent.logger.level = Logger.const_get(ENVied.BROWN_LOG_LEVEL.upcase.to_sym)
|
|
24
24
|
Brown::Agent.logger.formatter = proc { |s,dt,n,msg| "#{$$}-#{Thread.current.object_id} [#{s[0]}] #{msg}\n" }
|
|
25
25
|
|
|
26
|
-
agents =
|
|
26
|
+
agents = []
|
|
27
27
|
|
|
28
28
|
def stop_agents(agents)
|
|
29
|
-
agents.
|
|
29
|
+
agents.each do |th|
|
|
30
30
|
th[:agent_class] && th[:agent_class].stop
|
|
31
31
|
end
|
|
32
32
|
end
|
|
@@ -53,39 +53,45 @@ Brown::Agent.logger.info { "Brown starting up..." }
|
|
|
53
53
|
|
|
54
54
|
agent_classes.each do |klass|
|
|
55
55
|
th = Thread.new(klass) do |klass|
|
|
56
|
+
Thread.current[:agent_class] = klass
|
|
56
57
|
klass.run
|
|
57
|
-
Thread[:agent_class] = klass
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
agents
|
|
60
|
+
agents << th
|
|
61
61
|
|
|
62
62
|
Brown::Agent.logger.info { "Started agent #{klass}" }
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
loop do
|
|
66
|
-
|
|
66
|
+
if agents.empty?
|
|
67
|
+
break
|
|
68
|
+
end
|
|
67
69
|
|
|
68
|
-
agents.
|
|
70
|
+
agents.each do |th|
|
|
69
71
|
unless th.alive?
|
|
70
72
|
begin
|
|
71
|
-
th.join
|
|
73
|
+
th.join(0.5)
|
|
72
74
|
rescue Exception => ex
|
|
73
75
|
Brown::Agent.logger.fatal { "Agent #{th[:agent_class]} crashed: #{ex.message} (#{ex.class})" }
|
|
74
76
|
Brown::Agent.logger.info { ex.backtrace.map { |l| " #{l}" }.join("\n") }
|
|
75
|
-
else
|
|
76
|
-
Brown::Agent.logger.warn "Agent #{th[:agent_class]} terminated itself"
|
|
77
|
-
end
|
|
78
|
-
klass = th[:agent_class]
|
|
79
|
-
th[:agent_class] = nil
|
|
80
77
|
|
|
81
|
-
|
|
78
|
+
Brown::Agent.logger.info { "Re-starting #{klass} agent" }
|
|
82
79
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
new_th = Thread.new(klass) do |klass|
|
|
81
|
+
Thread.current[:agent_class] = klass
|
|
82
|
+
klass.run
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
agents << new_th
|
|
86
|
+
else
|
|
87
|
+
Brown::Agent.logger.warn "Agent #{th[:agent_class]} exited cleanly; not restarting"
|
|
86
88
|
end
|
|
87
89
|
|
|
88
|
-
|
|
90
|
+
klass = th[:agent_class]
|
|
91
|
+
th[:agent_class] = nil
|
|
92
|
+
agents.delete(th)
|
|
89
93
|
end
|
|
90
94
|
end
|
|
91
95
|
end
|
|
96
|
+
|
|
97
|
+
Brown::Agent.logger.info { "Brown exiting" }
|
data/lib/brown/agent.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: brown
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.2.
|
|
4
|
+
version: 2.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matt Palmer
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2015-06-
|
|
11
|
+
date: 2015-06-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bunny
|