trident 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +17 -0
- data/.travis.yml +8 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +33 -0
- data/Rakefile +18 -0
- data/bin/trident +8 -0
- data/lib/trident.rb +8 -0
- data/lib/trident/cli.rb +130 -0
- data/lib/trident/pool.rb +108 -0
- data/lib/trident/pool_handler.rb +31 -0
- data/lib/trident/pool_manager.rb +79 -0
- data/lib/trident/signal_handler.rb +167 -0
- data/lib/trident/utils.rb +9 -0
- data/lib/trident/version.rb +3 -0
- data/test/fixtures/integration_project/config/trident.yml +51 -0
- data/test/integration/trident_test.rb +105 -0
- data/test/test_helper.rb +144 -0
- data/test/unit/trident/cli_test.rb +253 -0
- data/test/unit/trident/pool_handler_test.rb +70 -0
- data/test/unit/trident/pool_manager_test.rb +131 -0
- data/test/unit/trident/pool_test.rb +233 -0
- data/test/unit/trident/signal_handler_test.rb +262 -0
- data/test/unit/trident/utils_test.rb +20 -0
- data/trident.example.yml +49 -0
- data/trident.gemspec +29 -0
- metadata +180 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NDRhNzEyMDc3ZGY3NGM3MDAzMzNkNWJmYTM5ODUxOTA4OTljN2YyMg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MjQ2YWVmMzAwZjEzNTUwZTkwNzJlOGU3YzNhMjc4ZTNlNTU2ZTdhYw==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ZTc0NGI3MjljOTg4YzI1MDhkMDg0YWVjNTUxY2ZiYjkxN2YyMDkyMGRhYzZm
|
10
|
+
MDRjMmQ4MDA0MmRjMGVjNmM2NWI1OTMwNDUyYTU2ZWE1YjI2MjA3YjZkN2Zm
|
11
|
+
NzY3NWZkZWJiYTBiODM5Njk4MDU1MWZmMGVjOTc3OTk0YmRhZDI=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
NTljZGRhZDc2NDBkYmVjYjkzZGM2OTM0M2Q0NmYzYWNlZjc3MmYxNzFkM2Uy
|
14
|
+
YWNkMWIxMzM4MzhkOTBmODVjNjkyMzliOWMyYTUyOTFmMTk3OWJlZTQ0Mjgy
|
15
|
+
NjllMmJlZjkyN2U3NWM4NzlhNjNhZDhjOGVjODZiZjY5ZDMyNDk=
|
data/.coveralls.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
service_name: travis-ci
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Matt Conway
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# Trident
|
2
|
+
|
3
|
+
A ruby gem for managing pools of forked workers
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'trident'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install trident
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
After installing the gem, use the 'trident' binary to generate an example configuration file:
|
22
|
+
|
23
|
+
trident --generate-config > config/trident.yml
|
24
|
+
|
25
|
+
Edit the file with your desired setup, then run trident to launch all your worker pools
|
26
|
+
|
27
|
+
See other command line options with
|
28
|
+
trident --help
|
29
|
+
|
30
|
+
|
31
|
+
## TODO
|
32
|
+
|
33
|
+
* Add support for reloading the trident config with a HUP signal
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
|
5
|
+
Rake::TestTask.new do |t|
|
6
|
+
t.pattern = 'test/**/*_test.rb'
|
7
|
+
t.libs.push 'test'
|
8
|
+
end
|
9
|
+
|
10
|
+
task :default => :test
|
11
|
+
|
12
|
+
task :console do
|
13
|
+
Bundler.require
|
14
|
+
require "trident"
|
15
|
+
ARGV.clear
|
16
|
+
require "irb"
|
17
|
+
IRB.start
|
18
|
+
end
|
data/bin/trident
ADDED
data/lib/trident.rb
ADDED
data/lib/trident/cli.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'clamp'
|
2
|
+
require 'erb'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Trident
|
6
|
+
class CLI < Clamp::Command
|
7
|
+
|
8
|
+
include GemLogger::LoggerSupport
|
9
|
+
include Trident::Utils
|
10
|
+
|
11
|
+
def self.description
|
12
|
+
"Starts the trident pool manager"
|
13
|
+
end
|
14
|
+
|
15
|
+
option "--verbose",
|
16
|
+
:flag, "verbose output\n",
|
17
|
+
:default => false
|
18
|
+
option "--generate-config",
|
19
|
+
:flag, "generates an example config file to stdout"
|
20
|
+
option "--config",
|
21
|
+
"FILENAME", "use the given config file\n",
|
22
|
+
:default => "config/trident.yml"
|
23
|
+
option "--logfile",
|
24
|
+
"FILENAME", "log to the given file"
|
25
|
+
option "--pidfile",
|
26
|
+
"FILENAME", "store pid in the given pidfile"
|
27
|
+
option "--daemon",
|
28
|
+
:flag, "run as a daemon",
|
29
|
+
:default => false
|
30
|
+
option "--pool",
|
31
|
+
"POOL", "only run the given pool(s)",
|
32
|
+
:multivalued => true
|
33
|
+
|
34
|
+
|
35
|
+
def execute
|
36
|
+
if generate_config?
|
37
|
+
puts File.read(File.expand_path("../../../trident.example.yml", __FILE__))
|
38
|
+
exit(0)
|
39
|
+
end
|
40
|
+
|
41
|
+
procline "cli", "(initializing)"
|
42
|
+
|
43
|
+
self.logfile = expand_path(logfile)
|
44
|
+
self.pidfile = expand_path(pidfile)
|
45
|
+
self.config = expand_path(config)
|
46
|
+
|
47
|
+
GemLogger.default_logger = Logger.new(logfile ? logfile : STDOUT)
|
48
|
+
GemLogger.default_logger.level = verbose? ? Logger::DEBUG : Logger::INFO
|
49
|
+
$stdout.sync = $stderr.sync = true
|
50
|
+
|
51
|
+
if daemon? && (logfile.nil? || pidfile.nil?)
|
52
|
+
signal_usage_error "--logfile and --pidfile are required when running as a daemon"
|
53
|
+
end
|
54
|
+
|
55
|
+
logger.info "Loading config from: #{config}"
|
56
|
+
config_hash = load_config(config)
|
57
|
+
|
58
|
+
if GC.respond_to?(:copy_on_write_friendly=)
|
59
|
+
GC.copy_on_write_friendly = true
|
60
|
+
end
|
61
|
+
|
62
|
+
daemonize(logfile) if daemon?
|
63
|
+
File.write(pidfile, Process.pid.to_s) if pidfile
|
64
|
+
|
65
|
+
handlers = create_handlers(config_hash['handlers'])
|
66
|
+
pools = create_pools(config_hash['pools'], handlers, pool_list)
|
67
|
+
|
68
|
+
manager = Trident::PoolManager.new(config_hash['application'],
|
69
|
+
pools.values,
|
70
|
+
config_hash['prefork'] == true)
|
71
|
+
sh_thread = Thread.new { Trident::SignalHandler.start(config_hash['signals'], manager) }
|
72
|
+
manager.start
|
73
|
+
sh_thread.join
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def project_root
|
79
|
+
@root ||= ENV['BUNDLE_GEMFILE'] ? "#{File.dirname(ENV['BUNDLE_GEMFILE'])}" : "."
|
80
|
+
end
|
81
|
+
|
82
|
+
def expand_path(path)
|
83
|
+
if path && path !~ /^\//
|
84
|
+
File.expand_path("#{project_root}/#{path}")
|
85
|
+
else
|
86
|
+
path
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Configure through yaml file
|
91
|
+
def load_config(path_to_yaml_file)
|
92
|
+
config = YAML::load(ERB.new(IO.read(path_to_yaml_file)).result)
|
93
|
+
config = config[Rails.env.to_s] if defined?(::Rails) && config.has_key?(Rails.env.to_s)
|
94
|
+
config
|
95
|
+
end
|
96
|
+
|
97
|
+
def daemonize(logfile)
|
98
|
+
Process.daemon
|
99
|
+
$stdout.reopen(logfile, "a")
|
100
|
+
$stderr.reopen(logfile, "a")
|
101
|
+
end
|
102
|
+
|
103
|
+
def create_handlers(handlers_config_hash)
|
104
|
+
handlers = {}
|
105
|
+
handlers_config_hash.each do |name, handler_config|
|
106
|
+
handler = Trident::PoolHandler.new(name,
|
107
|
+
handler_config['class'],
|
108
|
+
handler_config['environment'],
|
109
|
+
handler_config['signals'],
|
110
|
+
handler_config['options'])
|
111
|
+
handlers[name] = handler
|
112
|
+
end
|
113
|
+
handlers
|
114
|
+
end
|
115
|
+
|
116
|
+
def create_pools(pools_config_hash, handlers, pool_filter=[])
|
117
|
+
pools = {}
|
118
|
+
pools_config_hash.each do |name, pool_config|
|
119
|
+
handler = handlers[pool_config['handler']]
|
120
|
+
raise "No handler defined: #{pool_config['handler']}" unless handler
|
121
|
+
|
122
|
+
next if pool_filter.size > 0 && ! pool_filter.include?(name)
|
123
|
+
|
124
|
+
pool = Trident::Pool.new(name, handler, pool_config['size'], pool_config['options'])
|
125
|
+
pools[name] = pool
|
126
|
+
end
|
127
|
+
pools
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
data/lib/trident/pool.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
module Trident
|
2
|
+
class Pool
|
3
|
+
include GemLogger::LoggerSupport
|
4
|
+
include Trident::Utils
|
5
|
+
|
6
|
+
attr_reader :name, :handler, :size, :options, :workers
|
7
|
+
|
8
|
+
def initialize(name, handler, size, options={})
|
9
|
+
@name = name
|
10
|
+
@handler = handler
|
11
|
+
@size = size
|
12
|
+
@options = options || {}
|
13
|
+
@workers = Set.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def start
|
17
|
+
logger.info "<pool-#{name}> Starting pool"
|
18
|
+
maintain_worker_count('stop_gracefully')
|
19
|
+
logger.info "<pool-#{name}> Pool started with #{workers.size} workers"
|
20
|
+
end
|
21
|
+
|
22
|
+
def stop(action='stop_gracefully')
|
23
|
+
logger.info "<pool-#{name}> Stopping pool"
|
24
|
+
@size = 0
|
25
|
+
maintain_worker_count(action)
|
26
|
+
logger.info "<pool-#{name}> Pool stopped"
|
27
|
+
end
|
28
|
+
|
29
|
+
def wait
|
30
|
+
logger.info "<pool-#{name}> Waiting for pool"
|
31
|
+
cleanup_dead_workers(true)
|
32
|
+
logger.info "<pool-#{name}> Wait complete"
|
33
|
+
end
|
34
|
+
|
35
|
+
def update
|
36
|
+
logger.info "<pool-#{name}> Updating pool"
|
37
|
+
maintain_worker_count('stop_gracefully')
|
38
|
+
logger.info "<pool-#{name}> Pool up to date"
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def maintain_worker_count(kill_action)
|
44
|
+
cleanup_dead_workers(false)
|
45
|
+
|
46
|
+
if size > workers.size
|
47
|
+
spawn_workers(size - workers.size)
|
48
|
+
elsif size < workers.size
|
49
|
+
kill_workers(workers.size - size, kill_action)
|
50
|
+
else
|
51
|
+
logger.debug "<pool-#{name}> Worker count is correct"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def cleanup_dead_workers(blocking=true)
|
56
|
+
wait_flags = blocking ? 0 : Process::WNOHANG
|
57
|
+
workers.clone.each do |pid|
|
58
|
+
begin
|
59
|
+
wpid = Process.wait(pid, wait_flags)
|
60
|
+
rescue Errno::EINTR
|
61
|
+
logger.warn("<pool-#{name}> Interrupted cleaning up workers, retrying")
|
62
|
+
retry
|
63
|
+
rescue Errno::ECHILD
|
64
|
+
logger.warn("<pool-#{name}> Error cleaning up workers, ignoring")
|
65
|
+
# Calling process.wait on a pid that was already waited on throws
|
66
|
+
# a ECHLD, so may as well remove it from our list of workers
|
67
|
+
wpid = pid
|
68
|
+
end
|
69
|
+
workers.delete(wpid) if wpid
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def spawn_workers(count)
|
74
|
+
logger.info "<pool-#{name}> Spawning #{count} workers"
|
75
|
+
count.times do
|
76
|
+
spawn_worker
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def kill_workers(count, action)
|
81
|
+
logger.info "<pool-#{name}> Killing #{count} workers with #{action}"
|
82
|
+
workers.to_a[-count, count].each do |pid|
|
83
|
+
kill_worker(pid, action)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def spawn_worker
|
88
|
+
pid = fork do
|
89
|
+
procline "pool-#{name}-worker", "starting handler #{handler.name}"
|
90
|
+
Trident::SignalHandler.reset_for_fork
|
91
|
+
handler.load
|
92
|
+
handler.start(options)
|
93
|
+
end
|
94
|
+
workers << pid
|
95
|
+
logger.info "<pool-#{name}> Spawned worker #{pid}, worker count now at #{workers.size}"
|
96
|
+
end
|
97
|
+
|
98
|
+
def kill_worker(pid, action)
|
99
|
+
sig = handler.signal_for(action)
|
100
|
+
raise "<pool-#{name}> No signal for action: #{action}" unless sig
|
101
|
+
logger.info "<pool-#{name}> Sending signal to worker: #{pid}/#{sig}/#{action}"
|
102
|
+
Process.kill(sig, pid)
|
103
|
+
workers.delete(pid)
|
104
|
+
logger.info "<pool-#{name}> Killed worker #{pid}, worker count now at #{workers.size}"
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Trident
|
2
|
+
class PoolHandler
|
3
|
+
|
4
|
+
attr_reader :name, :worker_class_name, :environment, :signal_mappings, :options
|
5
|
+
|
6
|
+
def initialize(name, worker_class_name, environment, signal_mappings, options={})
|
7
|
+
@name = name
|
8
|
+
@worker_class_name = worker_class_name
|
9
|
+
@environment = environment
|
10
|
+
@signal_mappings = signal_mappings
|
11
|
+
@options = options || {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def load
|
15
|
+
eval environment if environment
|
16
|
+
end
|
17
|
+
|
18
|
+
def worker_class
|
19
|
+
self.class.const_get(worker_class_name)
|
20
|
+
end
|
21
|
+
|
22
|
+
def start(opts={})
|
23
|
+
worker_class.new(self.options.merge(opts)).start
|
24
|
+
end
|
25
|
+
|
26
|
+
def signal_for(action)
|
27
|
+
signal_mappings[action] || signal_mappings['default'] || "SIGTERM"
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Trident
|
2
|
+
class PoolManager
|
3
|
+
include GemLogger::LoggerSupport
|
4
|
+
include Trident::Utils
|
5
|
+
|
6
|
+
attr_reader :name, :pools, :prefork
|
7
|
+
|
8
|
+
def initialize(name, pools, prefork)
|
9
|
+
logger.info "Initializing pool manager"
|
10
|
+
procline "manager-#{name}", "(initializing)"
|
11
|
+
@name = name
|
12
|
+
@pools = pools
|
13
|
+
@prefork = prefork
|
14
|
+
end
|
15
|
+
|
16
|
+
def start
|
17
|
+
logger.info "Starting pools"
|
18
|
+
load_handlers if prefork
|
19
|
+
pools.each do |pool|
|
20
|
+
pool.start
|
21
|
+
end
|
22
|
+
procline "manager", "managing #{procline_display}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def stop_forcefully
|
26
|
+
stop('stop_forcefully')
|
27
|
+
end
|
28
|
+
|
29
|
+
def stop_gracefully
|
30
|
+
stop('stop_gracefully')
|
31
|
+
end
|
32
|
+
|
33
|
+
# waits for children to exit
|
34
|
+
def wait
|
35
|
+
logger.info "Waiting for pools to exit"
|
36
|
+
procline "manager", "waiting #{procline_display}"
|
37
|
+
pools.each do |pool|
|
38
|
+
pool.wait
|
39
|
+
end
|
40
|
+
:break
|
41
|
+
end
|
42
|
+
|
43
|
+
def update
|
44
|
+
pools.each do |pool|
|
45
|
+
pool.update
|
46
|
+
end
|
47
|
+
procline "manager", "managing #{procline_display}"
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def procline_display
|
53
|
+
display = ""
|
54
|
+
pools.each_with_index do |pool, i|
|
55
|
+
display << "#{pool.name}#{pool.workers.to_a.inspect}"
|
56
|
+
display << " " if i + 1 < pools.size
|
57
|
+
end
|
58
|
+
display
|
59
|
+
end
|
60
|
+
|
61
|
+
def load_handlers
|
62
|
+
procline "manager", "preforking #{procline_display}"
|
63
|
+
pools.each do |pool|
|
64
|
+
pool.handler.load
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# tells all children to stop using action
|
69
|
+
def stop(action)
|
70
|
+
logger.info "Stopping pools: #{action}"
|
71
|
+
procline "manager", "stopping #{procline_display}"
|
72
|
+
pools.each do |pool|
|
73
|
+
pool.stop(action)
|
74
|
+
end
|
75
|
+
:break
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|