furnish 0.0.4 → 0.1.0
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.
- 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
|