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
data/lib/furnish/scheduler.rb
CHANGED
@@ -32,6 +32,16 @@ module Furnish
|
|
32
32
|
#
|
33
33
|
attr_accessor :force_deprovision
|
34
34
|
|
35
|
+
##
|
36
|
+
#
|
37
|
+
# When true, calling #run or #recover also installs a SIGINFO (Ctrl+T in the
|
38
|
+
# terminal on macs) and SIGUSR2 handler which can be used to get
|
39
|
+
# information on the status of what's solved and what's working.
|
40
|
+
#
|
41
|
+
# Default is true.
|
42
|
+
#
|
43
|
+
attr_accessor :signal_handler
|
44
|
+
|
35
45
|
#
|
36
46
|
# Instantiate the Scheduler.
|
37
47
|
#
|
@@ -43,6 +53,8 @@ module Furnish
|
|
43
53
|
@working_threads = { }
|
44
54
|
@queue = Queue.new
|
45
55
|
@vm = Furnish::VM.new
|
56
|
+
@recovering = false
|
57
|
+
@signal_handler = true
|
46
58
|
end
|
47
59
|
|
48
60
|
#
|
@@ -63,6 +75,28 @@ module Furnish
|
|
63
75
|
end
|
64
76
|
end
|
65
77
|
|
78
|
+
#
|
79
|
+
# Is recovery running? See #recover.
|
80
|
+
#
|
81
|
+
def recovering?
|
82
|
+
@recovering
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
# Is recovery necessary? See #recover.
|
87
|
+
#
|
88
|
+
def needs_recovery?
|
89
|
+
needs_recovery.count > 0
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# A map of group name to Furnish::ProvisionerGroup for groups that failed
|
94
|
+
# their #startup or #shutdown. See #recover for more information
|
95
|
+
#
|
96
|
+
def needs_recovery
|
97
|
+
vm.need_recovery
|
98
|
+
end
|
99
|
+
|
66
100
|
#
|
67
101
|
# Schedule a group of VMs for provision. This takes a group name, which is a
|
68
102
|
# string, an array of provisioner objects, and a list of string dependencies.
|
@@ -76,6 +110,9 @@ module Furnish
|
|
76
110
|
schedule_provisioner_group(group)
|
77
111
|
end
|
78
112
|
|
113
|
+
alias s schedule_provision
|
114
|
+
alias sched schedule_provision
|
115
|
+
|
79
116
|
#
|
80
117
|
# Schedule a provision with a Furnish::ProvisionerGroup. Works exactly like
|
81
118
|
# Furnish::Scheduler#schedule_provision otherwise.
|
@@ -94,8 +131,12 @@ module Furnish
|
|
94
131
|
vm.sync_waiters do |waiters|
|
95
132
|
waiters.add(group.name)
|
96
133
|
end
|
134
|
+
|
135
|
+
return true
|
97
136
|
end
|
98
137
|
|
138
|
+
alias << schedule_provisioner_group
|
139
|
+
|
99
140
|
#
|
100
141
|
# Sleep until this list of dependencies are resolved. In parallel mode, will
|
101
142
|
# raise if an exception occurred while waiting for these resources. In
|
@@ -121,24 +162,11 @@ module Furnish
|
|
121
162
|
# and #running? and #stop to control and monitor the threads this class
|
122
163
|
# manages.
|
123
164
|
#
|
124
|
-
|
125
|
-
# SIGUSR2 handler which can be used to get information on the status of
|
126
|
-
# what's solved and what's working. You can disable this functionality by
|
127
|
-
# passing `false` as the first argument.
|
128
|
-
#
|
129
|
-
def run(install_handler=true)
|
165
|
+
def run
|
130
166
|
# short circuit if we're not serial and already running
|
131
|
-
return if
|
132
|
-
|
133
|
-
if install_handler
|
134
|
-
handler = lambda do |*args|
|
135
|
-
Furnish.logger.puts ["solved:", vm.solved.to_a].inspect
|
136
|
-
Furnish.logger.puts ["working:", vm.working.to_a].inspect
|
137
|
-
Furnish.logger.puts ["waiting:", vm.waiters.to_a].inspect
|
138
|
-
end
|
167
|
+
return if running?
|
139
168
|
|
140
|
-
|
141
|
-
end
|
169
|
+
install_handler if signal_handler
|
142
170
|
|
143
171
|
if @serial
|
144
172
|
service_resolved_waiters
|
@@ -151,6 +179,69 @@ module Furnish
|
|
151
179
|
end
|
152
180
|
end
|
153
181
|
|
182
|
+
#
|
183
|
+
# Initiate recovery. While running, #recovering? will be true.
|
184
|
+
#
|
185
|
+
# Recovery will step through all the items in #needs_recovery and attempt
|
186
|
+
# to recover them according to Furnish::ProvisionerGroup#recover. If
|
187
|
+
# recovery succeeds, the items will be in the solved formula and
|
188
|
+
# effectively provisioned. They will also be removed from the
|
189
|
+
# needs_recovery information.
|
190
|
+
#
|
191
|
+
# If recovery fails, #needs_recovery will not be touched (but the state at
|
192
|
+
# which recovery starts the next attempt may be different for those
|
193
|
+
# groups). Additionally, the return value of this method will be keyed by
|
194
|
+
# the group name, and an exception or false depending on what we got back
|
195
|
+
# during recovery. It is strongly recommended you check #needs_recovery? or
|
196
|
+
# the return value after calling this to locate flapping groups.
|
197
|
+
#
|
198
|
+
# Recovery is a serial process and blocks the main thread. It also installs
|
199
|
+
# a signal handler if #signal_handler is set. It does not interrupt or stop
|
200
|
+
# the scheduler, but note that in serial mode, the scheduler will likely
|
201
|
+
# already be stopped by the time you are able to call recovery. In
|
202
|
+
# threaded mode, this means any dependencies that are able to be
|
203
|
+
# provisioned after a successful recovery of a group will automatically
|
204
|
+
# start provisioning.
|
205
|
+
#
|
206
|
+
def recover
|
207
|
+
install_handler if signal_handler
|
208
|
+
|
209
|
+
@recovering = true
|
210
|
+
|
211
|
+
failures = { }
|
212
|
+
|
213
|
+
needs_recovery.keys.each do |k|
|
214
|
+
begin
|
215
|
+
group = vm.groups[k]
|
216
|
+
result = group.recover(force_deprovision)
|
217
|
+
vm.groups[k] = group
|
218
|
+
|
219
|
+
if result
|
220
|
+
needs_recovery.delete(k)
|
221
|
+
@queue << k
|
222
|
+
else
|
223
|
+
failures[k] = false
|
224
|
+
end
|
225
|
+
rescue => e
|
226
|
+
failures[k] = e
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
if @serial
|
231
|
+
begin
|
232
|
+
queue_loop
|
233
|
+
rescue => e
|
234
|
+
if_debug do
|
235
|
+
puts "During recovery, serial mode, encountered: #{e}: #{e.message}"
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
@recovering = false
|
241
|
+
|
242
|
+
return failures
|
243
|
+
end
|
244
|
+
|
154
245
|
#
|
155
246
|
# Instructs the scheduler to stop. Note that this is not an interrupt, and
|
156
247
|
# the queue will still be exhausted before terminating.
|
@@ -260,9 +351,11 @@ module Furnish
|
|
260
351
|
#
|
261
352
|
def with_timeout(do_loop=true)
|
262
353
|
Timeout.timeout(1) do
|
263
|
-
dead_working = @working_threads.
|
264
|
-
if dead_working.size > 0
|
265
|
-
dead_working.
|
354
|
+
dead_working = @working_threads.reject { |k,v| v.alive? }
|
355
|
+
if dead_working.keys.size > 0
|
356
|
+
dead_working.each do |k, t|
|
357
|
+
@working_threads.delete(k)
|
358
|
+
end
|
266
359
|
end
|
267
360
|
|
268
361
|
yield
|
@@ -319,7 +412,7 @@ module Furnish
|
|
319
412
|
#
|
320
413
|
def resolve_waiters
|
321
414
|
vm.sync_waiters do |waiters|
|
322
|
-
waiters.replace(waiters.to_set - (
|
415
|
+
waiters.replace(waiters.to_set - (vm.working.to_set + vm.solved.to_set))
|
323
416
|
end
|
324
417
|
end
|
325
418
|
|
@@ -352,12 +445,12 @@ module Furnish
|
|
352
445
|
# Similar to #startup -- just a shim to talk to a specific ProvisionerGroup
|
353
446
|
#
|
354
447
|
def shutdown(group_name)
|
355
|
-
|
448
|
+
group = vm.groups[group_name]
|
356
449
|
|
357
450
|
# if we can't find the provisioner, we probably got asked to clean up
|
358
451
|
# something we never scheduled. Just ignore that.
|
359
|
-
if
|
360
|
-
|
452
|
+
if group and can_deprovision?(group_name)
|
453
|
+
group.shutdown({}, force_deprovision)
|
361
454
|
end
|
362
455
|
end
|
363
456
|
|
@@ -381,9 +474,20 @@ module Furnish
|
|
381
474
|
# HACK: just give the working check something that will always work.
|
382
475
|
# Probably should just mock it.
|
383
476
|
@working_threads[group_name] = Thread.new { sleep }
|
384
|
-
|
477
|
+
begin
|
478
|
+
startup(group_name)
|
479
|
+
rescue => e
|
480
|
+
vm.need_recovery[group_name] = e
|
481
|
+
raise e
|
482
|
+
end
|
385
483
|
else
|
386
|
-
@working_threads[group_name] = Thread.new
|
484
|
+
@working_threads[group_name] = Thread.new do
|
485
|
+
begin
|
486
|
+
startup(group_name)
|
487
|
+
rescue => e
|
488
|
+
vm.need_recovery[group_name] = e
|
489
|
+
end
|
490
|
+
end
|
387
491
|
end
|
388
492
|
end
|
389
493
|
end
|
@@ -402,6 +506,7 @@ module Furnish
|
|
402
506
|
# any threads managing it.
|
403
507
|
#
|
404
508
|
def delete_group(group_name)
|
509
|
+
vm.need_recovery.delete(group_name)
|
405
510
|
vm.solved.delete(group_name)
|
406
511
|
vm.sync_waiters do |waiters|
|
407
512
|
waiters.delete(group_name)
|
@@ -412,5 +517,23 @@ module Furnish
|
|
412
517
|
vm.dependencies.delete(group_name)
|
413
518
|
vm.groups.delete(group_name)
|
414
519
|
end
|
520
|
+
|
521
|
+
#
|
522
|
+
# Installs our signal handler -- see #signal_handler.
|
523
|
+
#
|
524
|
+
def install_handler
|
525
|
+
handler = lambda do |*args|
|
526
|
+
# XXX See Palsy#with_t and Palsy#no_lock for why this is necessary.
|
527
|
+
Palsy.instance.no_lock do
|
528
|
+
Furnish.logger.puts ["solved:", vm.solved.to_a].inspect
|
529
|
+
Furnish.logger.puts ["working:", vm.working.to_a].inspect
|
530
|
+
Furnish.logger.puts ["waiting:", vm.waiters.to_a].inspect
|
531
|
+
Furnish.logger.puts ["provisioning:", vm.working.to_a.map { |w| [w, vm.groups[w].group_state['action'], vm.groups[w].group_state['provisioner']] }]
|
532
|
+
Furnish.logger.puts ["needs recovery:", needs_recovery.keys]
|
533
|
+
end
|
534
|
+
end
|
535
|
+
|
536
|
+
%w[USR2 INFO].each { |sig| trap(sig, &handler) if Signal.list[sig] }
|
537
|
+
end
|
415
538
|
end
|
416
539
|
end
|
data/lib/furnish/version.rb
CHANGED
data/lib/furnish/vm.rb
CHANGED
@@ -14,6 +14,8 @@ module Furnish
|
|
14
14
|
attr_reader :working
|
15
15
|
# the set of groups waiting to be provisioned.
|
16
16
|
attr_reader :waiters
|
17
|
+
# the set of groups that need recovery, and the exceptions they threw (if any)
|
18
|
+
attr_reader :need_recovery
|
17
19
|
|
18
20
|
#
|
19
21
|
# Create a new VM object. Should only happen in the Scheduler.
|
@@ -21,6 +23,7 @@ module Furnish
|
|
21
23
|
def initialize
|
22
24
|
@groups = Palsy::Map.new('vm_groups', 'provisioner_group')
|
23
25
|
@dependencies = Palsy::Map.new('vm_groups', 'dependency_group')
|
26
|
+
@need_recovery = Palsy::Map.new('vm_groups', 'need_recovery')
|
24
27
|
@solved = Palsy::Set.new('vm_scheduler', 'provisioned')
|
25
28
|
@working = Palsy::Set.new('vm_scheduler', 'working')
|
26
29
|
@waiters = Palsy::Set.new('vm_scheduler', 'waiters')
|
@@ -0,0 +1,275 @@
|
|
1
|
+
#
|
2
|
+
# Several dummy class mutations (inherited from Furnish::Provisioner::Dummy) we
|
3
|
+
# use in tests.
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'furnish/provisioners/dummy'
|
7
|
+
|
8
|
+
Dummy = Furnish::Provisioner::Dummy unless defined? Dummy
|
9
|
+
|
10
|
+
#
|
11
|
+
# FIXME move all these dummy classes to their own file
|
12
|
+
#
|
13
|
+
# FIXME Probably should generate these classes
|
14
|
+
#
|
15
|
+
|
16
|
+
class ReturnsInfoDummy < Dummy
|
17
|
+
def startup(args={})
|
18
|
+
retval = { :startup_blah => [1] }
|
19
|
+
super(retval)
|
20
|
+
return retval
|
21
|
+
end
|
22
|
+
|
23
|
+
def shutdown(args={})
|
24
|
+
retval = { :shutdown_blah => [1] }
|
25
|
+
super(retval)
|
26
|
+
return retval
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class AcceptsIntegerBarDummy < Dummy
|
31
|
+
configure_startup do
|
32
|
+
accepts :bar, "bar", Integer
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class YieldsIntegerBarDummy < Dummy
|
37
|
+
configure_startup do
|
38
|
+
yields :bar, "bar", Integer
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class AcceptsStringBarDummy < Dummy
|
43
|
+
configure_startup do
|
44
|
+
accepts :bar, "bar", String
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class YieldsStringBarDummy < Dummy
|
49
|
+
configure_startup do
|
50
|
+
yields :bar, "bar", String
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class AcceptsFooDummy < Dummy
|
55
|
+
configure_startup do
|
56
|
+
accepts :foo, "foo", String
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class YieldsFooDummy < Dummy
|
61
|
+
configure_startup do
|
62
|
+
yields :foo, "foo", String
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class RequiresBarDummy < Dummy
|
67
|
+
configure_startup do
|
68
|
+
requires :bar, "bar", Integer
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
class YieldsFooBarDummy < Dummy
|
73
|
+
configure_startup do
|
74
|
+
yields :bar, "bar", Integer
|
75
|
+
yields :foo, "foo", String
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class RequiresBarAcceptsFooDummy < Dummy
|
80
|
+
configure_startup do
|
81
|
+
requires :bar, "bar", Integer
|
82
|
+
accepts :foo, "foo", String
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class ShutdownAcceptsIntegerBarDummy < Dummy
|
87
|
+
configure_shutdown do
|
88
|
+
accepts :bar, "bar", Integer
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class ShutdownYieldsIntegerBarDummy < Dummy
|
93
|
+
configure_shutdown do
|
94
|
+
yields :bar, "bar", Integer
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class ShutdownAcceptsStringBarDummy < Dummy
|
99
|
+
configure_shutdown do
|
100
|
+
accepts :bar, "bar", String
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
class ShutdownYieldsStringBarDummy < Dummy
|
105
|
+
configure_shutdown do
|
106
|
+
yields :bar, "bar", String
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class ShutdownAcceptsFooDummy < Dummy
|
111
|
+
configure_shutdown do
|
112
|
+
accepts :foo, "foo", String
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
class ShutdownYieldsFooDummy < Dummy
|
117
|
+
configure_shutdown do
|
118
|
+
yields :foo, "foo", String
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class ShutdownRequiresBarDummy < Dummy
|
123
|
+
configure_shutdown do
|
124
|
+
requires :bar, "bar", Integer
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class ShutdownYieldsFooBarDummy < Dummy
|
129
|
+
configure_shutdown do
|
130
|
+
yields :bar, "bar", Integer
|
131
|
+
yields :foo, "foo", String
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
class ShutdownRequiresBarAcceptsFooDummy < Dummy
|
136
|
+
configure_shutdown do
|
137
|
+
requires :bar, "bar", Integer
|
138
|
+
accepts :foo, "foo", String
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
class StartFailDummy < Dummy
|
143
|
+
def startup(args={ })
|
144
|
+
super
|
145
|
+
false
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class StopFailDummy < Dummy
|
150
|
+
def shutdown(args={ })
|
151
|
+
super
|
152
|
+
false
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
class StartExceptionDummy < Dummy
|
157
|
+
def startup(args={ })
|
158
|
+
super
|
159
|
+
raise "ermagherd startup"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
class StopExceptionDummy < Dummy
|
164
|
+
def shutdown(args={ })
|
165
|
+
super
|
166
|
+
raise "ermagherd shutdown"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
class APIDummy < Furnish::Provisioner::API
|
171
|
+
furnish_property :foo, "does things with foo", Integer
|
172
|
+
furnish_property "a_string"
|
173
|
+
attr_accessor :bar
|
174
|
+
end
|
175
|
+
|
176
|
+
class BadDummy < Furnish::Provisioner::Dummy
|
177
|
+
attr_accessor :name
|
178
|
+
|
179
|
+
# this retardation lets us make it look like furnish_group_name doesn't exist
|
180
|
+
def respond_to?(meth, include_all=true)
|
181
|
+
super unless [:furnish_group_name, :furnish_group_name=].include?(meth)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
class SleepyDummy < Dummy
|
186
|
+
def startup(args={ })
|
187
|
+
sleep 1
|
188
|
+
super
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
class SleepyFailingDummy < SleepyDummy
|
193
|
+
def startup(args={ })
|
194
|
+
super
|
195
|
+
return false
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
class BrokenRecoverAPIDummy < Dummy
|
200
|
+
allows_recovery
|
201
|
+
end
|
202
|
+
|
203
|
+
class RecoverableDummy < Dummy
|
204
|
+
allows_recovery
|
205
|
+
|
206
|
+
def startup(args={ })
|
207
|
+
super
|
208
|
+
run_state[__method__] = @recovered
|
209
|
+
return @recovered
|
210
|
+
end
|
211
|
+
|
212
|
+
def shutdown(args={ })
|
213
|
+
super
|
214
|
+
run_state[__method__] = @recovered
|
215
|
+
return @recovered
|
216
|
+
end
|
217
|
+
|
218
|
+
def recover(state, args)
|
219
|
+
@recovered = { state => true }
|
220
|
+
return true
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
class RaisingRecoverableDummy < Dummy
|
225
|
+
allows_recovery
|
226
|
+
|
227
|
+
def startup(args={ })
|
228
|
+
super
|
229
|
+
run_state[__method__] = @recovered
|
230
|
+
raise unless @recovered
|
231
|
+
return @recovered
|
232
|
+
end
|
233
|
+
|
234
|
+
def shutdown(args={ })
|
235
|
+
super
|
236
|
+
run_state[__method__] = @recovered
|
237
|
+
raise unless @recovered
|
238
|
+
return @recovered
|
239
|
+
end
|
240
|
+
|
241
|
+
def recover(state, args)
|
242
|
+
@recovered = { state => true }
|
243
|
+
return true
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
class FailedRecoverDummy < Dummy
|
248
|
+
allows_recovery
|
249
|
+
|
250
|
+
def startup(args={ })
|
251
|
+
super
|
252
|
+
return run_state[__method__] = false
|
253
|
+
end
|
254
|
+
|
255
|
+
def shutdown(args={ })
|
256
|
+
super
|
257
|
+
return run_state[__method__] = false
|
258
|
+
end
|
259
|
+
|
260
|
+
def recover(state, args)
|
261
|
+
return run_state[__method__] = false
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
class ReturnsDataDummy < Dummy
|
266
|
+
def startup(args={ })
|
267
|
+
super
|
268
|
+
return({ :started => 1 })
|
269
|
+
end
|
270
|
+
|
271
|
+
def shutdown(args={ })
|
272
|
+
super
|
273
|
+
return({ :stopped => 1 })
|
274
|
+
end
|
275
|
+
end
|