furnish 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- # This call also installs a SIGINFO (Ctrl+T in the terminal on macs) and
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 @solver_thread and !@serial
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
- %w[USR2 INFO].each { |sig| trap(sig, &handler) if Signal.list[sig] }
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.values.reject(&:alive?)
264
- if dead_working.size > 0
265
- dead_working.map(&:join)
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 - (@working_threads.keys.to_set + vm.solved.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
- provisioner = vm.groups[group_name]
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 provisioner and can_deprovision?(group_name)
360
- provisioner.shutdown(@force_deprovision)
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
- startup(group_name)
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 { startup(group_name) }
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
@@ -1,4 +1,4 @@
1
1
  module Furnish
2
2
  # The current version of Furnish.
3
- VERSION = "0.0.4"
3
+ VERSION = "0.1.0"
4
4
  end
@@ -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