trident 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - jruby-19mode
6
+ - rbx-19mode
7
+
8
+ script: bundle exec rake
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in trident.gemspec
4
+ gemspec
5
+
6
+ # for code coverage during travis-ci test runs
7
+ gem 'coveralls', :require => false
8
+
9
+ gem "mocha", :require => false
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- encoding: utf-8 -*-
3
+
4
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
5
+
6
+ require 'trident'
7
+ Trident::CLI.run
8
+
data/lib/trident.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'gem_logger'
2
+ require "trident/version"
3
+ require "trident/utils"
4
+ require "trident/cli"
5
+ require "trident/pool"
6
+ require "trident/pool_handler"
7
+ require "trident/pool_manager"
8
+ require "trident/signal_handler"
@@ -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
@@ -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