pipemaster 0.4.2 → 0.5.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 CHANGED
@@ -1,3 +1,23 @@
1
+ 0.5.0 (2010-03-01)
2
+ This release adds background processes. The Pipefile can specify multiple
3
+ background activities. Pipemaster starts each activity by forking a new child
4
+ process (with the same environment as all other workers).
5
+
6
+ When Pipemaster terminates, it terminates all child processes (QUIT or TERM,
7
+ depending on signal). In addition, when reloading the configuration (HUP),
8
+ Pipemaster asks all background processes to terminate gracefully and restarts
9
+ new background processes based on the new configuration.
10
+
11
+ To define a background process:
12
+
13
+ background :resque do
14
+ resque = Resque::Worker.new(:tasks)
15
+ resque.work(5) # interval, will block
16
+ end
17
+
18
+ * Added: Background processes.
19
+ * Changed: Configuration method app becomes setup. Same semantics, just better name.
20
+
1
21
  0.4.2 (2010-02-19)
2
22
  * Fix: Pipe command exits with retcode on successful completion.
3
23
 
data/Gemfile CHANGED
@@ -1,4 +1,3 @@
1
- source "http://gemcutter.org"
2
- gem "gemcutter"
1
+ source "http://rubygems.org/"
3
2
  gem "rake"
4
3
  gem "yard"
@@ -21,6 +21,7 @@ module Pipemaster
21
21
  server.logger.info("forked child re-executing...")
22
22
  },
23
23
  :pid => nil,
24
+ :background => {},
24
25
  :commands => {}
25
26
  }
26
27
 
@@ -116,10 +117,10 @@ module Pipemaster
116
117
  Server::START_CTX[:cwd] = ENV["PWD"] = path
117
118
  end
118
119
 
119
- # Application is a block we run at startup. This is the heavy stuff (e.g.
120
- # starting up ActiveRecord) we want to run once and fork from.
121
- def app(*args, &block)
122
- set_hook(:app, block_given? ? block : args[0], 0)
120
+ # Setup block runs on startup. Put all the heavy stuff here (e.g. loading
121
+ # libraries, initializing state from database).
122
+ def setup(*args, &block)
123
+ set_hook(:setup, block_given? ? block : args[0], 0)
123
124
  end
124
125
 
125
126
  # Sets after_fork hook to a given block. This block will be called by
@@ -144,6 +145,25 @@ module Pipemaster
144
145
  set_hook(:before_exec, block_given? ? block : args[0], 1)
145
146
  end
146
147
 
148
+ # Background process: the block is executed in a child process. The master
149
+ # will tell the child process to stop/terminate using appropriate signals,
150
+ # restart the process after a successful upgrade. Block accepts two
151
+ # arguments: server and worker.
152
+ def background(name_or_hash, a_proc = nil, &block)
153
+ set[:background] = {} if set[:background] == :unset
154
+ if Hash === name_or_hash
155
+ name_or_hash.each_pair do |name, a_proc|
156
+ background name, a_proc
157
+ end
158
+ else
159
+ name = name_or_hash.to_sym
160
+ a_proc ||= block
161
+ arity = a_proc.arity
162
+ (arity == 0 || arity < 0) or raise ArgumentError, "background #{name}#{a_proc.inspect} has invalid arity: #{arity} (need 0)"
163
+ set[:background][name] = a_proc
164
+ end
165
+ end
166
+
147
167
  # Sets listeners to the given +addresses+, replacing or augmenting the
148
168
  # current set. This is for internal API use only, do not use it in your
149
169
  # Pipemaster config file. Use listen instead.
@@ -7,9 +7,11 @@ require "pipemaster/socket_helper"
7
7
  module Pipemaster
8
8
 
9
9
  class Server < Struct.new(:listener_opts, :timeout, :logger,
10
- :app, :before_fork, :after_fork, :before_exec,
11
- :pid, :reexec_pid, :init_listeners,
12
- :master_pid, :config, :ready_pipe)
10
+ :setup, :before_fork, :after_fork, :before_exec,
11
+ :background, :commands,
12
+ :pid, :reexec_pid, :master_pid,
13
+ :config, :ready_pipe, :init_listeners)
14
+
13
15
 
14
16
  include SocketHelper
15
17
 
@@ -22,6 +24,9 @@ module Pipemaster
22
24
  # This hash maps PIDs to Workers
23
25
  WORKERS = {}
24
26
 
27
+ # Background workers.
28
+ BACKGROUND = []
29
+
25
30
  # We populate this at startup so we can figure out how to reexecute
26
31
  # and upgrade the currently running instance of Pipemaster
27
32
  # This Hash is considered a stable interface and changing its contents
@@ -75,7 +80,6 @@ module Pipemaster
75
80
  config.commit!(self, :skip => [:listeners, :pid])
76
81
  end
77
82
 
78
- attr_accessor :commands
79
83
 
80
84
  def start
81
85
  self.master_pid = $$
@@ -88,12 +92,12 @@ module Pipemaster
88
92
  Pipemaster::Util.reopen_logs
89
93
  logger.info "master done reopening logs"
90
94
  end
91
- trap(:HUP) { reloaded = true ; load_config! }
95
+ trap(:HUP) { reloaded = true ; load_config! ; restart_background }
92
96
  trap(:USR2) { reexec }
93
97
 
94
98
  proc_name "pipemaster"
95
- logger.info "loading application"
96
- app.call if app
99
+ logger.info "running setup"
100
+ setup.call if setup
97
101
 
98
102
  logger.info "master process ready" # test_exec.rb relies on this message
99
103
  if ready_pipe
@@ -110,9 +114,9 @@ module Pipemaster
110
114
  end
111
115
  config_listeners.each { |addr| listen(addr) }
112
116
 
113
- reloaded = nil
114
117
  begin
115
118
  reloaded = false
119
+ restart_background
116
120
  while selected = Kernel.select(LISTENERS)
117
121
  selected.first.each do |socket|
118
122
  client = socket.accept_nonblock
@@ -295,6 +299,7 @@ module Pipemaster
295
299
  Process.kill(signal, wpid)
296
300
  rescue Errno::ESRCH
297
301
  worker = WORKERS.delete(wpid)
302
+ BACKGROUND.delete(wpid)
298
303
  end
299
304
  end
300
305
 
@@ -309,7 +314,8 @@ module Pipemaster
309
314
  self.pid = pid.chomp('.oldbin') if pid
310
315
  proc_name 'master'
311
316
  else
312
- worker = WORKERS.delete(wpid) rescue nil
317
+ WORKERS.delete(wpid) rescue nil
318
+ BACKGROUND.delete(wpid)
313
319
  logger.info "reaped #{status.inspect} "
314
320
  end
315
321
  end
@@ -414,10 +420,46 @@ module Pipemaster
414
420
  ensure
415
421
  socket.close_write
416
422
  socket.close
417
- exit
423
+ exit!
424
+ end
425
+ end
426
+
427
+ def restart_background
428
+ # Gracefully shut down all backgroud processes.
429
+ BACKGROUND.delete_if { |wpid| Process.kill(:QUIT, wpid) rescue true }
430
+ # Start them again.
431
+ background.each do |name, block|
432
+ worker = Worker.new
433
+ before_fork.call self, worker
434
+ pid = fork { run_in_background name, worker, &block }
435
+ BACKGROUND << pid
436
+ WORKERS[pid] = worker
418
437
  end
419
438
  end
420
439
 
440
+ def run_in_background(name, worker, &block)
441
+ trap(:QUIT) { exit }
442
+ [:TERM, :INT].each { |sig| trap(sig) { exit! } }
443
+ [:USR1, :USR2].each { |sig| trap(sig, nil) }
444
+ trap(:CHLD, 'DEFAULT')
445
+
446
+ WORKERS.clear
447
+ LISTENERS.each { |sock| sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
448
+ after_fork.call self, worker
449
+ proc_name "pipemaster: #{name}"
450
+ logger.info "#{Process.pid} background worker #{name}"
451
+ block.call
452
+ logger.info "#{Process.pid} finished worker #{name}"
453
+ rescue SystemExit => ex
454
+ logger.info "#{Process.pid} finished worker #{name}"
455
+ rescue =>ex
456
+ logger.info "#{Process.pid} failed: #{ex.message}"
457
+ socket.write "#{ex.class.name}: #{ex.message}\n"
458
+ socket.write 127.chr
459
+ ensure
460
+ exit!
461
+ end
462
+
421
463
  end
422
464
  end
423
465
 
data/pipemaster.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "pipemaster"
3
- spec.version = "0.4.2"
3
+ spec.version = "0.5.0"
4
4
  spec.author = "Assaf Arkin"
5
5
  spec.email = "assaf@labnotes.org"
6
6
  spec.homepage = "http://github.com/assaf/pipemaster"
data/test/test_helper.rb CHANGED
@@ -41,7 +41,7 @@ def redirect_test_io
41
41
  orig_err = STDERR.dup
42
42
  orig_out = STDOUT.dup
43
43
  STDERR.reopen("test_stderr.#{$$}.log", "a")
44
- STDOUT.reopen("test_stdout.#{$$}.log", "a")
44
+ #STDOUT.reopen("test_stdout.#{$$}.log", "a")
45
45
  STDERR.sync = STDOUT.sync = true
46
46
 
47
47
  at_exit do
@@ -5,7 +5,7 @@ require 'tempfile'
5
5
  require 'pipemaster'
6
6
 
7
7
  TestStruct = Struct.new(
8
- *(Pipemaster::Configurator::DEFAULTS.keys + %w(listener_opts listeners)))
8
+ *(Pipemaster::Configurator::DEFAULTS.keys + %w(listener_opts listeners background commands)))
9
9
  class TestConfigurator < Test::Unit::TestCase
10
10
 
11
11
  def test_config_init
@@ -147,5 +147,20 @@ class TestConfigurator < Test::Unit::TestCase
147
147
  end
148
148
  end
149
149
 
150
- end
150
+ def test_background_proc
151
+ test_struct = TestStruct.new
152
+ [ proc { }, Proc.new { |*a| }, lambda { |*a| } ].each do |my_proc|
153
+ Pipemaster::Configurator.new(:background => { :foo => my_proc }).commit!(test_struct)
154
+ assert_equal my_proc, test_struct.background[:foo]
155
+ end
156
+ end
157
+
158
+ def test_background_wrong_arity
159
+ [ proc { |a| }, Proc.new { |a,b| }, lambda { |a,b,c| } ].each do |my_proc|
160
+ assert_raises(ArgumentError) do
161
+ Pipemaster::Configurator.new(:background => { :foo => my_proc })
162
+ end
163
+ end
164
+ end
151
165
 
166
+ end
@@ -13,7 +13,7 @@ class ServerTest < Test::Unit::TestCase
13
13
  redirect_test_io do
14
14
  wait_master_ready("test_stderr.#$$.log")
15
15
  File.truncate("test_stderr.#$$.log", 0)
16
- Process.kill 9, @pid
16
+ Process.kill :QUIT, @pid
17
17
  end
18
18
  end
19
19
  end
@@ -22,7 +22,7 @@ class ServerTest < Test::Unit::TestCase
22
22
  redirect_test_io do
23
23
  @server = Pipemaster::Server.new({:listeners => [ "127.0.0.1:#{@port}" ]}.merge(options || {}))
24
24
  @pid = fork { @server.start }
25
- at_exit { Process.kill @pid, 9 }
25
+ at_exit { Process.kill :QUIT, @pid }
26
26
  wait_master_ready("test_stderr.#$$.log")
27
27
  end
28
28
  end
@@ -95,17 +95,57 @@ class ServerTest < Test::Unit::TestCase
95
95
  tmp.close!
96
96
  end
97
97
 
98
- def test_loading_app
98
+ def test_running_setup
99
99
  iam = "sad"
100
- start :app => lambda { iam.replace "happy" }, :commands => { :iam => lambda { $stdout << iam } }
100
+ start :setup => lambda { iam.replace "happy" }, :commands => { :iam => lambda { $stdout << iam } }
101
101
  assert_equal "happy", hit("127.0.0.1:#@port", :iam).last
102
102
  end
103
103
 
104
- def test_reloading_app
104
+ def test_running_setup_again
105
105
  text = "foo"
106
- start :app => lambda { text << "bar" }, :commands => { :text => lambda { $stdout << text } }
106
+ start :setup => lambda { text << "bar" }, :commands => { :text => lambda { $stdout << text } }
107
107
  assert_equal "foobar", hit("127.0.0.1:#@port", :text).last
108
- Process.kill "HUP", @pid
108
+ Process.kill :HUP, @pid
109
109
  assert_equal "foobar", hit("127.0.0.1:#@port", :text).last
110
110
  end
111
+
112
+ def test_running_background
113
+ sync = Tempfile.new("sync")
114
+ start :background => { :chmod => lambda { |*_| sync.chmod 0 while sleep 0.01 } }
115
+ assert_equal 0, sync.stat.mode & 1
116
+ sync.chmod 1 ; sleep 0.1
117
+ assert_equal 0, sync.stat.mode & 1
118
+ ensure
119
+ sync.close!
120
+ end
121
+
122
+ def test_stopping_background
123
+ sync = Tempfile.new("sync")
124
+ start :background => { :chmod => lambda { |*_| sync.chmod 0 while sleep 0.01 } }
125
+ sync.chmod 1 ; sleep 0.1
126
+ assert_equal 0, sync.stat.mode & 1
127
+ Process.kill :QUIT, @pid
128
+ sleep 0.1
129
+ sync.chmod 1 ; sleep 0.1
130
+ assert_equal 1, sync.stat.mode & 1
131
+ ensure
132
+ sync.close!
133
+ end
134
+
135
+ def test_restarting_background
136
+ config = Tempfile.new("config")
137
+ config.write "$flag = 0" ; config.flush
138
+ sync = Tempfile.new("sync")
139
+ start :config_file => config.path, :background => { :chmod => lambda { |*_| sync.chmod $flag while sleep 0.01 } }
140
+ sync.chmod 1 ; sleep 0.1
141
+ assert_equal 0, sync.stat.mode & 1
142
+ config.rewind ; config.write "$flag = 1" ; config.flush
143
+ Process.kill :HUP, @pid
144
+ sleep 0.1
145
+ sync.chmod 0 ; sleep 0.2
146
+ assert_equal 1, sync.stat.mode & 1
147
+ ensure
148
+ sync.close!
149
+ end
150
+
111
151
  end
metadata CHANGED
@@ -1,7 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pipemaster
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 5
8
+ - 0
9
+ version: 0.5.0
5
10
  platform: ruby
6
11
  authors:
7
12
  - Assaf Arkin
@@ -9,7 +14,7 @@ autorequire:
9
14
  bindir: bin
10
15
  cert_chain: []
11
16
 
12
- date: 2010-02-20 00:00:00 -08:00
17
+ date: 2010-03-01 00:00:00 -08:00
13
18
  default_executable:
14
19
  dependencies: []
15
20
 
@@ -51,7 +56,7 @@ licenses: []
51
56
  post_install_message: To get started run pipemaster --help
52
57
  rdoc_options:
53
58
  - --title
54
- - Pipemaster 0.4.2
59
+ - Pipemaster 0.5.0
55
60
  - --main
56
61
  - README.rdoc
57
62
  - --webcvs
@@ -62,18 +67,20 @@ required_ruby_version: !ruby/object:Gem::Requirement
62
67
  requirements:
63
68
  - - ">="
64
69
  - !ruby/object:Gem::Version
70
+ segments:
71
+ - 0
65
72
  version: "0"
66
- version:
67
73
  required_rubygems_version: !ruby/object:Gem::Requirement
68
74
  requirements:
69
75
  - - ">="
70
76
  - !ruby/object:Gem::Version
77
+ segments:
78
+ - 0
71
79
  version: "0"
72
- version:
73
80
  requirements: []
74
81
 
75
82
  rubyforge_project:
76
- rubygems_version: 1.3.5
83
+ rubygems_version: 1.3.6
77
84
  signing_key:
78
85
  specification_version: 3
79
86
  summary: Use the fork