loops 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/LICENSE +21 -0
- data/README.rdoc +238 -0
- data/Rakefile +48 -0
- data/VERSION.yml +5 -0
- data/bin/loops +16 -0
- data/bin/loops-memory-stats +259 -0
- data/generators/loops/loops_generator.rb +28 -0
- data/generators/loops/templates/app/loops/APP_README +1 -0
- data/generators/loops/templates/app/loops/queue_loop.rb +8 -0
- data/generators/loops/templates/app/loops/simple_loop.rb +12 -0
- data/generators/loops/templates/config/loops.yml +34 -0
- data/generators/loops/templates/script/loops +20 -0
- data/init.rb +1 -0
- data/lib/loops.rb +167 -0
- data/lib/loops/autoload.rb +20 -0
- data/lib/loops/base.rb +148 -0
- data/lib/loops/cli.rb +35 -0
- data/lib/loops/cli/commands.rb +124 -0
- data/lib/loops/cli/options.rb +273 -0
- data/lib/loops/command.rb +36 -0
- data/lib/loops/commands/debug_command.rb +8 -0
- data/lib/loops/commands/list_command.rb +11 -0
- data/lib/loops/commands/start_command.rb +24 -0
- data/lib/loops/commands/stats_command.rb +5 -0
- data/lib/loops/commands/stop_command.rb +18 -0
- data/lib/loops/daemonize.rb +68 -0
- data/lib/loops/engine.rb +207 -0
- data/lib/loops/errors.rb +6 -0
- data/lib/loops/logger.rb +212 -0
- data/lib/loops/process_manager.rb +114 -0
- data/lib/loops/queue.rb +78 -0
- data/lib/loops/version.rb +31 -0
- data/lib/loops/worker.rb +101 -0
- data/lib/loops/worker_pool.rb +55 -0
- data/loops.gemspec +98 -0
- data/spec/loop_lock_spec.rb +61 -0
- data/spec/loops/base_spec.rb +92 -0
- data/spec/loops/cli_spec.rb +156 -0
- data/spec/loops_spec.rb +20 -0
- data/spec/rails/another_loop.rb +4 -0
- data/spec/rails/app/loops/complex_loop.rb +12 -0
- data/spec/rails/app/loops/simple_loop.rb +6 -0
- data/spec/rails/config.yml +6 -0
- data/spec/rails/config/boot.rb +1 -0
- data/spec/rails/config/environment.rb +5 -0
- data/spec/rails/config/loops.yml +13 -0
- data/spec/spec_helper.rb +110 -0
- metadata +121 -0
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2008-2009 Alexey Kovyrin
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,238 @@
|
|
1
|
+
= Simple background loops framework
|
2
|
+
|
3
|
+
<tt>loops</tt> is a small and lightweight framework for Ruby on Rails, Merb and other ruby
|
4
|
+
frameworks created to support simple background loops in your application which are usually
|
5
|
+
used to do some background data processing on your servers (queue workers, batch tasks
|
6
|
+
processors, etc).
|
7
|
+
|
8
|
+
_Warning_: If you use some pre-2.0 version of this plugin, read a dedicated paragraph below.
|
9
|
+
|
10
|
+
|
11
|
+
== What would you use it for?
|
12
|
+
|
13
|
+
Originally loops plugin was created to make our own loops code a bit more organized. We used
|
14
|
+
to have dozens of different modules with methods that were called with script/runner and then
|
15
|
+
used with nohup and other painful backgrounding techniques. When you have such a number of
|
16
|
+
loops/workers to run in background it becomes a nightmare to manage them on a regular basis
|
17
|
+
(restarts, code upgrades, status/health checking, etc).
|
18
|
+
|
19
|
+
After a few takes on writing our scripts in a more organized way we were able to generalize most
|
20
|
+
of the code so now our loops started looking like a classes with a single mandatory public method
|
21
|
+
called *run*. Everything else (spawning many workers, managing them, logging, backgrounding,
|
22
|
+
pid-files management, etc) is handled by the plugin itself.
|
23
|
+
|
24
|
+
|
25
|
+
== But there are dozens of libraries like this! Why do we need yet another one?
|
26
|
+
|
27
|
+
The major idea behind this small project was to create a deadly simple and yet robust framework to
|
28
|
+
be able to run some tasks in background and do not think about spawning many workers, restarting
|
29
|
+
them when they die, etc. So, if you need to be able to run either one or many copies of your worker
|
30
|
+
and you do not want to think about re-spawning your scripts when they die and you do not want to
|
31
|
+
spend megabytes of RAM on separate copies of Ruby interpreter (when you run each copy of your
|
32
|
+
loop as a separate process controlled by monit/god/etc), then you should try this framework --
|
33
|
+
you're going to like it.
|
34
|
+
|
35
|
+
|
36
|
+
== How to install?
|
37
|
+
|
38
|
+
There are two options when approaching db-charmer installation:
|
39
|
+
|
40
|
+
* using the gem (recommended)
|
41
|
+
* install as a Rails plugin
|
42
|
+
|
43
|
+
To install as a gem, add this to your environment.rb:
|
44
|
+
|
45
|
+
config.gem 'loops'
|
46
|
+
|
47
|
+
And then run the command:
|
48
|
+
|
49
|
+
sudo rake gems:install
|
50
|
+
|
51
|
+
To install loops as a Rails plugin you need to do rhw following:
|
52
|
+
|
53
|
+
./script/plugin install git://github.com/kovyrin/loops.git
|
54
|
+
|
55
|
+
This will install the whole package in your vendor/plugins directory.
|
56
|
+
For merb applications, just check out the code and place it to the vendor/plugins directory.
|
57
|
+
|
58
|
+
|
59
|
+
After you are done with the installation, you need to generate binary and configuration
|
60
|
+
files by running:
|
61
|
+
|
62
|
+
./script/generate loops
|
63
|
+
|
64
|
+
This will create the following list of files:
|
65
|
+
* <tt>./script/loops</tt> - binary file that will be used to manage your loops
|
66
|
+
* <tt>./config/loops.yml</tt> - example configuration file
|
67
|
+
* <tt>./app/loops/simple.rb</tt> - REALLY simple loop example
|
68
|
+
* <tt>./app/loops/queue_loop.rb</tt> - simple ActiveMQ queue worker
|
69
|
+
|
70
|
+
|
71
|
+
== How to use?
|
72
|
+
|
73
|
+
Here is a simple loop scaffold for you to start from (put this file to
|
74
|
+
<tt>app/loops/hello_world_loop.rb</tt>):
|
75
|
+
|
76
|
+
class HelloWorldLoop < Loops::Base
|
77
|
+
def run
|
78
|
+
with_period_of(1) do # period is in seconds
|
79
|
+
debug("Hello, debug log!")
|
80
|
+
sleep(config['sleep_period']) # Do something "useful" and make it configurable
|
81
|
+
debug("Hello, debug log (yes, once again)!")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
When you have your loop ready to use, add the following lines to your (maybe empty yet)
|
87
|
+
<tt>config/loops.yml</tt> file:
|
88
|
+
|
89
|
+
hello_world:
|
90
|
+
type: simple
|
91
|
+
sleep_period: 10
|
92
|
+
|
93
|
+
This is it! To manage your loop, just run one of the following commands:
|
94
|
+
|
95
|
+
* To list all configured loops:
|
96
|
+
|
97
|
+
$ ./script/loops list
|
98
|
+
|
99
|
+
* To run all enabled (actually non-disabled) loops in foreground:
|
100
|
+
|
101
|
+
$ ./script/loops start
|
102
|
+
|
103
|
+
* To run all enabled loops in background:
|
104
|
+
|
105
|
+
$ ./script/loops start -d
|
106
|
+
|
107
|
+
* To run specific loop in background:
|
108
|
+
|
109
|
+
$ ./script/loops start hello_world -d
|
110
|
+
|
111
|
+
* To see all possible options:
|
112
|
+
|
113
|
+
$ ./script/loops help
|
114
|
+
|
115
|
+
|
116
|
+
_Notice:_ If you use loops as a gem, you will get a system binary called <tt>loops</tt> which you
|
117
|
+
could use instead of your <tt>./script/loops<tt> binary:
|
118
|
+
|
119
|
+
$ loops help
|
120
|
+
|
121
|
+
|
122
|
+
== How to run more than one worker?
|
123
|
+
|
124
|
+
If you want to have more than one copy of your worker running, that is as simple as adding one
|
125
|
+
option to your loop configuration:
|
126
|
+
|
127
|
+
hello_world:
|
128
|
+
type: simple
|
129
|
+
sleep_period: 10
|
130
|
+
workers_number: 1
|
131
|
+
|
132
|
+
This <tt>workers_number</tt> option would tell loops manager to spawn more than one copy of
|
133
|
+
your loop and run them in parallel. The only thing you'd need to do is to remember about
|
134
|
+
concurrent work of your loops. For example, if you have some kind of database table with elements
|
135
|
+
you need to process, you can create a simple database-based locks system or use any
|
136
|
+
memcache-based locks.
|
137
|
+
|
138
|
+
|
139
|
+
== How to run more than one loop using the same class?
|
140
|
+
|
141
|
+
You can run the same loop class with different configuration parameters by explicitly identifying
|
142
|
+
the loop class to execute:
|
143
|
+
|
144
|
+
hello:
|
145
|
+
type: simple
|
146
|
+
loop_name: some_module/my_worker
|
147
|
+
language: English
|
148
|
+
|
149
|
+
salut:
|
150
|
+
type: simple
|
151
|
+
loop_name: some_module/my_worker
|
152
|
+
language: French
|
153
|
+
|
154
|
+
Now two independent sets of loops are using the same class <tt>SomeModule::MyWorkerLoop</tt>
|
155
|
+
customized by the language parameter.
|
156
|
+
|
157
|
+
|
158
|
+
== I want to keep my loop running on machine reboots. How to do it?
|
159
|
+
|
160
|
+
We use monit to keep loop monitors runnings. You could use something like this in your configs:
|
161
|
+
|
162
|
+
check process loop-slow_logs with pidfile /your/project/current/tmp/pids/loop-slow_logs.pid
|
163
|
+
group loops
|
164
|
+
start program "/your/project/current/script/loops start slow_logs -e loops -p tmp/pids/loop-slow_logs.pid -d"
|
165
|
+
stop program "/your/project/current/script/loops stop slow_logs -e loops -p tmp/pids/loop-slow_logs.pid"
|
166
|
+
|
167
|
+
|
168
|
+
== ActiveMQ-based workers? What's that?
|
169
|
+
|
170
|
+
In some of our worker loops we poll ActiveMQ queue and process its items to perform some
|
171
|
+
asynchronous operations. So, to make it simpler for us to create such a workers, we've created
|
172
|
+
really simple loops class extension that wraps your code with basic queue polling/acknowledging
|
173
|
+
code and as the result, you can create a loops like this:
|
174
|
+
|
175
|
+
class MyQueueLoop < Loops::Queue
|
176
|
+
def process_message(message)
|
177
|
+
debug "Received a message: #{message.body}"
|
178
|
+
debug "sleeping..."
|
179
|
+
sleep(0.5 + rand(10) / 10.0) # do something "useful" with the message :-)
|
180
|
+
debug "done..."
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
With configs like this:
|
185
|
+
|
186
|
+
# An example of a STOMP queue-based loop
|
187
|
+
my_queue:
|
188
|
+
type: queue
|
189
|
+
host: 127.0.0.1
|
190
|
+
port: 61613
|
191
|
+
queue_name: blah
|
192
|
+
|
193
|
+
Of course, this solution scales perfectly and to make your queue processing faster you just
|
194
|
+
need to add more workers (by adding <tt>workers_number: N</tt> option).
|
195
|
+
|
196
|
+
_Warning_: This type of loops requires you to have the <tt>stomp</tt> gem installed in your
|
197
|
+
system.
|
198
|
+
|
199
|
+
|
200
|
+
== There is this <tt>workers_engine</tt> option in the config file. What do you use it for?
|
201
|
+
|
202
|
+
There are two so called "workers engines" in loops: <tt>fork</tt> and <tt>thread</tt>.
|
203
|
+
They're used to control the way process manager would spawn new loops workers:
|
204
|
+
with the <tt>fork</tt> engine we'll load all the loops classes and then fork ruby interpreter
|
205
|
+
as many times as many workers we need. With the <tt>thread</tt> engine we'd do Thread.new
|
206
|
+
instead of forking. Thread engine could be useful if you are sure your loop won't lock
|
207
|
+
ruby interpreter (it does not do native calls, etc) or if you use some interpreter that does
|
208
|
+
not support forks (like jruby).
|
209
|
+
|
210
|
+
The default engine is <tt>fork</tt>.
|
211
|
+
|
212
|
+
|
213
|
+
== What Ruby implementations does it work for?
|
214
|
+
|
215
|
+
We've tested and used the plugin on MRI 1.8.6/1.8.7 and on JRuby 1.4.0. At this point we do
|
216
|
+
not support demonization in JRuby and never tested the code on Ruby 1.9. Obviously because
|
217
|
+
of JVM limitations you won't be able to use <tt>fork</tt> workers engine in JRuby, but
|
218
|
+
threaded workers do pretty well.
|
219
|
+
|
220
|
+
|
221
|
+
== Migrating from pre-2.0 releases
|
222
|
+
|
223
|
+
Before version 2.0 has been released, this code was developed as a Rails plugin only and
|
224
|
+
did not have any versions numbering system in place. So we call all those old versions
|
225
|
+
a pre-2.0 releases. If you use one of those relases (if your loops plugin does not have
|
226
|
+
the <tt>VERSION.yml</tt> file in the root directory), be careful when upgrading because
|
227
|
+
there are a few incompatible changes we have made in the loops command: <tt>-h</tt>,
|
228
|
+
<tt>-a</tt>, <tt>-s</tt>, <tt>-L</tt> and <tt>-l</tt> options were deprecated
|
229
|
+
and replaced with a friendlier word commands.
|
230
|
+
Use <tt>loops help</tt> to get help.
|
231
|
+
|
232
|
+
|
233
|
+
== Who are the authors?
|
234
|
+
|
235
|
+
This plugin has been created in Scribd.com for our internal use and then the sources were opened
|
236
|
+
for other people to use. All the code in this package has been developed by Alexey Kovyrin,
|
237
|
+
Dmytro Shteflyuk and Alexey Verkhovsky for Scribd.com and is released under the MIT license.
|
238
|
+
For more details, see LICENSE file.
|
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'jeweler'
|
5
|
+
Jeweler::Tasks.new do |gemspec|
|
6
|
+
gemspec.name = 'loops'
|
7
|
+
gemspec.summary = 'Simple background loops framework for ruby'
|
8
|
+
gemspec.description = 'Loops is a small and lightweight framework for Ruby on Rails, Merb and other ruby frameworks created to support simple background loops in your application which are usually used to do some background data processing on your servers (queue workers, batch tasks processors, etc).'
|
9
|
+
gemspec.email = 'alexey@kovyrin.net'
|
10
|
+
gemspec.homepage = 'http://github.com/kovyrin/loops'
|
11
|
+
gemspec.authors = ['Alexey Kovyrin', 'Dmytro Shteflyuk']
|
12
|
+
gemspec.files.include ['lib/**/*']
|
13
|
+
end
|
14
|
+
Jeweler::GemcutterTasks.new
|
15
|
+
rescue LoadError
|
16
|
+
puts 'Jeweler not available. Install it with: sudo gem install jeweler'
|
17
|
+
end
|
18
|
+
|
19
|
+
begin
|
20
|
+
require 'spec/rake/spectask'
|
21
|
+
|
22
|
+
desc 'Default: run unit tests.'
|
23
|
+
task :default => :spec
|
24
|
+
|
25
|
+
desc 'Test the loops plugin.'
|
26
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
27
|
+
t.libs << 'lib'
|
28
|
+
t.pattern = 'spec/**/*_spec.rb'
|
29
|
+
t.verbose = true
|
30
|
+
t.spec_opts = ['-cfs']
|
31
|
+
end
|
32
|
+
rescue LoadError
|
33
|
+
puts 'RSpec not available. Install it with: sudo gem install rspec'
|
34
|
+
end
|
35
|
+
|
36
|
+
begin
|
37
|
+
require 'yard'
|
38
|
+
YARD::Rake::YardocTask.new(:yard) do |t|
|
39
|
+
t.options = ['--title', 'Loops Documentation']
|
40
|
+
if ENV['PRIVATE']
|
41
|
+
t.options.concat ['--protected', '--private']
|
42
|
+
else
|
43
|
+
t.options.concat ['--protected', '--no-private']
|
44
|
+
end
|
45
|
+
end
|
46
|
+
rescue LoadError
|
47
|
+
puts 'Yard not available. Install it with: sudo gem install yard'
|
48
|
+
end
|
data/VERSION.yml
ADDED
data/bin/loops
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib') unless $:.include?(File.dirname(__FILE__) + '/../lib')
|
3
|
+
|
4
|
+
require File.dirname(__FILE__) + '/../lib/loops'
|
5
|
+
|
6
|
+
begin
|
7
|
+
# The dup is to keep ARGV intact, so that tools like ruby-debug can respawn.
|
8
|
+
failure = Loops::CLI.execute
|
9
|
+
Kernel.exit(failure ? 1 : 0)
|
10
|
+
rescue SystemExit => e
|
11
|
+
Kernel.exit(e.status)
|
12
|
+
rescue Exception => e
|
13
|
+
STDERR.puts("#{e.message} (#{e.class})")
|
14
|
+
STDERR.puts(e.backtrace.join("\n"))
|
15
|
+
Kernel.exit(1)
|
16
|
+
end
|
@@ -0,0 +1,259 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
#
|
4
|
+
# This script is based on Phusion's passenger-memory-stats
|
5
|
+
# Almost all of the code is Copyright (c) 2008, 2009 Phusion
|
6
|
+
# For more details go to http://www.modrails.com/
|
7
|
+
#
|
8
|
+
|
9
|
+
# ANSI color codes
|
10
|
+
RESET = "\e[0m"
|
11
|
+
BOLD = "\e[1m"
|
12
|
+
WHITE = "\e[37m"
|
13
|
+
YELLOW = "\e[33m"
|
14
|
+
BLUE_BG = "\e[44m"
|
15
|
+
|
16
|
+
# Container for tabular data.
|
17
|
+
class Table
|
18
|
+
def initialize(column_names)
|
19
|
+
@column_names = column_names
|
20
|
+
@rows = []
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_row(values)
|
24
|
+
@rows << values.to_a
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_rows(list_of_rows)
|
28
|
+
list_of_rows.each do |row|
|
29
|
+
add_row(row)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def remove_column(name)
|
34
|
+
i = @column_names.index(name)
|
35
|
+
@column_names.delete_at(i)
|
36
|
+
@rows.each do |row|
|
37
|
+
row.delete_at(i)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_s(title = nil)
|
42
|
+
max_column_widths = [1] * @column_names.size
|
43
|
+
(@rows + [@column_names]).each do |row|
|
44
|
+
row.each_with_index do |value, i|
|
45
|
+
max_column_widths[i] = [value.to_s.size, max_column_widths[i]].max
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
format_string = max_column_widths.map{ |i| "%#{-i}s" }.join(" ")
|
50
|
+
header = sprintf(format_string, *@column_names).rstrip << "\n"
|
51
|
+
if title
|
52
|
+
free_space = header.size - title.size - 2
|
53
|
+
if free_space <= 0
|
54
|
+
left_bar_size = 3
|
55
|
+
right_bar_size = 3
|
56
|
+
else
|
57
|
+
left_bar_size = free_space / 2
|
58
|
+
right_bar_size = free_space - left_bar_size
|
59
|
+
end
|
60
|
+
result = "#{BLUE_BG}#{BOLD}#{YELLOW}\n"
|
61
|
+
result << "#{"-" * left_bar_size} #{title} #{"-" * right_bar_size}\n"
|
62
|
+
if !@rows.empty?
|
63
|
+
result << WHITE
|
64
|
+
result << header
|
65
|
+
end
|
66
|
+
else
|
67
|
+
result = header.dup
|
68
|
+
end
|
69
|
+
if @rows.empty?
|
70
|
+
result << RESET
|
71
|
+
else
|
72
|
+
result << ("-" * header.size) << "#{RESET}\n"
|
73
|
+
@rows.each do |row|
|
74
|
+
result << sprintf(format_string, *row).rstrip << "\n"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
result
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class MemoryStats
|
82
|
+
class Process
|
83
|
+
attr_accessor :pid
|
84
|
+
attr_accessor :ppid
|
85
|
+
attr_accessor :threads
|
86
|
+
attr_accessor :vm_size # in KB
|
87
|
+
attr_accessor :rss # in KB
|
88
|
+
attr_accessor :name
|
89
|
+
attr_accessor :private_dirty_rss # in KB
|
90
|
+
|
91
|
+
def vm_size_in_mb
|
92
|
+
return sprintf("%.1f MB", vm_size / 1024.0)
|
93
|
+
end
|
94
|
+
|
95
|
+
def rss_in_mb
|
96
|
+
return sprintf("%.1f MB", rss / 1024.0)
|
97
|
+
end
|
98
|
+
|
99
|
+
def private_dirty_rss_in_mb
|
100
|
+
if private_dirty_rss.is_a?(Numeric)
|
101
|
+
return sprintf("%.1f MB", private_dirty_rss / 1024.0)
|
102
|
+
else
|
103
|
+
return "?"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def to_a
|
108
|
+
return [pid, ppid, vm_size_in_mb, private_dirty_rss_in_mb, rss_in_mb, name]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def start
|
113
|
+
loops_processes = list_processes(:match =>
|
114
|
+
/^loops* (monitor|worker)/)
|
115
|
+
print_process_list("Loops processes", loops_processes, :show_ppid => true)
|
116
|
+
|
117
|
+
if RUBY_PLATFORM !~ /linux/
|
118
|
+
puts
|
119
|
+
puts "*** WARNING: The private dirty RSS can only be displayed " <<
|
120
|
+
"on Linux. You're currently using '#{RUBY_PLATFORM}'."
|
121
|
+
elsif ::Process.uid != 0 && loops_processes.any?{ |p| p.private_dirty_rss.nil? }
|
122
|
+
puts
|
123
|
+
puts "*** WARNING: Please run this tool as root. Otherwise the " <<
|
124
|
+
"private dirty RSS of processes cannot be determined."
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Returns a list of Process objects that match the given search criteria.
|
129
|
+
#
|
130
|
+
# # Search by executable path.
|
131
|
+
# list_processes(:exe => '/usr/sbin/apache2')
|
132
|
+
#
|
133
|
+
# # Search by executable name.
|
134
|
+
# list_processes(:name => 'ruby1.8')
|
135
|
+
#
|
136
|
+
# # Search by process name.
|
137
|
+
# list_processes(:match => 'Passenger FrameworkSpawner')
|
138
|
+
def list_processes(options)
|
139
|
+
if options[:exe]
|
140
|
+
name = options[:exe].sub(/.*\/(.*)/, '\1')
|
141
|
+
if RUBY_PLATFORM =~ /linux/
|
142
|
+
ps = "ps -C '#{name}'"
|
143
|
+
else
|
144
|
+
ps = "ps -A"
|
145
|
+
options[:match] = Regexp.new(Regexp.escape(name))
|
146
|
+
end
|
147
|
+
elsif options[:name]
|
148
|
+
if RUBY_PLATFORM =~ /linux/
|
149
|
+
ps = "ps -C '#{options[:name]}'"
|
150
|
+
else
|
151
|
+
ps = "ps -A"
|
152
|
+
options[:match] = Regexp.new(" #{Regexp.escape(options[:name])}")
|
153
|
+
end
|
154
|
+
elsif options[:match]
|
155
|
+
ps = "ps -A"
|
156
|
+
else
|
157
|
+
raise ArgumentError, "Invalid options."
|
158
|
+
end
|
159
|
+
|
160
|
+
processes = []
|
161
|
+
case RUBY_PLATFORM
|
162
|
+
when /solaris/
|
163
|
+
list = `#{ps} -o pid,ppid,nlwp,vsz,rss,comm`.split("\n")
|
164
|
+
threads_known = true
|
165
|
+
when /darwin/
|
166
|
+
list = `#{ps} -w -o pid,ppid,vsz,rss,command`.split("\n")
|
167
|
+
threads_known = false
|
168
|
+
else
|
169
|
+
list = `#{ps} -w -o pid,ppid,nlwp,vsz,rss,command`.split("\n")
|
170
|
+
threads_known = true
|
171
|
+
end
|
172
|
+
list.shift
|
173
|
+
list.each do |line|
|
174
|
+
line.gsub!(/^ */, '')
|
175
|
+
line.gsub!(/ *$/, '')
|
176
|
+
|
177
|
+
p = Process.new
|
178
|
+
if threads_known
|
179
|
+
p.pid, p.ppid, p.threads, p.vm_size, p.rss, p.name = line.split(/ +/, 6)
|
180
|
+
else
|
181
|
+
p.pid, p.ppid, p.vm_size, p.rss, p.name = line.split(/ +/, 5)
|
182
|
+
p.threads = "?"
|
183
|
+
end
|
184
|
+
p.name.sub!(/\Aruby: /, '')
|
185
|
+
p.name.sub!(/ \(ruby\)\Z/, '')
|
186
|
+
if p.name !~ /^ps/ && (!options[:match] || p.name.match(options[:match]))
|
187
|
+
# Convert some values to integer.
|
188
|
+
[:pid, :ppid, :vm_size, :rss].each do |attr|
|
189
|
+
p.send("#{attr}=", p.send(attr).to_i)
|
190
|
+
end
|
191
|
+
p.threads = p.threads.to_i if threads_known
|
192
|
+
|
193
|
+
if platform_provides_private_dirty_rss_information?
|
194
|
+
p.private_dirty_rss = determine_private_dirty_rss(p.pid)
|
195
|
+
end
|
196
|
+
processes << p
|
197
|
+
end
|
198
|
+
end
|
199
|
+
return processes
|
200
|
+
end
|
201
|
+
|
202
|
+
private
|
203
|
+
def platform_provides_private_dirty_rss_information?
|
204
|
+
return RUBY_PLATFORM =~ /linux/
|
205
|
+
end
|
206
|
+
|
207
|
+
# Returns the private dirty RSS for the given process, in KB.
|
208
|
+
def determine_private_dirty_rss(pid)
|
209
|
+
total = 0
|
210
|
+
File.read("/proc/#{pid}/smaps").split("\n").each do |line|
|
211
|
+
line =~ /^(Private)_Dirty: +(\d+)/
|
212
|
+
if $2
|
213
|
+
total += $2.to_i
|
214
|
+
end
|
215
|
+
end
|
216
|
+
if total == 0
|
217
|
+
return nil
|
218
|
+
else
|
219
|
+
return total
|
220
|
+
end
|
221
|
+
rescue Errno::EACCES, Errno::ENOENT
|
222
|
+
return nil
|
223
|
+
end
|
224
|
+
|
225
|
+
def print_process_list(title, processes, options = {})
|
226
|
+
table = Table.new(%w{PID PPID VMSize Private Resident Name})
|
227
|
+
table.add_rows(processes)
|
228
|
+
if options.has_key?(:show_ppid) && !options[:show_ppid]
|
229
|
+
table.remove_column('PPID')
|
230
|
+
end
|
231
|
+
if platform_provides_private_dirty_rss_information?
|
232
|
+
table.remove_column('Resident')
|
233
|
+
else
|
234
|
+
table.remove_column('Private')
|
235
|
+
end
|
236
|
+
puts table.to_s(title)
|
237
|
+
|
238
|
+
if platform_provides_private_dirty_rss_information?
|
239
|
+
total_private_dirty_rss = 0
|
240
|
+
some_private_dirty_rss_cannot_be_determined = false
|
241
|
+
processes.each do |p|
|
242
|
+
if p.private_dirty_rss.is_a?(Numeric)
|
243
|
+
total_private_dirty_rss += p.private_dirty_rss
|
244
|
+
else
|
245
|
+
some_private_dirty_rss_cannot_be_determined = true
|
246
|
+
end
|
247
|
+
end
|
248
|
+
puts "### Processes: #{processes.size}"
|
249
|
+
printf "### Total private dirty RSS: %.2f MB", total_private_dirty_rss / 1024.0
|
250
|
+
if some_private_dirty_rss_cannot_be_determined
|
251
|
+
puts " (?)"
|
252
|
+
else
|
253
|
+
puts
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
MemoryStats.new.start
|