furnish 0.0.4 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +36 -0
- data/Gemfile +0 -2
- data/Guardfile +2 -2
- data/furnish.gemspec +3 -2
- data/lib/furnish.rb +2 -4
- data/lib/furnish/protocol.rb +226 -0
- data/lib/furnish/provisioner.rb +1 -38
- data/lib/furnish/provisioner_group.rb +254 -38
- data/lib/furnish/provisioners/api.rb +430 -0
- data/lib/furnish/provisioners/dummy.rb +19 -12
- data/lib/furnish/scheduler.rb +148 -25
- data/lib/furnish/version.rb +1 -1
- data/lib/furnish/vm.rb +3 -0
- data/test/dummy_classes.rb +275 -0
- data/test/mt_cases.rb +103 -45
- data/test/test_api.rb +68 -0
- data/test/test_dummy.rb +6 -6
- data/test/test_protocol.rb +211 -0
- data/test/test_provisioner_group.rb +163 -11
- data/test/test_scheduler_basic.rb +57 -4
- data/test/test_scheduler_threaded.rb +5 -17
- metadata +18 -13
@@ -0,0 +1,430 @@
|
|
1
|
+
require 'furnish/provisioner'
|
2
|
+
require 'furnish/protocol'
|
3
|
+
|
4
|
+
module Furnish # :nodoc:
|
5
|
+
module Provisioner # :nodoc:
|
6
|
+
#
|
7
|
+
# API base class for furnish provisioners. Ideally, you will want to inherit
|
8
|
+
# from this and supply your overrides. Nothing in furnish expects this
|
9
|
+
# class to be the base class of your provisioners, this just lets you save
|
10
|
+
# some trouble. That said, all methods in this class are expected to be
|
11
|
+
# implemented by your provisioner if you choose not to use it.
|
12
|
+
#
|
13
|
+
# The method documentation here also declares expectations on how
|
14
|
+
# provisioners are expected to operate, so even if you don't use this
|
15
|
+
# class, reading the documentation and knowing what's expected are
|
16
|
+
# essential to writing a working provisioner.
|
17
|
+
#
|
18
|
+
# Note that all Provisioners *must* be capable of being marshalled with
|
19
|
+
# Ruby's Marshal library. If they are unable to do this, many parts of
|
20
|
+
# Furnish will not work. There are a few things that break Marshal, such as
|
21
|
+
# proc/lambda in instance variables, usage of the Singleton module, and
|
22
|
+
# singleton class manipulation. Marshal has tooling to work around this if
|
23
|
+
# you absolutely need it; see Marshal's documentation for more information.
|
24
|
+
# Additionally, There are less important (but worth knowing) performance
|
25
|
+
# complications around extend and refinements that will not break anything,
|
26
|
+
# but should be noted as well.
|
27
|
+
#
|
28
|
+
# This class provides some basic boilerplate for:
|
29
|
+
#
|
30
|
+
# * initializer/constructor usage (see API.new)
|
31
|
+
# * property management / querying (see API.furnish_property)
|
32
|
+
# * #furnish_group_name (see Furnish::ProvisionerGroup) usage
|
33
|
+
# * Implementations of Furnish::Protocol for #startup and #shutdown (see
|
34
|
+
# API.configure_startup and API.configure_shutdown)
|
35
|
+
# * standard #report output
|
36
|
+
# * #to_s and #inspect for various ruby functions
|
37
|
+
#
|
38
|
+
# Additionally, "abstract" methods have been defined for provisioner
|
39
|
+
# control methods:
|
40
|
+
#
|
41
|
+
# * #startup
|
42
|
+
# * #shutdown
|
43
|
+
#
|
44
|
+
# Which will raise unless implemented by your subclass.
|
45
|
+
#
|
46
|
+
# Both parameters and return values are expected to be normal for these
|
47
|
+
# methods. Return base types are in parens next to the method name. Please
|
48
|
+
# see the methods themselves for parameter information.
|
49
|
+
#
|
50
|
+
# * #startup (Hash or false)
|
51
|
+
# * #shutdown (Hash or false)
|
52
|
+
# * #report
|
53
|
+
#
|
54
|
+
# And it would be wise to read the documentation on how those should be
|
55
|
+
# written.
|
56
|
+
#
|
57
|
+
class API
|
58
|
+
|
59
|
+
#
|
60
|
+
# Ensures all properties are inherited by subclasses that inherit from API.
|
61
|
+
#
|
62
|
+
def self.inherited(inheriting)
|
63
|
+
[
|
64
|
+
:@furnish_properties,
|
65
|
+
:@startup_protocol,
|
66
|
+
:@shutdown_protocol
|
67
|
+
].each do |prop|
|
68
|
+
inheriting.instance_variable_set(prop, (instance_variable_get(prop).dup rescue nil))
|
69
|
+
end
|
70
|
+
|
71
|
+
inheriting.instance_variable_set(:@allows_recovery, instance_variable_get(:@allows_recovery))
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# The set of furnish properties for this class. Returns a hash which is
|
76
|
+
# keyed with symbols representing the property name, and the value itself
|
77
|
+
# is a hash which contains two additional key/value combinations, `:type`
|
78
|
+
# and `:description`, e.g.:
|
79
|
+
#
|
80
|
+
# {
|
81
|
+
# :my_property => {
|
82
|
+
# :description => "some description",
|
83
|
+
# :type => Object
|
84
|
+
# }
|
85
|
+
# }
|
86
|
+
#
|
87
|
+
# See API.furnish_property for more information.
|
88
|
+
#
|
89
|
+
def self.furnish_properties
|
90
|
+
@furnish_properties ||= { }
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# Configure a furnish property. Used by the standard initializer on
|
95
|
+
# API.new for parameter validation, and provides a queryable interface
|
96
|
+
# for external consumers via API.furnish_properties, which will be
|
97
|
+
# exposed as a class method for your provisioner class.
|
98
|
+
#
|
99
|
+
# The name is a symbol or will be converted to one, and will generate an
|
100
|
+
# accessor for instances of this class. No attempt is made to type check
|
101
|
+
# accessor writes outside of the constructor.
|
102
|
+
#
|
103
|
+
# The description is a string which describes what the property controls.
|
104
|
+
# It is unused by Furnish but exists to allow external consumers the
|
105
|
+
# ability to expose this to third parties. The default is an empty
|
106
|
+
# string.
|
107
|
+
#
|
108
|
+
# The type is a class name (default Object) used for parameter checking
|
109
|
+
# in API.new's initializer. If the value provided during construction is
|
110
|
+
# not a kind of this class, an ArgumentError will be raised. No attempt
|
111
|
+
# is made to deal with inner collection types ala Generics.
|
112
|
+
#
|
113
|
+
# Example:
|
114
|
+
#
|
115
|
+
# class MyProv < API
|
116
|
+
# furnish_property :foo, "does a foo", Integer
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# obj = MyProv.new(:bar => 1) # raises, no property
|
120
|
+
# obj = MyProv.new(:foo => "string") # raises, invalid type
|
121
|
+
# obj = MyProv.new(:foo => 1) # succeeds
|
122
|
+
#
|
123
|
+
# obj.foo == 1
|
124
|
+
#
|
125
|
+
# MyProv.furnish_properties[:foo] ==
|
126
|
+
# { :description => "does a foo", :type => Integer }
|
127
|
+
#
|
128
|
+
def self.furnish_property(name, description="", type=Object)
|
129
|
+
name = name.to_sym unless name.kind_of?(Symbol)
|
130
|
+
|
131
|
+
attr_accessor name
|
132
|
+
|
133
|
+
furnish_properties[name] = {
|
134
|
+
:description => description,
|
135
|
+
:type => type
|
136
|
+
}
|
137
|
+
end
|
138
|
+
|
139
|
+
#
|
140
|
+
# Indicate whether or not this Provisioner allows recovery functions.
|
141
|
+
# Will be used in recovery mode to determine whether or not to
|
142
|
+
# automatically deprovision the group, or attempt to recover the group
|
143
|
+
# provision.
|
144
|
+
#
|
145
|
+
# Recovery (the feature) is defaulted to false, but calling this method
|
146
|
+
# with no arguments will turn it on (i.e., set it to true). You may also
|
147
|
+
# provide a boolean argument if you wish to turn it off.
|
148
|
+
#
|
149
|
+
# Note that if you turn this on, you must also define a #recover state
|
150
|
+
# method which implements your recovery routines. If you turn it on and
|
151
|
+
# do not define the #recover routine, NotImplementedError will be raised
|
152
|
+
# during recovery.
|
153
|
+
#
|
154
|
+
# Usage:
|
155
|
+
#
|
156
|
+
# # turn it on
|
157
|
+
# class MyProv < API
|
158
|
+
# allows_recovery
|
159
|
+
# end
|
160
|
+
#
|
161
|
+
# # turn it off explicitly
|
162
|
+
# class MyProv < API
|
163
|
+
# allows_recovery false
|
164
|
+
# end
|
165
|
+
#
|
166
|
+
# # not specifying means it's off.
|
167
|
+
# class MyProv < API
|
168
|
+
# ...
|
169
|
+
# end
|
170
|
+
#
|
171
|
+
def self.allows_recovery(val=true)
|
172
|
+
@allows_recovery = val
|
173
|
+
end
|
174
|
+
|
175
|
+
#
|
176
|
+
# Predicate to determine if this provisioner supports recovery or not.
|
177
|
+
#
|
178
|
+
# Please see API.allows_recovery and #recover for more information.
|
179
|
+
#
|
180
|
+
def self.allows_recovery?
|
181
|
+
@allows_recovery ||= false
|
182
|
+
@allows_recovery
|
183
|
+
end
|
184
|
+
|
185
|
+
#
|
186
|
+
# This contains the Furnish::Protocol configuration for startup (aka
|
187
|
+
# provisioning) state execution. See API.configure_startup for more
|
188
|
+
# information.
|
189
|
+
#
|
190
|
+
def self.startup_protocol
|
191
|
+
@startup_protocol ||= Furnish::Protocol.new
|
192
|
+
end
|
193
|
+
|
194
|
+
#
|
195
|
+
# This contains the Furnish::Protocol configuration for shutdown (aka
|
196
|
+
# provisioning) state execution. See API.configure_shutdown for more
|
197
|
+
# information.
|
198
|
+
#
|
199
|
+
def self.shutdown_protocol
|
200
|
+
@shutdown_protocol ||= Furnish::Protocol.new
|
201
|
+
end
|
202
|
+
|
203
|
+
#
|
204
|
+
# configure the Furnish::Protocol for startup state execution. This
|
205
|
+
# allows you to define constraints for your provisioner that are used at
|
206
|
+
# scheduling time to determine whether or not the ProvisionerGroup will
|
207
|
+
# be able to finish its provision.
|
208
|
+
#
|
209
|
+
# It's a bit like run-time type inference for a full provision; it just
|
210
|
+
# tries to figure out if it'll break before it runs.
|
211
|
+
#
|
212
|
+
# The block provided will be instance_eval'd over a Furnish::Protocol
|
213
|
+
# object. You can use methods like Furnish::Protocol#accepts_from_any,
|
214
|
+
# Furnish::Protocol#requires, Furnish::Protocol#accepts,
|
215
|
+
# Furnish::Protocol#yields to describe what it'll pass on to the next
|
216
|
+
# provisioner or expects from the one coming before it.
|
217
|
+
#
|
218
|
+
# Example:
|
219
|
+
#
|
220
|
+
# class MyProv < API
|
221
|
+
# configure_startup do
|
222
|
+
# requires :ip_address, "the IP address returned by the last provision", String
|
223
|
+
# accepts :network, "A CIDR network used by the last provision", String
|
224
|
+
# yields :port, "A TCP port for the service allocated by this provisioner", Integer
|
225
|
+
# end
|
226
|
+
# end
|
227
|
+
#
|
228
|
+
# This means:
|
229
|
+
#
|
230
|
+
# * An IP address has to come from the previous provisioner named "ip_address".
|
231
|
+
# * If a network CIDR was supplied, it will be used.
|
232
|
+
# * This provision will provide a TCP port number for whatever it makes,
|
233
|
+
# which the next provisioner can work with.
|
234
|
+
#
|
235
|
+
# See Furnish::Protocol for more information.
|
236
|
+
#
|
237
|
+
def self.configure_startup(&block)
|
238
|
+
startup_protocol.configure(&block)
|
239
|
+
end
|
240
|
+
|
241
|
+
#
|
242
|
+
# This is exactly like API.configure_startup, but for the shutdown
|
243
|
+
# process. Do note that shutdown runs the provisioners backwards, meaning
|
244
|
+
# the validation process also follows suit.
|
245
|
+
#
|
246
|
+
def self.configure_shutdown(&block)
|
247
|
+
shutdown_protocol.configure(&block)
|
248
|
+
end
|
249
|
+
|
250
|
+
##
|
251
|
+
# The furnish_group_name is set by Furnish::ProvisionerGroup when
|
252
|
+
# scheduling is requested via Furnish::Scheduler. It is a hint to the
|
253
|
+
# provisioner as to what the name of the group it's in is, which can be
|
254
|
+
# used to persist data, name things in a unique way, etc.
|
255
|
+
#
|
256
|
+
# Note that furnish_group_name will be nil until the
|
257
|
+
# Furnish::ProvisionerGroup is constructed. This means that it will not
|
258
|
+
# be set when #initialize runs, or any time before it joins the group. It
|
259
|
+
# is wise to only rely on this value being set when #startup or #shutdown
|
260
|
+
# execute.
|
261
|
+
attr_accessor :furnish_group_name
|
262
|
+
|
263
|
+
#
|
264
|
+
# Default constructor. If given arguments, must be of type Hash, keys are
|
265
|
+
# the name of furnish properties. Raises ArgumentError if no furnish
|
266
|
+
# property exists, or the type of the value provided is not a kind of
|
267
|
+
# type specified in the property. See API.furnish_property for more
|
268
|
+
# information.
|
269
|
+
#
|
270
|
+
# Does nothing more, not required anywhere in furnish itself -- you may
|
271
|
+
# redefine this constructor and work against completely differently input
|
272
|
+
# and behavior, or call this as a superclass initializer and then do your
|
273
|
+
# work.
|
274
|
+
#
|
275
|
+
def initialize(args={})
|
276
|
+
unless args.kind_of?(Hash)
|
277
|
+
raise ArgumentError, "Arguments must be a kind of hash"
|
278
|
+
end
|
279
|
+
|
280
|
+
args.each do |k, v|
|
281
|
+
props = self.class.furnish_properties
|
282
|
+
|
283
|
+
if props.has_key?(k)
|
284
|
+
if v.kind_of?(props[k][:type])
|
285
|
+
send("#{k}=", v)
|
286
|
+
else
|
287
|
+
raise ArgumentError, "Value for furnish property #{k} on #{self.class.name} does not match type #{props[k][:type]}"
|
288
|
+
end
|
289
|
+
else
|
290
|
+
raise ArgumentError, "Invalid argument #{k}, not a furnish property for #{self.class.name}"
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
#
|
296
|
+
# Called by Furnish::ProvisionerGroup#startup which is itself called by
|
297
|
+
# Furnish::Scheduler#startup. Indicates the resource this provisioner
|
298
|
+
# manages is to be created.
|
299
|
+
#
|
300
|
+
# Arguments will come from the return values of the previous
|
301
|
+
# provisioner's startup or an empty Hash if this is the first
|
302
|
+
# provisioner. Return value is expected to be false if the provision
|
303
|
+
# failed in a non-exceptional way, or a hash of values for the next
|
304
|
+
# provisioner if successful. See API.startup_protocol for how you can
|
305
|
+
# validate what you accept and yield for parameters will always work.
|
306
|
+
#
|
307
|
+
# The routine in this base class will raise NotImplementedError,
|
308
|
+
# expecting you to override it in your provisioner.
|
309
|
+
#
|
310
|
+
def startup(args={})
|
311
|
+
raise NotImplementedError, "startup method not implemented for #{self.class.name}"
|
312
|
+
end
|
313
|
+
|
314
|
+
#
|
315
|
+
# called by Furnish::ProvisionerGroup#shutdown which is itself called by
|
316
|
+
# Furnish::Scheduler#shutdown. Indicates the resource this provisioner
|
317
|
+
# manages is to be destroyed.
|
318
|
+
#
|
319
|
+
# Arguments are exactly the same as #startup. Note that provisioners run
|
320
|
+
# in reverse order when executing shutdown methods. See
|
321
|
+
# API.shutdown_protocol for information on how to validate incoming
|
322
|
+
# parameters and how to declare what you yield.
|
323
|
+
#
|
324
|
+
# The routine in this base class will raise NotImplementedError,
|
325
|
+
# expecting you to override it in your provisioner.
|
326
|
+
#
|
327
|
+
def shutdown(args={})
|
328
|
+
raise NotImplementedError, "shutdown method not implemented for #{self.class.name}"
|
329
|
+
end
|
330
|
+
|
331
|
+
#
|
332
|
+
# Initiate recovery. This is an optional state transition, and if
|
333
|
+
# unavailable will assume recovery is not possible. Inheriting from this
|
334
|
+
# class provides this and therefore will always be available. See
|
335
|
+
# API.allows_recovery for information on how to control this feature in
|
336
|
+
# your provisioner. If recovery is possible in your provisioner and you
|
337
|
+
# have not defined a working recover method of your own,
|
338
|
+
# NotImplementedError will be raised.
|
339
|
+
#
|
340
|
+
# recover takes two arguments, the desired state and the arguments passed
|
341
|
+
# during the initial attempt at state transition: for example, `:startup`
|
342
|
+
# and a hash that conforms to Furnish::Protocol definitions.
|
343
|
+
#
|
344
|
+
# recover is expected to return true if recovery was successful, and
|
345
|
+
# false if it was not. If successful, the original state will be invoked
|
346
|
+
# with its original arguments, just like it was receiving the transition
|
347
|
+
# for the first time. Therefore, for recover to be successful, it should
|
348
|
+
# clean up any work the state has already done.
|
349
|
+
#
|
350
|
+
# See Furnish::Scheduler#recover for information on how to deal with
|
351
|
+
# recovery that will not succeed.
|
352
|
+
#
|
353
|
+
# Example: a provisioner for a security group crashes during running
|
354
|
+
# startup. The scheduler marks that group as needing recovery, and marks
|
355
|
+
# the scheduler in general as needing recovery.
|
356
|
+
# Furnish::Scheduler#recover is called, which locates our group and calls
|
357
|
+
# its recover routine, which delegates to your provisioner. Your
|
358
|
+
# provisioner then cleans up the security group attempted to be created
|
359
|
+
# (if it does not exist, that's fine too). Then, it returns true. Then
|
360
|
+
# the scheduler will retry the startup routine for your security group
|
361
|
+
# provisioner, which will attempt the same thing as if it has never tried
|
362
|
+
# to begin with.
|
363
|
+
#
|
364
|
+
def recover(state, args)
|
365
|
+
if self.class.allows_recovery?
|
366
|
+
raise NotImplementedError, "#{self.class} allows recovery but no #recover method was defined."
|
367
|
+
else
|
368
|
+
return false
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
#
|
373
|
+
# returns an array of strings with some high-level information about the
|
374
|
+
# provision. Intended for UI tools that need to query a provisioner group
|
375
|
+
# about the resources it manages.
|
376
|
+
#
|
377
|
+
# Default is to return the provisioner group name or "unknown" if it is
|
378
|
+
# not set yet.
|
379
|
+
#
|
380
|
+
def report
|
381
|
+
[furnish_group_name || "unknown"]
|
382
|
+
end
|
383
|
+
|
384
|
+
#
|
385
|
+
# Used by various logging pieces throughout furnish and Ruby itself.
|
386
|
+
#
|
387
|
+
# Default is to return "group name[provisioner class name]" where group
|
388
|
+
# name will be "unknown" if not set.
|
389
|
+
#
|
390
|
+
# For example, in a group called 'test1', and the provisioner class is
|
391
|
+
# Furnish::Provisioner::EC2:
|
392
|
+
#
|
393
|
+
# "test1[Furnish::Provisioner::EC2]"
|
394
|
+
#
|
395
|
+
def to_s
|
396
|
+
name = furnish_group_name || "unknown"
|
397
|
+
"#{name}[#{self.class.name}]"
|
398
|
+
end
|
399
|
+
|
400
|
+
alias inspect to_s # FIXME make this less stupid later
|
401
|
+
|
402
|
+
#
|
403
|
+
# Assists with equality checks. See #hash for how this is done.
|
404
|
+
#
|
405
|
+
def ==(other)
|
406
|
+
self.hash == other.hash
|
407
|
+
end
|
408
|
+
|
409
|
+
#
|
410
|
+
# Computes something suitable for storing in hash tables, also used for
|
411
|
+
# equality (see #==).
|
412
|
+
#
|
413
|
+
# Accomplishes this by collecting all instance variables in an array,
|
414
|
+
# converting strings to a common encoding if necessary. Then, appends the
|
415
|
+
# class and Marshals the contents of the array.
|
416
|
+
#
|
417
|
+
def hash
|
418
|
+
Marshal.dump(
|
419
|
+
instance_variables.sort.map do |x|
|
420
|
+
y = instance_variable_get(x)
|
421
|
+
y.kind_of?(String) ?
|
422
|
+
y.encode("UTF-8", :invalid => :replace, :replace => "?".chr) :
|
423
|
+
y
|
424
|
+
end +
|
425
|
+
[self.class]
|
426
|
+
)
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
@@ -7,8 +7,7 @@ module Furnish
|
|
7
7
|
# In short, unless you're writing tests you should probably never use this
|
8
8
|
# code.
|
9
9
|
#
|
10
|
-
class Dummy
|
11
|
-
|
10
|
+
class Dummy < API
|
12
11
|
#--
|
13
12
|
# Some dancing around the marshal issues with this provisioner. Note that
|
14
13
|
# after restoration, any delegates you set will no longer exist, so
|
@@ -17,11 +16,11 @@ module Furnish
|
|
17
16
|
|
18
17
|
# basic Palsy::Object store for stuffing random stuff
|
19
18
|
attr_reader :store
|
19
|
+
|
20
20
|
# order tracking via Palsy::List, delegation makes a breadcrumb here
|
21
21
|
# that's ordered between all provisioners.
|
22
22
|
attr_reader :order
|
23
|
-
|
24
|
-
attr_accessor :name
|
23
|
+
|
25
24
|
# arbitrary identifier for Dummy#call_order
|
26
25
|
attr_accessor :id
|
27
26
|
|
@@ -33,12 +32,20 @@ module Furnish
|
|
33
32
|
@order = Palsy::List.new('dummy_order', 'shared')
|
34
33
|
end
|
35
34
|
|
35
|
+
#
|
36
|
+
# used in state transitions to capture run information
|
37
|
+
#
|
38
|
+
def run_state
|
39
|
+
@run_state ||= Palsy::Map.new('dummy_run_state', [self.class.name, respond_to?(:furnish_group_name) ? furnish_group_name : name].join("-"))
|
40
|
+
end
|
41
|
+
|
36
42
|
#
|
37
43
|
# call order is ordering on a per-provisioner group basis, and is used to
|
38
44
|
# validate that groups do indeed execute in the proper order.
|
39
45
|
#
|
40
46
|
def call_order
|
41
|
-
|
47
|
+
# respond_to? here is to assist with deprecation tests
|
48
|
+
@call_order ||= Palsy::List.new('dummy_order', respond_to?(:furnish_group_name) ? furnish_group_name : name)
|
42
49
|
end
|
43
50
|
|
44
51
|
#
|
@@ -46,26 +53,26 @@ module Furnish
|
|
46
53
|
#
|
47
54
|
def report
|
48
55
|
do_delegate(__method__) do
|
49
|
-
[
|
56
|
+
[furnish_group_name, @persist]
|
50
57
|
end
|
51
58
|
end
|
52
59
|
|
53
60
|
#
|
54
61
|
# startup shim
|
55
62
|
#
|
56
|
-
def startup(
|
63
|
+
def startup(args={ })
|
57
64
|
@persist = "floop"
|
58
65
|
do_delegate(__method__) do
|
59
|
-
|
66
|
+
run_state[__method__] = args
|
60
67
|
end
|
61
68
|
end
|
62
69
|
|
63
70
|
#
|
64
71
|
# shutdown shim
|
65
72
|
#
|
66
|
-
def shutdown
|
73
|
+
def shutdown(args={ })
|
67
74
|
do_delegate(__method__) do
|
68
|
-
|
75
|
+
run_state[__method__] = args
|
69
76
|
end
|
70
77
|
end
|
71
78
|
|
@@ -77,8 +84,8 @@ module Furnish
|
|
77
84
|
meth_name = meth_name.to_s
|
78
85
|
|
79
86
|
# indicate we actually did something
|
80
|
-
@store[ [
|
81
|
-
@order.push(
|
87
|
+
@store[ [furnish_group_name, meth_name].join("-") ] = Time.now.to_i
|
88
|
+
@order.push(furnish_group_name)
|
82
89
|
call_order.push(id || "unknown")
|
83
90
|
|
84
91
|
yield
|