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.
Files changed (3) hide show
  1. data/README.md +27 -0
  2. data/lib/stud/try.rb +104 -60
  3. 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.
@@ -1,75 +1,119 @@
1
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.
2
+
3
+ # A class implementing 'retry-on-failure'
10
4
  #
11
- # Examples
5
+ # Example:
12
6
  #
13
- # # Try 10 times to fetch http://google.com/
14
- # response = try(10.times) { Net::HTTP.get_response("google.com", "/") }
7
+ # Try.new.try(5.times) { your_code }
15
8
  #
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
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
- # # 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
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
- last_exception = nil
24
+ FOREVER = Forever.new
36
25
 
37
- # Retry after a sleep to be nice.
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
- # 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
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
- # 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
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
- # Exponential backoff
64
- sleep(backoff)
65
- backoff = [backoff * 2, backoff_max].min unless backoff == backoff_max
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
- # 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
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.1
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