stud 0.0.1 → 0.0.2
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/README.md +27 -0
- data/lib/stud/try.rb +104 -60
- metadata +34 -2
data/README.md
CHANGED
@@ -0,0 +1,27 @@
|
|
1
|
+
# Stud.
|
2
|
+
|
3
|
+
Ruby's stdlib is missing many things I use to solve most of my software
|
4
|
+
problems. Things like like retrying on a failure, supervising workers, resource
|
5
|
+
pools, etc.
|
6
|
+
|
7
|
+
In general, I started exploring solutions to these things in code over in my
|
8
|
+
[software-patterns](https://github.com/jordansissel/software-patterns) repo.
|
9
|
+
This library (stud) aims to be a well-tested, production-quality implementation
|
10
|
+
of the patterns described in that repo.
|
11
|
+
|
12
|
+
For now, these all exist in a single repo because, so far, implementations of
|
13
|
+
each 'pattern' are quite small by code size.
|
14
|
+
|
15
|
+
## Features
|
16
|
+
|
17
|
+
* retry on failure, with back-off, where failure is any exception.
|
18
|
+
* generic resource pools
|
19
|
+
* supervising tasks
|
20
|
+
* tasks (threads that can return values, exceptions, etc)
|
21
|
+
* interval execution (do X every N seconds)
|
22
|
+
|
23
|
+
## TODO:
|
24
|
+
|
25
|
+
* Make sure all things are documented. rubydoc.info should be able to clearly
|
26
|
+
show folks how to use features of this library.
|
27
|
+
* Add tests to cover all supported features.
|
data/lib/stud/try.rb
CHANGED
@@ -1,75 +1,119 @@
|
|
1
1
|
module Stud
|
2
|
-
|
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.
|
2
|
+
|
3
|
+
# A class implementing 'retry-on-failure'
|
10
4
|
#
|
11
|
-
#
|
5
|
+
# Example:
|
12
6
|
#
|
13
|
-
#
|
14
|
-
# response = try(10.times) { Net::HTTP.get_response("google.com", "/") }
|
7
|
+
# Try.new.try(5.times) { your_code }
|
15
8
|
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
# Output:
|
21
|
-
# Failed (divided by 0). Retrying in 0.01 seconds...
|
22
|
-
# => 25
|
9
|
+
# A failure is indicated by any exception being raised.
|
10
|
+
# On success, the return value of the block is the return value of the try
|
11
|
+
# call.
|
23
12
|
#
|
24
|
-
#
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
end
|
13
|
+
# On final failure (ran out of things to try), the last exception is raised.
|
14
|
+
class Try
|
15
|
+
# An infinite enumerator
|
16
|
+
class Forever
|
17
|
+
include Enumerable
|
18
|
+
def each(&block)
|
19
|
+
a = 0
|
20
|
+
yield a += 1 while true
|
21
|
+
end
|
22
|
+
end # class Forever
|
34
23
|
|
35
|
-
|
24
|
+
FOREVER = Forever.new
|
36
25
|
|
37
|
-
|
38
|
-
backoff = 0.01
|
39
|
-
backoff_max = 2.0
|
26
|
+
BACKOFF_SCHEDULE = [0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28, 2.0]
|
40
27
|
|
41
|
-
#
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
y << a
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
28
|
+
# Log a failure.
|
29
|
+
#
|
30
|
+
# You should override this method if you want a better logger.
|
31
|
+
def log_failure(exception, fail_count, message)
|
32
|
+
puts "Failed (#{exception}). #{message}"
|
33
|
+
end # def log_failure
|
51
34
|
|
52
|
-
#
|
53
|
-
#
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
35
|
+
# This method is called when a try attempt fails.
|
36
|
+
#
|
37
|
+
# The default implementation will sleep with exponential backoff up to a
|
38
|
+
# maximum of 2 seconds (see BACKOFF_SCHEDULE)
|
39
|
+
#
|
40
|
+
# exception - the exception causing the failure
|
41
|
+
# fail_count - how many times we have failed.
|
42
|
+
def failure(exception, fail_count)
|
43
|
+
backoff = BACKOFF_SCHEDULE[fail_count] || BACKOFF_SCHEDULE.last
|
44
|
+
log_failure(exception, fail_count, "Sleeping for #{backoff}")
|
45
|
+
sleep(backoff)
|
46
|
+
end # def failure
|
62
47
|
|
63
|
-
|
64
|
-
|
65
|
-
|
48
|
+
# Public: try a block of code until either it succeeds or we give up.
|
49
|
+
#
|
50
|
+
# enumerable - an Enumerable or omitted/nil, #each is invoked and is tried
|
51
|
+
# that number of times. If this value is omitted or nil, we will try until
|
52
|
+
# success with no limit on the number of tries.
|
53
|
+
#
|
54
|
+
# Returns the return value of the block once the block succeeds.
|
55
|
+
# Raises the last seen exception if we run out of tries.
|
56
|
+
#
|
57
|
+
# Examples
|
58
|
+
#
|
59
|
+
# # Try 10 times to fetch http://google.com/
|
60
|
+
# response = try(10.times) { Net::HTTP.get_response("google.com", "/") }
|
61
|
+
#
|
62
|
+
# # Try many times, yielding the value of the enumeration to the block.
|
63
|
+
# # This allows you to try different inputs.
|
64
|
+
# response = try([0, 2, 4, 6]) { |val| 50 / val }
|
65
|
+
#
|
66
|
+
# Output:
|
67
|
+
# Failed (divided by 0). Retrying in 0.01 seconds...
|
68
|
+
# => 25
|
69
|
+
#
|
70
|
+
# # Try forever
|
71
|
+
# return_value = try { ... }
|
72
|
+
def try(enumerable=FOREVER, &block)
|
73
|
+
if block.arity == 0
|
74
|
+
# If the block takes no arguments, give none
|
75
|
+
procedure = lambda { |val| block.call }
|
76
|
+
else
|
77
|
+
# Otherwise, pass the current 'enumerable' value to the block.
|
78
|
+
procedure = lambda { |val| block.call(val) }
|
66
79
|
end
|
67
|
-
end
|
68
80
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
81
|
+
# Track the last exception so we can reraise it on failure.
|
82
|
+
last_exception = nil
|
83
|
+
|
84
|
+
# When 'enumerable' runs out of things, if we still haven't succeeded,
|
85
|
+
# we'll reraise
|
86
|
+
fail_count = 0
|
87
|
+
enumerable.each do |val|
|
88
|
+
begin
|
89
|
+
# If the 'procedure' (the block, really) succeeds, we'll break
|
90
|
+
# and return the return value of the block. Win!
|
91
|
+
return procedure.call(val)
|
92
|
+
rescue => exception
|
93
|
+
last_exception = exception
|
94
|
+
fail_count += 1
|
95
|
+
|
96
|
+
# Note: Since we can't reliably detect the 'end' of an enumeration, we
|
97
|
+
# will call 'failure' for the final iteration (if it failed) and sleep
|
98
|
+
# even though there's no strong reason to backoff on the last error.
|
99
|
+
failure(exception, fail_count)
|
100
|
+
end
|
101
|
+
end # enumerable.each
|
102
|
+
|
103
|
+
# generally make the exception appear from the 'try' method itself, not from
|
104
|
+
# any deeply nested enumeration/begin/etc
|
105
|
+
# It is my hope that this makes the backtraces easier to read, not more
|
106
|
+
# difficult. If you find this is not the case, please please please let me
|
107
|
+
# know.
|
108
|
+
last_exception.set_backtrace(StandardError.new.backtrace)
|
109
|
+
raise last_exception
|
110
|
+
end # def try
|
111
|
+
end # class Stud::Try
|
112
|
+
|
113
|
+
TRY = Try.new
|
114
|
+
# A simple try method for the common case.
|
115
|
+
def try(enumerable=Stud::Try::Forever, &block)
|
116
|
+
return TRY.try(enumerable, &block)
|
73
117
|
end # def try
|
74
118
|
|
75
119
|
extend self
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stud
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,7 +10,39 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
date: 2012-06-10 00:00:00.000000000 Z
|
13
|
-
dependencies:
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: insist
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
14
46
|
description: small reusable bits of code I'm tired of writing over and over. A library
|
15
47
|
form of my software-patterns github repo.
|
16
48
|
email: jls@semicomplete.com
|