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