sponges 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -22,7 +22,7 @@ Internally, `sponges` strongly relies on Unix signals.
22
22
 
23
23
  ## Installation
24
24
 
25
- Ruby 1.9.2 (or superior) and Redis are required.
25
+ Ruby 1.9.2 (or superior).
26
26
 
27
27
  Install it with rubygems:
28
28
 
@@ -66,15 +66,14 @@ Sponges.configure do |config|
66
66
  config.size = 3
67
67
  config.daemonize = true
68
68
  config.after_fork do
69
- puts "Execute some when a child process is created"
69
+ puts "Execute code when a child process is created"
70
70
  end
71
71
  config.on_chld do
72
72
  puts "Execute code when a child process is killed"
73
73
  end
74
74
 
75
75
  # Register a pool named "worker_name".
76
- # Options are optionnal. Consider it as a default, options given by command have
77
- # the precedence.
76
+ #
78
77
  Sponges.start "worker_name" do
79
78
  Worker.new({some: args}).run
80
79
  end
@@ -99,9 +98,10 @@ Start 8 instances of the worker and daemonize them:
99
98
  ruby example.rb start -d -s 8 # By default, size equals cpu core's size.
100
99
  ```
101
100
 
102
- Retart gracefully 4 instances of the worker and daemonize them:
101
+ Retart gracefully 4 instances of the worker, with a timeout of 3 seconds and
102
+ daemonize them:
103
103
  ``` bash
104
- ruby example.rb restart -g -d -s 4
104
+ ruby example.rb restart -g -d -s 4 -t 3
105
105
  ```
106
106
 
107
107
  Stop workers with a `QUIT` signal :
@@ -116,10 +116,12 @@ ruby example.rb kill
116
116
 
117
117
  Stop workers with a `HUP` signal :
118
118
  ``` bash
119
- ruby example.rb stop -g
119
+ ruby example.rb stop -g -t 5
120
120
  ```
121
121
  In this case, you will have to trap the `HUP` signal, and handle a clean stop
122
- from each workers. The point is to wait for a task to be done before quitting.
122
+ from each workers. The point is to wait for a task to be done before quitting. A
123
+ timeout can be specify with the `-t` option. When this timeout is hited, the
124
+ process is killed.
123
125
 
124
126
  Increment worker's pool size :
125
127
  ``` bash
@@ -136,6 +138,24 @@ Show a list of workers and their children.
136
138
  ruby example.rb list
137
139
  ```
138
140
 
141
+ ## Stores
142
+
143
+ sponges can store pids in memory or in redis. Memory is the default store. The
144
+ `list` command is not available with the memory store.
145
+
146
+ To select the redis store, you need to add `nest` to your application's
147
+ Gemfile, and do the following.
148
+
149
+ ``` ruby
150
+ gem "sponges"
151
+ ```
152
+
153
+ ``` ruby
154
+ Sponges.configure do |config|
155
+ config.store = :redis
156
+ end
157
+ ```
158
+
139
159
  ## Acknowledgements
140
160
 
141
161
  sponges would not have been the same without [Jesse
data/lib/sponges.rb CHANGED
@@ -2,13 +2,15 @@
2
2
  require 'boson/runner'
3
3
  require 'socket'
4
4
  require 'logger'
5
- require 'nest'
6
5
  require 'machine'
7
6
  require_relative 'sponges/configuration'
8
7
  require_relative 'sponges/supervisor'
9
8
  require_relative 'sponges/runner'
10
9
  require_relative 'sponges/commander'
11
10
  require_relative 'sponges/cli'
11
+ require_relative 'sponges/store'
12
+ require_relative 'sponges/store/memory'
13
+ require_relative 'sponges/store/redis'
12
14
 
13
15
  module Sponges
14
16
  SIGNALS = [:INT, :QUIT, :TERM]
data/lib/sponges/cli.rb CHANGED
@@ -17,8 +17,13 @@ module Sponges
17
17
  end
18
18
 
19
19
  option :gracefully, type: :boolean
20
+ option :timeout, type: :numeric
20
21
  desc "Stop workers"
21
22
  def stop(options = {})
23
+ options = {
24
+ timeout: Sponges::Configuration.timeout,
25
+ gracefully: Sponges::Configuration.gracefully
26
+ }.reject{|k, v| v.nil?}.merge(options)
22
27
  Sponges::Commander.new(Sponges::Configuration.worker_name, options).stop
23
28
  end
24
29
 
@@ -30,6 +35,7 @@ module Sponges
30
35
  option :daemonize, type: :boolean
31
36
  option :size, type: :numeric
32
37
  option :gracefully, type: :boolean
38
+ option :timeout, type: :numeric
33
39
  desc "Restart workers"
34
40
  def restart(options = {})
35
41
  stop(options)
@@ -50,6 +56,10 @@ module Sponges
50
56
 
51
57
  desc "Show running processes"
52
58
  def list
59
+ if Sponges::Configuration.store == :memory
60
+ puts "Command not available with the memory store"
61
+ exit
62
+ end
53
63
  redis = Nest.new('sponges')
54
64
  puts %q{
55
65
  ___ _ __ ___ _ __ __ _ ___ ___
@@ -1,12 +1,16 @@
1
1
  # encoding: utf-8
2
+ require 'timeout'
3
+
2
4
  module Sponges
3
5
  # This class concern is to send messages to supervisor. It's used to send
4
6
  # messages like 'stop' or 'restart'
5
7
  #
6
8
  class Commander
9
+ attr_reader :store
10
+
7
11
  def initialize(name, options = {})
8
12
  @name, @options = name, options
9
- @redis = Nest.new('sponges', Configuration.redis || Redis.new)[Socket.gethostname]
13
+ @store = Sponges::Store.new(@name)
10
14
  end
11
15
 
12
16
  # Kills the supervisor, and then all workers.
@@ -14,8 +18,14 @@ module Sponges
14
18
  def kill
15
19
  Sponges.logger.info "Runner #{@name} kill message received."
16
20
  stop :KILL
17
- Array(@redis[:worker][@name][:pids].smembers).each do|pid|
18
- kill_process(:KILL, pid, "Worker")
21
+ children_pids.each do|pid|
22
+ begin
23
+ kill_process(:KILL, pid, "Worker")
24
+ rescue Errno::ESRCH => e
25
+ # Don't panic
26
+ ensure
27
+ store.delete_children pid
28
+ end
19
29
  end
20
30
  end
21
31
 
@@ -24,8 +34,18 @@ module Sponges
24
34
  def stop(signal = nil)
25
35
  signal ||= gracefully? ? :HUP : :QUIT
26
36
  Sponges.logger.info "Runner #{@name} stop message received."
27
- if pid = @redis[:worker][@name][:supervisor].get
28
- kill_process(signal, pid)
37
+ if pid = supervisor_pid
38
+ if @options[:timeout]
39
+ begin
40
+ Timeout::timeout(@options[:timeout]) do
41
+ kill_process(signal, pid)
42
+ end
43
+ rescue Timeout::Error
44
+ kill
45
+ end
46
+ else
47
+ kill_process(signal, pid)
48
+ end
29
49
  else
30
50
  Sponges.logger.info "No supervisor found."
31
51
  end
@@ -35,9 +55,9 @@ module Sponges
35
55
  #
36
56
  def increment
37
57
  Sponges.logger.info "Runner #{@name} increment message received."
38
- if pid = @redis[:worker][@name][:supervisor].get
58
+ if pid = supervisor_pid
39
59
  begin
40
- Process.kill :TTIN, pid.to_i
60
+ Process.kill :TTIN, supervisor.to_i
41
61
  rescue Errno::ESRCH => e
42
62
  Sponges.logger.error e
43
63
  end
@@ -50,9 +70,9 @@ module Sponges
50
70
  #
51
71
  def decrement
52
72
  Sponges.logger.info "Runner #{@name} decrement message received."
53
- if pid = @redis[:worker][@name][:supervisor].get
73
+ if supervisor_pid
54
74
  begin
55
- Process.kill :TTOU, pid.to_i
75
+ Process.kill :TTOU, supervisor_pid.to_i
56
76
  rescue Errno::ESRCH => e
57
77
  Sponges.logger.error e
58
78
  end
@@ -63,6 +83,14 @@ module Sponges
63
83
 
64
84
  private
65
85
 
86
+ def supervisor_pid
87
+ @store.supervisor_pid
88
+ end
89
+
90
+ def children_pids
91
+ @store.children_pids
92
+ end
93
+
66
94
  def kill_process(signal, pid, type = "Supervisor")
67
95
  pid = pid.to_i
68
96
  begin
@@ -73,7 +101,7 @@ module Sponges
73
101
  end
74
102
  Sponges.logger.info "#{type} #{pid} has stopped."
75
103
  rescue Errno::ESRCH => e
76
- Sponges.logger.error e
104
+ # Don't panic
77
105
  end
78
106
  end
79
107
 
@@ -5,7 +5,7 @@ module Sponges
5
5
  class Configuration
6
6
  class << self
7
7
  ACCESSOR = [:worker_name, :worker, :logger, :redis, :size,
8
- :daemonize, :after_fork
8
+ :daemonize, :after_fork, :timeout, :gracefully, :store
9
9
  ]
10
10
  attr_accessor *ACCESSOR
11
11
 
@@ -27,6 +27,14 @@ module Sponges
27
27
  def on_chld(&block)
28
28
  Hook._on_chld = block
29
29
  end
30
+
31
+ def store
32
+ @store || :memory
33
+ end
34
+
35
+ def redis
36
+ @redis ||= Redis.new
37
+ end
30
38
  end
31
39
  end
32
40
 
@@ -4,15 +4,17 @@ module Sponges
4
4
  # watch over the supervisor.
5
5
  #
6
6
  class Runner
7
+ attr_reader :store
8
+
7
9
  def initialize(name, options = {}, block)
8
10
  @name, @block = name, block
9
11
  @options = default_options.merge options
10
- @redis = Nest.new('sponges', Configuration.redis || Redis.new)
11
- if running?
12
+ @store = Sponges::Store.new(@name)
13
+ if store.running?
12
14
  Sponges.logger.error "Runner #{@name} already started."
13
15
  exit
14
16
  end
15
- @redis[:hostnames].sadd Socket.gethostname
17
+ store.register_hostname Socket.gethostname
16
18
  end
17
19
 
18
20
  def start
@@ -29,20 +31,6 @@ module Sponges
29
31
 
30
32
  private
31
33
 
32
- def running?
33
- if pid = @redis[Socket.gethostname][:worker][@name][:supervisor].get
34
- begin
35
- Process.kill 0, pid.to_i
36
- true
37
- rescue Errno::ESRCH => e
38
- @redis[Socket.gethostname][:worker][@name][:supervisor].del
39
- false
40
- end
41
- else
42
- false
43
- end
44
- end
45
-
46
34
  def trap_signals
47
35
  Sponges::SIGNALS.each do |signal|
48
36
  trap(signal) {|signal| kill_supervisor(signal) }
@@ -63,7 +51,7 @@ module Sponges
63
51
  def fork_supervisor
64
52
  fork do
65
53
  $PROGRAM_NAME = "#{@name}_supervisor"
66
- Supervisor.new(@name, @options, @block).start
54
+ Supervisor.new(@name, @options, store, @block).start
67
55
  end
68
56
  end
69
57
 
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+ module Sponges
3
+ class Store
4
+ class << self
5
+ def new(name)
6
+ case Sponges::Configuration.store
7
+ when :memory
8
+ Sponges::Store::Memory.new(name)
9
+ when :redis
10
+ Sponges::Store::Redis.new(name)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,65 @@
1
+ # encoding: utf-8
2
+ require 'forwardable'
3
+
4
+ module Sponges
5
+ class Store
6
+ class Memory
7
+ extend Forwardable
8
+
9
+ attr_writer :supervisor_pid
10
+ attr_reader :pids, :name
11
+
12
+ def initialize(name)
13
+ @pids, @name = [], name
14
+ end
15
+
16
+ def_delegator :@pids, :<<, :add_children
17
+ def_delegator :@pids, :delete, :delete_children
18
+
19
+ def supervisor_pid
20
+ return @supervisor_pid if @supervisor_pid
21
+ s = find_supervisor
22
+ @supervisor_pid = s.pid if s
23
+ end
24
+
25
+ def children_pids
26
+ @pids.any? ? @pids : find_childs.map(&:pid)
27
+ end
28
+
29
+ def running?
30
+ !!find_supervisor
31
+ end
32
+
33
+ def register(supervisor_pid)
34
+ @supervisor_pid = supervisor_pid
35
+ end
36
+
37
+ def register_hostname(hostname)
38
+ end
39
+
40
+ def on_fork
41
+ end
42
+
43
+ def clear
44
+ end
45
+
46
+ private
47
+
48
+ def supervisor_name
49
+ "#{name}_supervisor"
50
+ end
51
+
52
+ def childs_name
53
+ "#{name}_child"
54
+ end
55
+
56
+ def find_supervisor
57
+ Sys::ProcTable.ps.select {|f| f.cmdline == supervisor_name }.first
58
+ end
59
+
60
+ def find_childs
61
+ Sys::ProcTable.ps.select {|f| f.cmdline =~ /^#{childs_name}/ }
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,74 @@
1
+ # encoding: utf-8
2
+ module Sponges
3
+ class Store
4
+ class Redis
5
+ attr_reader :redis, :hostname_store, :name
6
+ private :redis, :hostname_store
7
+
8
+ def initialize(name)
9
+ @name = name
10
+ @redis ||= Nest.new('sponges', Configuration.redis)
11
+ @hostname_store = @redis[Socket.gethostname]
12
+ end
13
+
14
+ def supervisor_pid
15
+ hostname_store[:worker][name][:supervisor].get
16
+ end
17
+
18
+ def children_pids
19
+ Array(hostname_store[:worker][name][:pids].smembers)
20
+ end
21
+
22
+ def running?
23
+ if pid = hostname_store[:worker][name][:supervisor].get
24
+ begin
25
+ Process.kill 0, pid.to_i
26
+ true
27
+ rescue Errno::ESRCH => e
28
+ hostname_store[:worker][name][:supervisor].del
29
+ false
30
+ end
31
+ else
32
+ false
33
+ end
34
+ end
35
+
36
+ def register_hostname(hostname)
37
+ redis[:hostnames].sadd hostname
38
+ end
39
+
40
+ def register(supervisor_pid)
41
+ hostname_store[:workers].sadd name
42
+ hostname_store[:worker][name][:supervisor].set supervisor_pid
43
+ end
44
+
45
+ def add_children(pid)
46
+ hostname_store[:worker][name][:pids].sadd pid
47
+ end
48
+
49
+ def delete_children(pid)
50
+ hostname_store[:worker][name][:pids].srem pid
51
+ end
52
+
53
+ def clear(name)
54
+ hostname_store[:worker][name][:supervisor].del
55
+ hostname_store[:workers].srem name
56
+ end
57
+
58
+ def on_fork
59
+ Sponges::Configuration.redis.client.reconnect
60
+ end
61
+
62
+ private
63
+
64
+ def find_supervisor
65
+ Sys::ProcTable.ps.select {|f| f.cmdline == supervisor_name }.first
66
+ end
67
+
68
+ def find_childs
69
+ Sys::ProcTable.ps.select {|f| f.cmdline =~ /^#{childs_name}/ }
70
+ end
71
+ end
72
+ end
73
+ end
74
+
@@ -1,15 +1,17 @@
1
1
  # encoding: utf-8
2
2
  module Sponges
3
3
  class Supervisor
4
- def initialize(name, options, block)
5
- @name, @options, @block = name, options, block
6
- set_up_redis
7
- @pids = @redis[:worker][name][:pids]
4
+ attr_reader :store, :name, :options
5
+
6
+ def initialize(name, options, store, block)
7
+ @name, @options, @store, @block = name, options, store, block
8
+ store.on_fork
9
+ store.register Process.pid
8
10
  @children_seen = 0
9
11
  end
10
12
 
11
13
  def start
12
- @options[:size].times do
14
+ options[:size].times do
13
15
  fork_children
14
16
  end
15
17
  trap_signals
@@ -22,16 +24,6 @@ module Sponges
22
24
 
23
25
  private
24
26
 
25
- def set_up_redis
26
- if Configuration.redis
27
- redis_client = Configuration.redis
28
- redis_client.client.reconnect
29
- end
30
- @redis = Nest.new('sponges', redis_client || Redis.new)[Socket.gethostname]
31
- @redis[:workers].sadd @name
32
- @redis[:worker][@name][:supervisor].set Process.pid
33
- end
34
-
35
27
  def fork_children
36
28
  name = children_name
37
29
  pid = fork do
@@ -40,11 +32,11 @@ module Sponges
40
32
  @block.call
41
33
  end
42
34
  Sponges.logger.info "Supervisor create a child with #{pid} pid."
43
- @pids.sadd pid
35
+ store.add_children pid
44
36
  end
45
37
 
46
38
  def children_name
47
- "#{@name}_child_#{@children_seen +=1}"
39
+ "#{name}_child_#{@children_seen +=1}"
48
40
  end
49
41
 
50
42
  def trap_signals
@@ -59,19 +51,19 @@ module Sponges
59
51
  end
60
52
  trap(:TTOU) do
61
53
  Sponges.logger.warn "Supervisor decrement child's pool by one."
62
- if pids.first
63
- kill_one(pids.first, :HUP)
54
+ if store.children_pids.first
55
+ kill_one(store.children_pids.first, :HUP)
64
56
  else
65
57
  Sponges.logger.warn "No more child to kill."
66
58
  end
67
59
  end
68
60
  trap(:CHLD) do
69
- pids.each do |pid|
61
+ store.children_pids.each do |pid|
70
62
  begin
71
63
  dead = Process.waitpid(pid.to_i, Process::WNOHANG)
72
64
  if dead
73
65
  Sponges.logger.warn "Child #{dead} died. Restarting a new one..."
74
- @pids.srem dead
66
+ store.delete_children dead
75
67
  Sponges::Hook.on_chld
76
68
  fork_children
77
69
  end
@@ -88,30 +80,26 @@ module Sponges
88
80
  Process.waitall
89
81
  Sponges.logger.info "Children shutdown complete."
90
82
  Sponges.logger.info "Supervisor shutdown. Exiting..."
91
- pid = @redis[:worker][@name][:supervisor]
92
- @redis[:worker][@name][:supervisor].del
93
- @redis[:workers].srem @name
83
+ pid = store.supervisor_pid
84
+ store.clear(name)
94
85
  Process.kill :USR1, pid.to_i
95
86
  end
96
87
 
97
88
  def kill_them_all(signal)
98
- pids.each do |pid|
89
+ store.children_pids.each do |pid|
99
90
  kill_one(pid, signal)
100
91
  end
101
92
  end
102
93
 
103
94
  def kill_one(pid, signal)
104
95
  begin
105
- @pids.srem pid
106
96
  Process.kill signal, pid.to_i
97
+ Process.waitpid pid.to_i
98
+ store.delete_children pid
107
99
  Sponges.logger.info "Child #{pid} receive a #{signal} signal."
108
100
  rescue Errno::ESRCH => e
109
101
  # Don't panic
110
102
  end
111
103
  end
112
-
113
- def pids
114
- Array(@pids.smembers)
115
- end
116
104
  end
117
105
  end
@@ -1,4 +1,4 @@
1
1
  # encoding: utf-8
2
2
  module Sponges
3
- VERSION = "0.2.0"
3
+ VERSION = "0.3.0"
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sponges
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-23 00:00:00.000000000 Z
12
+ date: 2012-12-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: boson
@@ -27,22 +27,6 @@ dependencies:
27
27
  - - ! '>='
28
28
  - !ruby/object:Gem::Version
29
29
  version: '0'
30
- - !ruby/object:Gem::Dependency
31
- name: nest
32
- requirement: !ruby/object:Gem::Requirement
33
- none: false
34
- requirements:
35
- - - ! '>='
36
- - !ruby/object:Gem::Version
37
- version: '0'
38
- type: :runtime
39
- prerelease: false
40
- version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
- requirements:
43
- - - ! '>='
44
- - !ruby/object:Gem::Version
45
- version: '0'
46
30
  - !ruby/object:Gem::Dependency
47
31
  name: machine
48
32
  requirement: !ruby/object:Gem::Requirement
@@ -92,6 +76,9 @@ files:
92
76
  - lib/sponges/commander.rb
93
77
  - lib/sponges/configuration.rb
94
78
  - lib/sponges/runner.rb
79
+ - lib/sponges/store.rb
80
+ - lib/sponges/store/memory.rb
81
+ - lib/sponges/store/redis.rb
95
82
  - lib/sponges/supervisor.rb
96
83
  - lib/sponges/version.rb
97
84
  homepage: https://github.com/AF83/sponges