stud 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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