stud 0.0.1
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/CHANGELIST +0 -0
- data/LICENSE +0 -0
- data/README.md +0 -0
- data/lib/stud/interval.rb +23 -0
- data/lib/stud/pool.rb +215 -0
- data/lib/stud/supervise.rb +26 -0
- data/lib/stud/task.rb +32 -0
- data/lib/stud/try.rb +76 -0
- metadata +55 -0
data/CHANGELIST
ADDED
File without changes
|
data/LICENSE
ADDED
File without changes
|
data/README.md
ADDED
File without changes
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Stud
|
2
|
+
# This implementation tries to keep clock more accurately.
|
3
|
+
# Prior implementations still permitted skew, where as this one
|
4
|
+
# will attempt to correct for skew.
|
5
|
+
#
|
6
|
+
# The execution patterns of this method should be that
|
7
|
+
# the start time of 'block.call' should always be at time T*interval
|
8
|
+
def interval(time, &block)
|
9
|
+
start = Time.now
|
10
|
+
while true
|
11
|
+
block.call
|
12
|
+
duration = Time.now - start
|
13
|
+
# Sleep only if the duration was less than the time interval
|
14
|
+
if duration < time
|
15
|
+
sleep(time - duration)
|
16
|
+
start += time
|
17
|
+
else
|
18
|
+
# Duration exceeded interval time, reset the clock and do not sleep.
|
19
|
+
start = Time.now
|
20
|
+
end
|
21
|
+
end # loop forever
|
22
|
+
end # def interval
|
23
|
+
end # module Stud
|
data/lib/stud/pool.rb
ADDED
@@ -0,0 +1,215 @@
|
|
1
|
+
require "thread"
|
2
|
+
|
3
|
+
module Stud
|
4
|
+
# Public: A thread-safe, generic resource pool.
|
5
|
+
#
|
6
|
+
# This class is agnostic as to the resources in the pool. You can put
|
7
|
+
# database connections, sockets, threads, etc. It's up to you!
|
8
|
+
#
|
9
|
+
# Examples:
|
10
|
+
#
|
11
|
+
# pool = Pool.new
|
12
|
+
# pool.add(Sequel.connect("postgres://pg-readonly-1/prod"))
|
13
|
+
# pool.add(Sequel.connect("postgres://pg-readonly-2/prod"))
|
14
|
+
# pool.add(Sequel.connect("postgres://pg-readonly-3/prod"))
|
15
|
+
#
|
16
|
+
# pool.fetch # => Returns one of the Sequel::Database values from the pool
|
17
|
+
class Pool
|
18
|
+
|
19
|
+
class Error < StandardError; end
|
20
|
+
|
21
|
+
# An error indicating a given resource is busy.
|
22
|
+
class ResourceBusy < Error; end
|
23
|
+
|
24
|
+
# An error indicating a given resource is not found.
|
25
|
+
class NotFound < Error; end
|
26
|
+
|
27
|
+
# You performed an invalid action.
|
28
|
+
class InvalidAction < Error; end
|
29
|
+
|
30
|
+
# Default all methods to private. See the bottom of the class definition
|
31
|
+
# for public method declarations.
|
32
|
+
private
|
33
|
+
|
34
|
+
# Public: initialize a new pool.
|
35
|
+
#
|
36
|
+
# max_size - if specified, limits the number of resources allowed in the pool.
|
37
|
+
def initialize(max_size=nil)
|
38
|
+
# Available resources
|
39
|
+
@available = Hash.new
|
40
|
+
# Busy resources
|
41
|
+
@busy = Hash.new
|
42
|
+
|
43
|
+
# The pool lock
|
44
|
+
@lock = Mutex.new
|
45
|
+
|
46
|
+
# Locks for blocking {#fetch} calls if the pool is full.
|
47
|
+
@full_lock = Mutex.new
|
48
|
+
@full_cv = ConditionVariable.new
|
49
|
+
|
50
|
+
# Maximum size of this pool.
|
51
|
+
@max_size = max_size
|
52
|
+
end # def initialize
|
53
|
+
|
54
|
+
# Private: Is this pool size-limited?
|
55
|
+
#
|
56
|
+
# Returns true if this pool was created with a max_size. False, otherwise.
|
57
|
+
def sized?
|
58
|
+
return !@max_size.nil?
|
59
|
+
end # def sized?
|
60
|
+
|
61
|
+
# Private: Is this pool full?
|
62
|
+
#
|
63
|
+
# Returns true if the pool is sized and the count of resources is at maximum.
|
64
|
+
def full?
|
65
|
+
return sized? && (count == @max_size)
|
66
|
+
end # def full?
|
67
|
+
|
68
|
+
# Public: the count of resources in the pool
|
69
|
+
#
|
70
|
+
# Returns the count of resources in the pool.
|
71
|
+
def count
|
72
|
+
return (@busy.size + @available.size)
|
73
|
+
end # def count
|
74
|
+
|
75
|
+
# Public: Add a new resource to this pool.
|
76
|
+
#
|
77
|
+
# The resource, once added, is assumed to be available for use.
|
78
|
+
# That means once you add it, you must not use it unless you receive it from
|
79
|
+
# {Pool#fetch}
|
80
|
+
#
|
81
|
+
# resource - the object resource to add to the pool.
|
82
|
+
#
|
83
|
+
# Returns nothing
|
84
|
+
def add(resource)
|
85
|
+
@lock.synchronize do
|
86
|
+
@available[resource.object_id] = resource
|
87
|
+
end
|
88
|
+
return nil
|
89
|
+
end # def add
|
90
|
+
|
91
|
+
# Public: Fetch an available resource.
|
92
|
+
#
|
93
|
+
# If no resource is available, and the pool is not full, the
|
94
|
+
# default_value_block will be called and the return value of it used as the
|
95
|
+
# resource.
|
96
|
+
#
|
97
|
+
# If no resource is availabe, and the pool is full, this call will block
|
98
|
+
# until a resource is available.
|
99
|
+
#
|
100
|
+
# Returns a resource ready to be used.
|
101
|
+
def fetch(&default_value_block)
|
102
|
+
@lock.synchronize do
|
103
|
+
object_id, resource = @available.shift
|
104
|
+
if !resource.nil?
|
105
|
+
@busy[resource.object_id] = resource
|
106
|
+
return resource
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
@full_lock.synchronize do
|
111
|
+
if full?
|
112
|
+
# This should really use a logger.
|
113
|
+
puts "=> Pool is full and nothing available. Waiting for a release..."
|
114
|
+
@full_cv.wait(@full_lock)
|
115
|
+
return fetch(&default_value_block)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# TODO(sissel): If no block is given, we should block until a resource is
|
120
|
+
# available.
|
121
|
+
|
122
|
+
# If we get here, no resource is available and the pool is not full.
|
123
|
+
resource = default_value_block.call
|
124
|
+
# Only add the resource if the default_value_block returned one.
|
125
|
+
if !resource.nil?
|
126
|
+
add(resource)
|
127
|
+
return fetch
|
128
|
+
end
|
129
|
+
end # def fetch
|
130
|
+
|
131
|
+
# Public: Remove a resource from the pool.
|
132
|
+
#
|
133
|
+
# This is useful if the resource is no longer useful. For example, if it is
|
134
|
+
# a database connection and that connection has failed.
|
135
|
+
#
|
136
|
+
# This resource *MUST* be available and not busy.
|
137
|
+
#
|
138
|
+
# Raises Pool::NotFound if no such resource is found.
|
139
|
+
# Raises Pool::ResourceBusy if the resource is found but in use.
|
140
|
+
def remove(resource)
|
141
|
+
# Find the object by object_id
|
142
|
+
#p [:internal, :busy => @busy, :available => @available]
|
143
|
+
@lock.synchronize do
|
144
|
+
if available?(resource)
|
145
|
+
raise InvalidAction, "This resource must be busy for you to remove " \
|
146
|
+
"it (ie; it must be fetched from the pool)"
|
147
|
+
end
|
148
|
+
@busy.delete(resource.object_id)
|
149
|
+
end
|
150
|
+
end # def remove
|
151
|
+
|
152
|
+
# Private: Verify this resource is in the pool.
|
153
|
+
#
|
154
|
+
# You *MUST* call this method only when you are holding @lock.
|
155
|
+
#
|
156
|
+
# Returns :available if it is available, :busy if busy, false if not in the pool.
|
157
|
+
def include?(resource)
|
158
|
+
if @available.include?(resource.object_id)
|
159
|
+
return :available
|
160
|
+
elsif @busy.include?(resource.object_id)
|
161
|
+
return :busy
|
162
|
+
else
|
163
|
+
return false
|
164
|
+
end
|
165
|
+
end # def include?
|
166
|
+
|
167
|
+
# Private: Is this resource available?
|
168
|
+
# You *MUST* call this method only when you are holding @lock.
|
169
|
+
#
|
170
|
+
# Returns true if this resource is available in the pool.
|
171
|
+
# Raises NotFound if the resource given is not in the pool at all.
|
172
|
+
def available?(resource)
|
173
|
+
case include?(resource)
|
174
|
+
when :available; return true
|
175
|
+
when :busy; return false
|
176
|
+
else; raise NotFound, "No resource, #{resource.inspect}, found in pool"
|
177
|
+
end
|
178
|
+
end # def avilable?
|
179
|
+
|
180
|
+
# Private: Is this resource busy?
|
181
|
+
#
|
182
|
+
# You *MUST* call this method only when you are holding @lock.
|
183
|
+
#
|
184
|
+
# Returns true if this resource is busy.
|
185
|
+
# Raises NotFound if the resource given is not in the pool at all.
|
186
|
+
def busy?(resource)
|
187
|
+
return !available?(resource)
|
188
|
+
end # def busy?
|
189
|
+
|
190
|
+
# Public: Release this resource back to the pool.
|
191
|
+
#
|
192
|
+
# After you finish using a resource you received with {#fetch}, you must
|
193
|
+
# release it back to the pool using this method.
|
194
|
+
#
|
195
|
+
# Alternately, you can {#remove} it if you want to remove it from the pool
|
196
|
+
# instead of releasing it.
|
197
|
+
def release(resource)
|
198
|
+
@lock.synchronize do
|
199
|
+
if !include?(resource)
|
200
|
+
raise NotFound, "No resource, #{resource.inspect}, found in pool"
|
201
|
+
end
|
202
|
+
|
203
|
+
# Release is a no-op if this resource is already available.
|
204
|
+
#return if available?(resource)
|
205
|
+
@busy.delete(resource.object_id)
|
206
|
+
@available[resource.object_id] = resource
|
207
|
+
|
208
|
+
# Notify any threads waiting on a resource from the pool.
|
209
|
+
@full_lock.synchronize { @full_cv.signal }
|
210
|
+
end
|
211
|
+
end # def release
|
212
|
+
|
213
|
+
public(:add, :remove, :fetch, :release, :sized?, :count)
|
214
|
+
end # class Pool
|
215
|
+
end # module Stud
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Stud
|
2
|
+
class Supervisor
|
3
|
+
def initialize(*args, &block)
|
4
|
+
@args = args
|
5
|
+
@block = block
|
6
|
+
|
7
|
+
run
|
8
|
+
end # def initialize
|
9
|
+
|
10
|
+
def run
|
11
|
+
while true
|
12
|
+
task = Task.new(*@args, &@block)
|
13
|
+
begin
|
14
|
+
puts :result => task.wait
|
15
|
+
rescue => e
|
16
|
+
puts e
|
17
|
+
puts e.backtrace
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end # def run
|
21
|
+
end # class Supervisor
|
22
|
+
|
23
|
+
def self.supervise(&block)
|
24
|
+
Supervisor.new(&block)
|
25
|
+
end # def supervise
|
26
|
+
end # module Stud
|
data/lib/stud/task.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require "thread"
|
2
|
+
|
3
|
+
module Stud
|
4
|
+
class Task
|
5
|
+
def initialize(*args, &block)
|
6
|
+
# A queue to receive the result of the block
|
7
|
+
# TODO(sissel): Don't use a queue, just store it in an instance variable.
|
8
|
+
@queue = Queue.new
|
9
|
+
|
10
|
+
@thread = Thread.new(@queue, *args) do |queue, *args|
|
11
|
+
begin
|
12
|
+
result = block.call(*args)
|
13
|
+
queue << [:return, result]
|
14
|
+
rescue => e
|
15
|
+
queue << [:exception, e]
|
16
|
+
end
|
17
|
+
end # thread
|
18
|
+
end # def initialize
|
19
|
+
|
20
|
+
def wait
|
21
|
+
@thread.join
|
22
|
+
reason, result = @queue.pop
|
23
|
+
|
24
|
+
if reason == :exception
|
25
|
+
#raise StandardError.new(result)
|
26
|
+
raise result
|
27
|
+
else
|
28
|
+
return result
|
29
|
+
end
|
30
|
+
end # def wait
|
31
|
+
end # class Task
|
32
|
+
end # module Stud
|
data/lib/stud/try.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
module Stud
|
2
|
+
# Public: try a block of code until either it succeeds or we give up.
|
3
|
+
#
|
4
|
+
# enumerable - an Enumerable or omitted, #each is invoked and is tried that
|
5
|
+
# number of times. If this value is omitted or nil, we will try until
|
6
|
+
# success with no limit on the number of tries.
|
7
|
+
#
|
8
|
+
# Returns the return value of the block once the block succeeds.
|
9
|
+
# Raises the last seen exception if we run out of tries.
|
10
|
+
#
|
11
|
+
# Examples
|
12
|
+
#
|
13
|
+
# # Try 10 times to fetch http://google.com/
|
14
|
+
# response = try(10.times) { Net::HTTP.get_response("google.com", "/") }
|
15
|
+
#
|
16
|
+
# # Try many times, yielding the value of the enumeration to the block.
|
17
|
+
# # This allows you to try different inputs.
|
18
|
+
# response = try([0, 2, 4, 6]) { |val| 50 / val }
|
19
|
+
#
|
20
|
+
# Output:
|
21
|
+
# Failed (divided by 0). Retrying in 0.01 seconds...
|
22
|
+
# => 25
|
23
|
+
#
|
24
|
+
# # Try forever
|
25
|
+
# return_value = try { ... }
|
26
|
+
def try(enumerable=nil, &block)
|
27
|
+
if block.arity == 0
|
28
|
+
# If the block takes no arguments, give none
|
29
|
+
procedure = lambda { |val| block.call }
|
30
|
+
else
|
31
|
+
# Otherwise, pass the current 'enumerable' value to the block.
|
32
|
+
procedure = lambda { |val| block.call(val) }
|
33
|
+
end
|
34
|
+
|
35
|
+
last_exception = nil
|
36
|
+
|
37
|
+
# Retry after a sleep to be nice.
|
38
|
+
backoff = 0.01
|
39
|
+
backoff_max = 2.0
|
40
|
+
|
41
|
+
# Try forever if enumerable is nil.
|
42
|
+
if enumerable.nil?
|
43
|
+
enumerable = Enumerator.new do |y|
|
44
|
+
a = 0
|
45
|
+
while true
|
46
|
+
a += 1
|
47
|
+
y << a
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# When 'enumerable' runs out of things, if we still haven't succeeded, we'll
|
53
|
+
# reraise
|
54
|
+
enumerable.each do |val|
|
55
|
+
begin
|
56
|
+
# If the 'procedure' (the block, really) succeeds, we'll break
|
57
|
+
# and return the return value of the block. Win!
|
58
|
+
return procedure.call(val)
|
59
|
+
rescue => e
|
60
|
+
puts "Failed (#{e}). Retrying in #{backoff} seconds..."
|
61
|
+
last_exception = e
|
62
|
+
|
63
|
+
# Exponential backoff
|
64
|
+
sleep(backoff)
|
65
|
+
backoff = [backoff * 2, backoff_max].min unless backoff == backoff_max
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# generally make the exception appear from the 'try' method itself, not from
|
70
|
+
# any deeply nested enumeration/begin/etc
|
71
|
+
last_exception.set_backtrace(StandardError.new.backtrace)
|
72
|
+
raise last_exception
|
73
|
+
end # def try
|
74
|
+
|
75
|
+
extend self
|
76
|
+
end # module Stud
|
metadata
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stud
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jordan Sissel
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-06-10 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: small reusable bits of code I'm tired of writing over and over. A library
|
15
|
+
form of my software-patterns github repo.
|
16
|
+
email: jls@semicomplete.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- lib/stud/task.rb
|
22
|
+
- lib/stud/pool.rb
|
23
|
+
- lib/stud/interval.rb
|
24
|
+
- lib/stud/try.rb
|
25
|
+
- lib/stud/supervise.rb
|
26
|
+
- LICENSE
|
27
|
+
- CHANGELIST
|
28
|
+
- README.md
|
29
|
+
homepage: https://github.com/jordansissel/ruby-stud
|
30
|
+
licenses: []
|
31
|
+
post_install_message:
|
32
|
+
rdoc_options: []
|
33
|
+
require_paths:
|
34
|
+
- lib
|
35
|
+
- lib
|
36
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ! '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
requirements: []
|
49
|
+
rubyforge_project:
|
50
|
+
rubygems_version: 1.8.24
|
51
|
+
signing_key:
|
52
|
+
specification_version: 3
|
53
|
+
summary: stud - common code techniques
|
54
|
+
test_files: []
|
55
|
+
has_rdoc:
|