trident 0.1.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.
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