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