processwanker 0.0.7

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/lib/service.rb ADDED
@@ -0,0 +1,512 @@
1
+ ############################################################################
2
+ #
3
+ # service.rb
4
+ #
5
+ # class representing a running service
6
+ #
7
+ ############################################################################
8
+
9
+ require 'process_util'
10
+ require 'log'
11
+ require 'events'
12
+
13
+ module ProcessWanker
14
+
15
+ class Service
16
+
17
+ include Log
18
+
19
+ ############################################################################
20
+ #
21
+ # current state
22
+ #
23
+ ############################################################################
24
+
25
+ attr_accessor :params
26
+ attr_accessor :want_state
27
+ attr_accessor :last_action_time
28
+ attr_accessor :current_state
29
+ attr_accessor :prev_state
30
+ attr_accessor :last_transition_time
31
+ attr_accessor :attempt_count
32
+ attr_accessor :last_fail_time
33
+ attr_accessor :show_state
34
+ attr_accessor :dependencies
35
+ attr_accessor :suppress
36
+ attr_accessor :ignore
37
+ attr_accessor :config_node
38
+ attr_accessor :want_state_mode
39
+ attr_accessor :stable
40
+
41
+ ############################################################################
42
+ #
43
+ # initialize
44
+ #
45
+ # iparams is a hash containing:
46
+ #
47
+ # :name name of service (unique to this machine)
48
+ #
49
+ # :tags array of tags
50
+ # :group_name group name
51
+ # :min_action_delay_secs minimum delay between (automatic) actions
52
+ # :start_grace_secs delay between successive start attempts
53
+ # :stop_grace_secs delay between stop attempts
54
+ # :stable_secs seconds the process must be in desired state to be considered stable
55
+ # :fail_trigger_count number of transitions to trigger failing
56
+ # :fail_suppress_secs seconds to delay actions after failing detected
57
+ # :initial_state (defaults to current state) :up or :down
58
+ #
59
+ ############################################################################
60
+
61
+ def initialize(iparams)
62
+
63
+ # extract params
64
+ extract_params(
65
+ iparams,
66
+ [
67
+ :name,
68
+ :tags,
69
+ :group_name,
70
+ :min_action_delay_secs,
71
+ :start_grace_secs,
72
+ :stop_grace_secs,
73
+ :stable_secs,
74
+ :fail_trigger_count,
75
+ :fail_suppress_secs,
76
+ :initial_state,
77
+ :dependencies,
78
+ :log_file,
79
+ ])
80
+
81
+ # warn about extra params
82
+ iparams.keys.each do |k|
83
+ warn "warning: ignoring unrecognized parameter: #{k.to_s}"
84
+ end
85
+
86
+ # apply defaults
87
+ @params={
88
+ :min_action_delay_secs => 1,
89
+ :stable_secs => 20,
90
+ :fail_trigger_count => 5,
91
+ :fail_suppress_secs => 300,
92
+ :group_name => "default",
93
+ :start_grace_secs => 5,
94
+ :stop_grace_secs => 5,
95
+ :dependencies => []
96
+ }.merge(@params)
97
+
98
+ @params[:tags] ||= []
99
+
100
+ # convert necessary things to symbols
101
+ @params[:initial_state] = @params[:initial_state].to_sym if(@params[:initial_state])
102
+
103
+ # set state values
104
+ @last_action_time=Time.at(0)
105
+ @last_action=nil
106
+ @current_state=safe_do_ping()
107
+ @want_state=@params[:initial_state] || @current_state
108
+ @prev_state=safe_do_ping()
109
+ @last_transition_time=Time.at(0)
110
+ @attempt_count=0
111
+ @last_fail_time=Time.at(0)
112
+ @show_state=""
113
+ @suppress=false
114
+ @want_state_mode=:boot
115
+ @stable=false
116
+
117
+ # register with the manager
118
+ ServiceMgr::register_service(self)
119
+ end
120
+
121
+ ############################################################################
122
+ #
123
+ #
124
+ #
125
+ ############################################################################
126
+
127
+ def validate_params()
128
+ raise "no name provided" if(!params[:name])
129
+ @params.keys.each do |k|
130
+ if(!keys.include?(k))
131
+ warn "warning: unrecognized parameter #{k.to_s} ignored"
132
+ end
133
+ end
134
+ end
135
+
136
+ ############################################################################
137
+ #
138
+ #
139
+ #
140
+ ############################################################################
141
+
142
+ def extract_params(params,keys)
143
+ @params ||= {}
144
+ keys.each do |k|
145
+ @params[k]=params[k] if(params.has_key?(k))
146
+ params.delete(k)
147
+ end
148
+ end
149
+
150
+ ############################################################################
151
+ #
152
+ # return the host-wide unique name of the service
153
+ #
154
+ ############################################################################
155
+
156
+ def name
157
+ @params[:name]
158
+ end
159
+
160
+ ############################################################################
161
+ #
162
+ # return the name of the group this process belongs to
163
+ #
164
+ ############################################################################
165
+
166
+ def group_name
167
+ @params[:group_name]
168
+ end
169
+
170
+ ############################################################################
171
+ #
172
+ # start the service. should not block for a considerable amount of time
173
+ #
174
+ ############################################################################
175
+
176
+ def do_start(attempt_ct)
177
+ raise "method not defined in base class"
178
+ end
179
+
180
+ ############################################################################
181
+ #
182
+ # stop the service - should not block for a considerable amount of time
183
+ #
184
+ ############################################################################
185
+
186
+ def do_stop(attempt_ct)
187
+ raise "method not defined in base class"
188
+ end
189
+
190
+ ############################################################################
191
+ #
192
+ # return state (:up, :down)
193
+ #
194
+ ############################################################################
195
+
196
+ def do_ping
197
+ raise "method not defined in base class"
198
+ end
199
+
200
+ ############################################################################
201
+ #
202
+ # resolve dependencies through name lookup
203
+ #
204
+ ############################################################################
205
+
206
+ def resolve_dependencies
207
+
208
+ @dependencies=[]
209
+ params[:dependencies].each do |dep|
210
+ ServiceMgr::instance.match_services(dep.service).each do |k,v|
211
+ @dependencies << { :service => v, :dep => dep }
212
+ end
213
+ end
214
+ end
215
+
216
+ ############################################################################
217
+ #
218
+ # safe, exception-catching methods
219
+ #
220
+ ############################################################################
221
+
222
+ def safe_do(name)
223
+ ProcessWanker::with_logged_rescue("#{name} - safe_do") do
224
+ yield
225
+ end
226
+ end
227
+
228
+ def safe_do_start(attempt_ct)
229
+ safe_do("#{name}:do_start") { do_start(attempt_ct) }
230
+ end
231
+
232
+ def safe_do_stop(attempt_ct)
233
+ safe_do("#{name}:do_start") { do_stop(attempt_ct) }
234
+ end
235
+
236
+ def safe_do_ping()
237
+ p=:down
238
+ safe_do("#{name}:do_start") { p=do_ping }
239
+ p
240
+ end
241
+
242
+ ############################################################################
243
+ #
244
+ # main logic
245
+ #
246
+ ############################################################################
247
+
248
+ def tick
249
+
250
+ #
251
+ # get current time
252
+ #
253
+
254
+ now=Time.now
255
+
256
+ #
257
+ # get current state, check for change, record transition time
258
+ #
259
+
260
+ state = safe_do_ping()
261
+ if(@current_state != state)
262
+ @prev_state = @current_state
263
+ @last_transition_time = now
264
+ @current_state = state
265
+ @stable = false
266
+ @show_state = state.to_s
267
+ end
268
+
269
+ #
270
+ # handle special :restart case
271
+ #
272
+
273
+ want=@want_state
274
+ if(want == :restart)
275
+ if(@current_state == :down)
276
+ want = :up
277
+ @want_state = :up
278
+ @last_action_time = Time.now
279
+ else
280
+ want = :down
281
+ end
282
+ end
283
+
284
+ #
285
+ # check dependencies
286
+ #
287
+
288
+ deps_ok=true
289
+ @dependencies.each do |d|
290
+ s=d[:service]
291
+ if(s.current_state != :up)
292
+ deps_ok=false
293
+ next
294
+ end
295
+ tt=now - s.last_transition_time
296
+ if(tt < d[:dep].up_for)
297
+ deps_ok=false
298
+ end
299
+ end
300
+ @suppress=(!deps_ok) && (@want_state != :down)
301
+ if(@suppress)
302
+ want = :down
303
+ end
304
+
305
+ #
306
+ # have we been in the same state for a while?
307
+ #
308
+
309
+ stabilized=false
310
+ elapsed = now - @last_transition_time
311
+ if(!@stable && elapsed >= @params[:stable_secs])
312
+ stabilized=true
313
+ @stable=true
314
+ end
315
+
316
+ #
317
+ # are we in the desired state?
318
+ #
319
+
320
+ if(@current_state == want)
321
+
322
+ # did we just stabilize?
323
+ if(stabilized)
324
+
325
+ @attempt_count = 0
326
+ @show_state = @current_state.to_s + " [stable]"
327
+
328
+ # was this request part of a user request? if not, notify
329
+ if(@want_state_mode == :none && want == :up)
330
+ Event::dispatch("restarted",self)
331
+ end
332
+
333
+ # clear request mode
334
+ @want_state_mode=:none
335
+ end
336
+
337
+ # nothing more to do
338
+ return
339
+ end
340
+
341
+ #
342
+ # are we ignoring the process?
343
+ #
344
+
345
+ if(want == :ignore)
346
+ @show_state = "#{@current_state.to_s} (ignored)"
347
+ return
348
+ end
349
+
350
+ #
351
+ # is it too soon to do anything?
352
+ #
353
+
354
+ proposed_action = { :up => :start , :down => :stop }[want]
355
+ return if(!check_action_delay(now,proposed_action))
356
+
357
+ #
358
+ # actually attempt to cause a change
359
+ #
360
+
361
+ # update state
362
+ @attempt_count=0 if(proposed_action != @last_action)
363
+ @last_action=proposed_action
364
+ @last_action_time=now
365
+
366
+ # check for failing
367
+ if(@attempt_count >= @params[:fail_trigger_count])
368
+ info("#{self.name} has now had #{@attempt_count} attempts. considering it failed.")
369
+
370
+ @show_state = "failing(#{proposed_action})"
371
+ @last_fail_time = now
372
+ @attempt_count = 0 # reset for next time
373
+
374
+ Event::dispatch("fail-#{proposed_action.to_s}",self)
375
+
376
+ return
377
+ end
378
+
379
+ # was this request part of a user request? if not, notify
380
+ if(@want_state_mode == :none && proposed_action == :start && @attempt_count==0)
381
+ Event::dispatch("restarting",self)
382
+ end
383
+
384
+ # do it
385
+ @show_state = "#{proposed_action} [#{@attempt_count}]"
386
+ if(proposed_action == :start)
387
+
388
+ Event::dispatch("pre-launch",self)
389
+
390
+ info("calling do_start for #{self.name}")
391
+ safe_do_start(@attempt_count)
392
+
393
+ else
394
+
395
+ info("calling do_stop for #{self.name}")
396
+ safe_do_stop(@attempt_count)
397
+
398
+ end
399
+ @attempt_count += 1
400
+
401
+ end
402
+
403
+ ############################################################################
404
+ #
405
+ # check the various timers, and see if we're allowed to take action
406
+ # on a specific service at this point...
407
+ #
408
+ ############################################################################
409
+
410
+ def check_action_delay(now,proposed_action)
411
+
412
+ elapsed=now - @last_action_time
413
+
414
+ # check general-purpose between-action-delay
415
+ return(false) if(elapsed < @params[:min_action_delay_secs])
416
+
417
+ if(proposed_action == @last_action)
418
+
419
+ # check grace periods
420
+ return(false) if(@last_action == :start && elapsed < @params[:start_grace_secs])
421
+ return(false) if(@last_action == :stop && elapsed < @params[:stop_grace_secs])
422
+
423
+ # check failing suppression
424
+ since_fail=now - @last_fail_time
425
+ return(false) if(since_fail < @params[:fail_suppress_secs])
426
+ end
427
+
428
+ true
429
+ end
430
+
431
+ ############################################################################
432
+ #
433
+ # set want state in response to a user request - clear all delay state,
434
+ # start with a clean slate.
435
+ #
436
+ ############################################################################
437
+
438
+ def set_want_state(state)
439
+ @want_state = state
440
+ @want_state_mode = :user
441
+ @attempt_count = 0
442
+ @last_action_time = Time.at(0)
443
+ @last_fail_time = Time.at(0)
444
+ if(@want_state != @current_state)
445
+ @show_state = "received #{state.inspect}"
446
+ end
447
+ end
448
+
449
+ ############################################################################
450
+ #
451
+ #
452
+ #
453
+ ############################################################################
454
+
455
+ def matches_spec(spec)
456
+
457
+ # ensure it's in array form
458
+ spec=spec.split(",")
459
+
460
+ # check for inversion on first item
461
+ if(spec.first[0..0]=="~")
462
+ # insert implicit "all" at front
463
+ spec=["all"] + spec
464
+ end
465
+
466
+ matches=false
467
+ spec.each do |p|
468
+ matches = matches_single(p,matches)
469
+ end
470
+ matches
471
+
472
+ end
473
+
474
+ ############################################################################
475
+ #
476
+ #
477
+ #
478
+ ############################################################################
479
+
480
+ def matches_single(p,prev)
481
+
482
+ if(p == "all")
483
+ return(true)
484
+ elsif(p[0..0] == "/")
485
+ r=Regexp.new(p.split("/")[1])
486
+ return(true) if(group_name =~ r)
487
+ return(true) if(name =~ r)
488
+ elsif(p[0..0] == "~")
489
+ return(prev && !matches_single(p[1..-1],false))
490
+ elsif(p[0..3] == "tag:")
491
+ tag=p[4..-1]
492
+ return(true) if(params[:tags] && params[:tags].include?(tag))
493
+ elsif(p == name)
494
+ return(true)
495
+ elsif(p == group_name)
496
+ return(true)
497
+ end
498
+
499
+ prev
500
+ end
501
+
502
+
503
+ ############################################################################
504
+ #
505
+ #
506
+ #
507
+ ############################################################################
508
+
509
+
510
+ end
511
+
512
+ end
@@ -0,0 +1,88 @@
1
+ ############################################################################
2
+ #
3
+ # dummy_service.rb
4
+ #
5
+ # class representing a fake service
6
+ #
7
+ ############################################################################
8
+
9
+ require 'service'
10
+ require 'digest/md5'
11
+ require 'etc'
12
+ require 'process_service'
13
+ require 'process_util'
14
+
15
+ module ProcessWanker
16
+
17
+ class DummyService < Service
18
+ include Log
19
+
20
+ ############################################################################
21
+ #
22
+ #
23
+ #
24
+ ############################################################################
25
+
26
+ def self.nice_name
27
+ "dummy_service"
28
+ end
29
+
30
+ ############################################################################
31
+ #
32
+ #
33
+ #
34
+ ############################################################################
35
+
36
+ def initialize(iparams)
37
+ @state=(iparams[:initial_state] || :down).to_sym
38
+ super(iparams)
39
+ end
40
+
41
+ ############################################################################
42
+ #
43
+ # start
44
+ #
45
+ ############################################################################
46
+
47
+ def do_start(attempt_count)
48
+ debug("do_start #{self.name}")
49
+ @state=:up
50
+ end
51
+
52
+ ############################################################################
53
+ #
54
+ # stop
55
+ #
56
+ ############################################################################
57
+
58
+ def do_stop(attempt_count)
59
+ debug("do_stop #{self.name}")
60
+ @state=:down
61
+ end
62
+
63
+ ############################################################################
64
+ #
65
+ # ping
66
+ #
67
+ # return run state of process
68
+ #
69
+ ############################################################################
70
+
71
+ def do_ping
72
+ @state
73
+ end
74
+
75
+ ############################################################################
76
+ #
77
+ #
78
+ #
79
+ ############################################################################
80
+
81
+ end
82
+
83
+
84
+ ServiceMgr::register_service_class(DummyService)
85
+
86
+ end
87
+
88
+
@@ -0,0 +1,126 @@
1
+ ############################################################################
2
+ #
3
+ # process_service.rb
4
+ #
5
+ # class representing a service that is a command-line launchable process
6
+ #
7
+ ############################################################################
8
+
9
+ require 'service'
10
+ require 'digest/md5'
11
+ require 'etc'
12
+ require 'process_service'
13
+ require 'process_util'
14
+
15
+ module ProcessWanker
16
+
17
+ class PIDService < ProcessService
18
+
19
+ ############################################################################
20
+ #
21
+ #
22
+ #
23
+ ############################################################################
24
+
25
+ def self.nice_name
26
+ "pid_service"
27
+ end
28
+
29
+ ############################################################################
30
+ #
31
+ # iparams is a hash containing:
32
+ #
33
+ # :start_cmd
34
+ # :pid_file
35
+ # :start_dir (optional)
36
+ # :run_user (optional)
37
+ # :soft_kill_limit (optional)
38
+ #
39
+ # plus anything to be passed to Service
40
+ #
41
+ ############################################################################
42
+
43
+ def initialize(iparams)
44
+
45
+ # extract parameters
46
+ extract_params(
47
+ iparams,
48
+ [
49
+ :pid_file,
50
+ ])
51
+
52
+ raise "service has no pid_file" if(!@params[:pid_file])
53
+
54
+ super(iparams)
55
+ end
56
+
57
+ ############################################################################
58
+ #
59
+ # stop
60
+ #
61
+ # stop the process (either with -TERM or -KILL)
62
+ #
63
+ ############################################################################
64
+
65
+ def do_stop(attempt_count)
66
+ info("do_stop[#{attempt_count}] for #{self.inspect}")
67
+
68
+ kl=@params[:soft_kill_limit]
69
+ mode = (kl && attempt_count >= kl) ? :hard : :soft
70
+
71
+ if(mode == :soft)
72
+ if(params[:stop_cmd])
73
+ system(params[:stop_cmd])
74
+ return
75
+ end
76
+ end
77
+
78
+ ProcessWanker::with_logged_rescue("PidServer::do_stop - reading pid file") do
79
+ pid = File.read(@params[:pid_file]).strip.to_i
80
+ Process.kill({ :hard => "KILL", :soft => "TERM" }[mode],pid)
81
+ end
82
+
83
+ end
84
+
85
+ ############################################################################
86
+ #
87
+ # ping
88
+ #
89
+ # return run state of process
90
+ #
91
+ ############################################################################
92
+
93
+ def do_ping
94
+ begin
95
+ pid = File.read(@params[:pid_file]).strip.to_i
96
+ return(:up) if(ProcessUtil::all_processes[pid])
97
+ rescue Exception => e
98
+ end
99
+ :down
100
+ end
101
+
102
+ ############################################################################
103
+ #
104
+ # override the env_hash() method, to prevent our services being tagged
105
+ # in the same way process_services are.
106
+ #
107
+ ############################################################################
108
+
109
+ def env_hash()
110
+ nil
111
+ end
112
+
113
+ ############################################################################
114
+ #
115
+ #
116
+ #
117
+ ############################################################################
118
+
119
+ end
120
+
121
+
122
+ ServiceMgr::register_service_class(PIDService)
123
+
124
+ end
125
+
126
+