thread_tools 0.23
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.
- data/LICENSE +22 -0
- data/README.rdoc +57 -0
- data/lib/thread_tools/debugmutex.rb +56 -0
- data/lib/thread_tools/mongrel_pool.rb +76 -0
- data/lib/thread_tools/semaphore.rb +39 -0
- data/lib/thread_tools/threadpool.rb +101 -0
- data/test/test_mongrel.rb +45 -0
- data/test/test_semaphore.rb +25 -0
- data/test/test_threadpool.rb +41 -0
- data/thread_tools.gemspec +32 -0
- metadata +67 -0
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2009 Daniel Tralamazza <daniel@tralamazza.com>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
= thread_tools
|
2
|
+
|
3
|
+
* http://github.com/differential/thread_tools
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
A collection of utilities that I often use for threaded code.
|
8
|
+
|
9
|
+
== FEATURES/PROBLEMS:
|
10
|
+
|
11
|
+
* Thread pool
|
12
|
+
* Mongrel patch (thread pool)
|
13
|
+
* Semaphore
|
14
|
+
* Debug mutex
|
15
|
+
|
16
|
+
== SYNOPSIS:
|
17
|
+
|
18
|
+
* Bounded thread pool
|
19
|
+
require 'thread_tools/threadpool'
|
20
|
+
|
21
|
+
tpool = ThreadTools::ThreadPool.new(2)
|
22
|
+
10.times {|i|
|
23
|
+
tpool.spawn(i) {|ti|
|
24
|
+
puts "#{Thread.current} => #{ti}"
|
25
|
+
}
|
26
|
+
}
|
27
|
+
tpool.shutdown
|
28
|
+
|
29
|
+
* Stupid simple semaphore
|
30
|
+
require 'thread_tools/semaphore'
|
31
|
+
|
32
|
+
sem = ThreadTools::Semaphore.new(1)
|
33
|
+
sem.acquire
|
34
|
+
sem.release
|
35
|
+
|
36
|
+
* Thread pool for Mongrel
|
37
|
+
require 'mongrel'
|
38
|
+
require 'thread_tools/mongrel_pool'
|
39
|
+
|
40
|
+
server = Mongrel::HttpServer.new(host, port)
|
41
|
+
server.run(10).join
|
42
|
+
|
43
|
+
== REQUIREMENTS:
|
44
|
+
|
45
|
+
* Mongrel patch requires mongrel (duh)
|
46
|
+
* Rake if you want to run the test cases
|
47
|
+
|
48
|
+
== INSTALL:
|
49
|
+
|
50
|
+
gem install thread_tools
|
51
|
+
or
|
52
|
+
gem build thread_tools.gemspec
|
53
|
+
gem install thread_tools-<version>.gem
|
54
|
+
|
55
|
+
== LICENSE:
|
56
|
+
|
57
|
+
see LICENSE
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# Author: Daniel Tralamazza
|
2
|
+
# Date: 29 Sep 2009
|
3
|
+
#
|
4
|
+
# Mutex
|
5
|
+
#
|
6
|
+
# This class will make things slower, expect more contentions than normal
|
7
|
+
# I derived this mutex because I needed to check contention levels and trace
|
8
|
+
# owner changes
|
9
|
+
|
10
|
+
require 'thread'
|
11
|
+
|
12
|
+
|
13
|
+
module ThreadTools
|
14
|
+
|
15
|
+
class DebugMutex < Mutex
|
16
|
+
attr_reader :contentions
|
17
|
+
attr_reader :owner
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@contentions = 0
|
21
|
+
end
|
22
|
+
|
23
|
+
def lock
|
24
|
+
unless try_lock
|
25
|
+
super
|
26
|
+
@owner = Thread.current
|
27
|
+
end
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def try_lock
|
32
|
+
ret = super
|
33
|
+
unless (ret)
|
34
|
+
@contentions += 1
|
35
|
+
else
|
36
|
+
@owner = Thread.current
|
37
|
+
end
|
38
|
+
ret
|
39
|
+
end
|
40
|
+
|
41
|
+
def unlock
|
42
|
+
@owner = nil
|
43
|
+
super
|
44
|
+
end
|
45
|
+
|
46
|
+
def synchronize
|
47
|
+
lock
|
48
|
+
begin
|
49
|
+
yield
|
50
|
+
ensure
|
51
|
+
unlock
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# Author: Daniel Tralamazza
|
2
|
+
# Date: 28 Sep 2009
|
3
|
+
#
|
4
|
+
# This code patches mongrel to use a thread pool instead of creating a new thread
|
5
|
+
# for every request. It also traps SIGTERM to close all connections
|
6
|
+
# Changed method run to accept an extra parameter to set thread pool size
|
7
|
+
#
|
8
|
+
|
9
|
+
|
10
|
+
require File.expand_path(File.dirname(__FILE__)+'/threadpool')
|
11
|
+
|
12
|
+
|
13
|
+
module Mongrel
|
14
|
+
class HttpServer
|
15
|
+
|
16
|
+
def run(_pool_size = 100)
|
17
|
+
trap("TERM") { stop } # trap "kill"
|
18
|
+
|
19
|
+
@thread_pool = ThreadTools::ThreadPool.new(_pool_size, @workers)
|
20
|
+
|
21
|
+
BasicSocket.do_not_reverse_lookup = true
|
22
|
+
|
23
|
+
configure_socket_options
|
24
|
+
|
25
|
+
if defined?($tcp_defer_accept_opts) and $tcp_defer_accept_opts
|
26
|
+
@socket.setsockopt(*$tcp_defer_accept_opts) rescue nil
|
27
|
+
end
|
28
|
+
|
29
|
+
@acceptor = Thread.new do
|
30
|
+
begin
|
31
|
+
while true
|
32
|
+
begin
|
33
|
+
client = @socket.accept
|
34
|
+
|
35
|
+
if defined?($tcp_cork_opts) and $tcp_cork_opts
|
36
|
+
client.setsockopt(*$tcp_cork_opts) rescue nil
|
37
|
+
end
|
38
|
+
|
39
|
+
worker_list = @workers.list
|
40
|
+
|
41
|
+
if worker_list.length >= @num_processors
|
42
|
+
STDERR.puts "Server overloaded with #{worker_list.length} processors (#@num_processors max). Dropping connection."
|
43
|
+
client.close rescue nil
|
44
|
+
reap_dead_workers("max processors")
|
45
|
+
else
|
46
|
+
@thread_pool.spawn(client) do |c|
|
47
|
+
Thread.current[:started_on] = Time.now
|
48
|
+
process_client(c)
|
49
|
+
end
|
50
|
+
sleep @throttle if @throttle > 0
|
51
|
+
end
|
52
|
+
rescue StopServer
|
53
|
+
break
|
54
|
+
rescue Errno::EMFILE
|
55
|
+
reap_dead_workers("too many open files")
|
56
|
+
sleep 0.5
|
57
|
+
rescue Errno::ECONNABORTED
|
58
|
+
# client closed the socket even before accept
|
59
|
+
client.close rescue nil
|
60
|
+
rescue Object => e
|
61
|
+
STDERR.puts "#{Time.now}: Unhandled listen loop exception #{e.inspect}."
|
62
|
+
STDERR.puts e.backtrace.join("\n")
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
@thread_pool.shutdown
|
67
|
+
graceful_shutdown
|
68
|
+
ensure
|
69
|
+
@socket.close
|
70
|
+
# STDERR.puts "#{Time.now}: Closed socket."
|
71
|
+
end
|
72
|
+
end
|
73
|
+
@acceptor
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# Author: Daniel Tralamazza
|
2
|
+
# Date:
|
3
|
+
#
|
4
|
+
# Semaphore
|
5
|
+
#
|
6
|
+
|
7
|
+
|
8
|
+
require 'thread'
|
9
|
+
|
10
|
+
|
11
|
+
module ThreadTools
|
12
|
+
|
13
|
+
# Poor man's semaphore
|
14
|
+
class Semaphore
|
15
|
+
attr_reader :count
|
16
|
+
def initialize(_count)
|
17
|
+
@count = _count
|
18
|
+
@mtx = Mutex.new
|
19
|
+
@cv = ConditionVariable.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def acquire
|
23
|
+
@mtx.synchronize do
|
24
|
+
@cv.wait(@mtx) until @count > 0
|
25
|
+
@count -= 1
|
26
|
+
end
|
27
|
+
@count
|
28
|
+
end
|
29
|
+
|
30
|
+
def release
|
31
|
+
@mtx.synchronize do
|
32
|
+
@count += 1
|
33
|
+
@cv.signal
|
34
|
+
end
|
35
|
+
@count
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# Author: Daniel Tralamazza
|
2
|
+
# Date: 25 Sep 2009
|
3
|
+
#
|
4
|
+
# ThreadPool
|
5
|
+
#
|
6
|
+
# Usage:
|
7
|
+
#
|
8
|
+
# tpool = ThreadTools::ThreadPool.new(3)
|
9
|
+
# 12.times do |i|
|
10
|
+
# tpool.spawn(i, "hi") {|ti, ts|
|
11
|
+
# puts "#{Thread.current} (#{ti}) says #{ts}\n"
|
12
|
+
# }
|
13
|
+
# end
|
14
|
+
# tpool.shutdown
|
15
|
+
|
16
|
+
|
17
|
+
require 'thread'
|
18
|
+
require File.expand_path(File.dirname(__FILE__)+'/semaphore')
|
19
|
+
|
20
|
+
|
21
|
+
module ThreadTools
|
22
|
+
|
23
|
+
class ThreadPool
|
24
|
+
# kill the worker thread if an excpetion is raised (default false)
|
25
|
+
attr_accessor :kill_worker_on_exception
|
26
|
+
# number of worker threads
|
27
|
+
attr_reader :size
|
28
|
+
|
29
|
+
# _size should be at least 1
|
30
|
+
def initialize(_size, _thr_group = nil)
|
31
|
+
@kill_worker_on_exception = false
|
32
|
+
@size = 0
|
33
|
+
@pool_mtx = Mutex.new
|
34
|
+
@pool_cv = ConditionVariable.new
|
35
|
+
@pool = []
|
36
|
+
@thr_grp = _thr_group
|
37
|
+
_size = 1 if _size < 1
|
38
|
+
_size.times { create_worker }
|
39
|
+
end
|
40
|
+
|
41
|
+
def create_worker
|
42
|
+
Thread.new do
|
43
|
+
thr = Thread.current
|
44
|
+
@pool_mtx.synchronize do
|
45
|
+
@size += 1
|
46
|
+
if (!@thr_grp.nil?)
|
47
|
+
@thr_grp.add(thr)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
thr[:jobs] = [] # XXX array not really necessary
|
51
|
+
thr[:sem] = Semaphore.new(0)
|
52
|
+
loop do
|
53
|
+
@pool_mtx.synchronize do
|
54
|
+
@pool << thr # puts this thread in the pool
|
55
|
+
@pool_cv.signal
|
56
|
+
end
|
57
|
+
thr[:sem].acquire # wait here for jobs to become available
|
58
|
+
job = thr[:jobs].shift # pop out a job
|
59
|
+
if (job.nil? || job[:block].nil?)
|
60
|
+
break # exit thread if job or block is nil
|
61
|
+
end
|
62
|
+
begin
|
63
|
+
job[:block].call(*job[:args]) # call block
|
64
|
+
rescue
|
65
|
+
if (@kill_worker_on_exception)
|
66
|
+
break # exit thread on exception
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
@pool_mtx.synchronize do
|
71
|
+
@size -= 1
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def spawn(*args, &block)
|
77
|
+
thr = nil
|
78
|
+
@pool_mtx.synchronize do
|
79
|
+
# wait here until a worker is available
|
80
|
+
@pool_cv.wait(@pool_mtx) until !(thr = @pool.shift).nil?
|
81
|
+
thr[:jobs] << { :args => args, :block => block }
|
82
|
+
thr[:sem].release
|
83
|
+
end
|
84
|
+
thr
|
85
|
+
end
|
86
|
+
|
87
|
+
def shutdown
|
88
|
+
thr = nil
|
89
|
+
while !@pool.empty? do
|
90
|
+
@pool_mtx.synchronize do
|
91
|
+
@pool_cv.wait(@pool_mtx) until !(thr = @pool.shift).nil?
|
92
|
+
end
|
93
|
+
thr[:jobs].clear # clear any pending job
|
94
|
+
thr[:jobs] << nil # queue a nil job
|
95
|
+
thr[:sem].release
|
96
|
+
thr.join # wait here for the thread to die
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'mongrel'
|
3
|
+
require File.expand_path(File.dirname(__FILE__)+'/../lib/thread_tools/mongrel_pool')
|
4
|
+
require 'net/http'
|
5
|
+
|
6
|
+
|
7
|
+
class MongrelPoolTest < Test::Unit::TestCase
|
8
|
+
class MyHandler < Mongrel::HttpHandler
|
9
|
+
attr_reader :uniq_threads
|
10
|
+
def initialize()
|
11
|
+
super
|
12
|
+
@uniq_threads = {}
|
13
|
+
end
|
14
|
+
def process(req, res)
|
15
|
+
@uniq_threads[Thread.current] = true
|
16
|
+
res.start do |head,out|
|
17
|
+
head["Content-Type"] = "text/html; charset=\"utf-8\""
|
18
|
+
out << "test"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def setup
|
24
|
+
super
|
25
|
+
@server = Mongrel::HttpServer.new("127.0.0.1", 18881)
|
26
|
+
@handler = MyHandler.new
|
27
|
+
@server.register("/", @handler)
|
28
|
+
@acceptor = @server.run(2)
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_send_recv_shutdown
|
32
|
+
http = Net::HTTP.new("localhost", 18881)
|
33
|
+
4.times {
|
34
|
+
headers, body = http.get("/")
|
35
|
+
# received correct response
|
36
|
+
assert_equal(headers.code, "200")
|
37
|
+
assert_equal(body, "test")
|
38
|
+
}
|
39
|
+
# number of unique threads
|
40
|
+
assert_equal(@handler.uniq_threads.size, 2)
|
41
|
+
@server.stop(true)
|
42
|
+
# all workers are dead
|
43
|
+
assert_equal(@server.workers.list.size, 0)
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require File.expand_path(File.dirname(__FILE__)+'/../lib/thread_tools/semaphore')
|
3
|
+
|
4
|
+
|
5
|
+
class SemaphoreTest < Test::Unit::TestCase
|
6
|
+
def test_acquire_release
|
7
|
+
sem = ThreadTools::Semaphore.new(1)
|
8
|
+
# should be able to acquire
|
9
|
+
assert_equal(sem.acquire, 0)
|
10
|
+
ok = false
|
11
|
+
Thread.new do
|
12
|
+
# should block here
|
13
|
+
sem.acquire
|
14
|
+
# and unblock
|
15
|
+
ok = true
|
16
|
+
end
|
17
|
+
Thread.pass
|
18
|
+
sem.release
|
19
|
+
sleep 0.1
|
20
|
+
# release should unblock acquire
|
21
|
+
assert_equal(ok, true)
|
22
|
+
# semaphore count has to be 0
|
23
|
+
assert_equal(sem.count, 0)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require File.expand_path(File.dirname(__FILE__)+'/../lib/thread_tools/threadpool')
|
3
|
+
|
4
|
+
|
5
|
+
class ThreadPoolTest < Test::Unit::TestCase
|
6
|
+
def setup
|
7
|
+
super
|
8
|
+
@tpool = ThreadTools::ThreadPool.new(2)
|
9
|
+
@tpool.kill_worker_on_exception = true
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_thread_spawn
|
13
|
+
uniq_threads = {}
|
14
|
+
sum_orig = 0
|
15
|
+
sum_lock = Mutex.new
|
16
|
+
sum_thr = 0
|
17
|
+
4.times {|i|
|
18
|
+
sum_orig += i
|
19
|
+
@tpool.spawn(i) {|ti|
|
20
|
+
sum_lock.synchronize do
|
21
|
+
sum_thr += ti
|
22
|
+
end
|
23
|
+
uniq_threads[Thread.current] = true
|
24
|
+
if (ti == 3)
|
25
|
+
# this exception should kill this worker
|
26
|
+
raise "oups"
|
27
|
+
end
|
28
|
+
}
|
29
|
+
}
|
30
|
+
sleep 0.1
|
31
|
+
# 2 workers - 1 from raised exceptions = 1
|
32
|
+
assert_equal(@tpool.size, 1)
|
33
|
+
@tpool.shutdown
|
34
|
+
# same sum
|
35
|
+
assert_equal(sum_orig, sum_thr)
|
36
|
+
# number of different threads
|
37
|
+
assert_equal(uniq_threads.size, 2)
|
38
|
+
# after shutdown all threads are dead
|
39
|
+
assert_equal(@tpool.size, 0)
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = %q{thread_tools}
|
3
|
+
s.version = '0.23'
|
4
|
+
s.summary = %q{Utilities for threaded apps}
|
5
|
+
s.platform = Gem::Platform::RUBY
|
6
|
+
s.email = %q{daniel@tralamazza.com}
|
7
|
+
s.homepage = %q{http://github.com/differential/thread_tools}
|
8
|
+
s.description = %q{Thread tools is a collection of classes and utilities to help you write threaded code.}
|
9
|
+
s.has_rdoc = true
|
10
|
+
s.authors = ['Daniel Tralamazza']
|
11
|
+
s.files = [
|
12
|
+
'thread_tools.gemspec',
|
13
|
+
'README.rdoc',
|
14
|
+
'LICENSE',
|
15
|
+
'lib/thread_tools/threadpool.rb',
|
16
|
+
'lib/thread_tools/mongrel_pool.rb',
|
17
|
+
'lib/thread_tools/semaphore.rb',
|
18
|
+
'lib/thread_tools/debugmutex.rb',
|
19
|
+
'test/test_mongrel.rb',
|
20
|
+
'test/test_semaphore.rb',
|
21
|
+
'test/test_threadpool.rb',
|
22
|
+
]
|
23
|
+
s.test_files = [
|
24
|
+
'test/test_mongrel.rb',
|
25
|
+
'test/test_semaphore.rb',
|
26
|
+
'test/test_threadpool.rb',
|
27
|
+
]
|
28
|
+
s.rdoc_options = ['--main', 'README.rdoc']
|
29
|
+
s.extra_rdoc_files = ['README.rdoc']
|
30
|
+
s.require_paths = ['lib']
|
31
|
+
s.rubyforge_project = 'threadtools'
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: thread_tools
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "0.23"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Daniel Tralamazza
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-10-05 00:00:00 +02:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Thread tools is a collection of classes and utilities to help you write threaded code.
|
17
|
+
email: daniel@tralamazza.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.rdoc
|
24
|
+
files:
|
25
|
+
- thread_tools.gemspec
|
26
|
+
- README.rdoc
|
27
|
+
- LICENSE
|
28
|
+
- lib/thread_tools/threadpool.rb
|
29
|
+
- lib/thread_tools/mongrel_pool.rb
|
30
|
+
- lib/thread_tools/semaphore.rb
|
31
|
+
- lib/thread_tools/debugmutex.rb
|
32
|
+
- test/test_mongrel.rb
|
33
|
+
- test/test_semaphore.rb
|
34
|
+
- test/test_threadpool.rb
|
35
|
+
has_rdoc: true
|
36
|
+
homepage: http://github.com/differential/thread_tools
|
37
|
+
licenses: []
|
38
|
+
|
39
|
+
post_install_message:
|
40
|
+
rdoc_options:
|
41
|
+
- --main
|
42
|
+
- README.rdoc
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: "0"
|
50
|
+
version:
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: "0"
|
56
|
+
version:
|
57
|
+
requirements: []
|
58
|
+
|
59
|
+
rubyforge_project: threadtools
|
60
|
+
rubygems_version: 1.3.5
|
61
|
+
signing_key:
|
62
|
+
specification_version: 3
|
63
|
+
summary: Utilities for threaded apps
|
64
|
+
test_files:
|
65
|
+
- test/test_mongrel.rb
|
66
|
+
- test/test_semaphore.rb
|
67
|
+
- test/test_threadpool.rb
|