-
Notifications
You must be signed in to change notification settings - Fork 3
Repeater helper #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ba56350
ddd1d2a
939625e
59dec80
3532743
02174fb
f1636a2
c3b1fb5
f28a501
6e578b5
db46f8b
12e966c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| Q = require 'q' | ||
| _ = require 'underscore' | ||
|
|
||
| ###* | ||
| * Repeater is designed to repeat some arbitrary function unless the execution of this function does not throw any errors | ||
| * | ||
| * Options: | ||
| * attempts - Int - how many times execution of the function should be repeated until repeater will give up (default 10) | ||
| * timeout - Long - the delay between attempts | ||
| * timeoutType - String - The type of the timeout: | ||
| * 'constant' - always the same timeout | ||
| * 'variable' - timeout grows with the attempts count (it also contains random component) | ||
| ### | ||
| class Repeater | ||
| constructor: (options = {}) -> | ||
| @_attempts = if options.attempts? then options.attempts else 10 | ||
| @_timeout = if options.timeout? then options.timeout else 100 | ||
| @_timeoutType = options.timeoutType or 'variable' | ||
|
|
||
| ###* | ||
| * Executes arbitrary function | ||
| * | ||
| * Options: | ||
| * task - () => Promise[Any] - the task that should be executed | ||
| * recoverableError - Error => Boolean - function that decides, whether an error can be recovered by repeating the task execution | ||
| ### | ||
| execute: (options) -> | ||
| throw new Error '`task` function is undefined' unless _.isFunction(options.task) | ||
| throw new Error '`recoverableError` function is undefined' unless _.isFunction(options.recoverableError) | ||
|
|
||
| d = Q.defer() | ||
|
|
||
| @_repeat(@_attempts, options, d, null) | ||
|
|
||
| d.promise | ||
|
|
||
| _repeat: (remainingAttempts, options, defer, lastError) -> | ||
| {task, recoverableError} = options | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here as well, and use {task, recoverableError} = _.defaults options,
task: # something
recoverableError: # somethingor throw an Error if those values aren't there |
||
|
|
||
| if remainingAttempts is 0 | ||
| defer.reject new Error("Unsuccessful after #{@_attempts} attempts. Cause: #{lastError.stack}") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why there are 2
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, maybe a better naming here? Like
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, I still would like to have the other renamed to |
||
| else | ||
| task() | ||
| .then (res) -> | ||
| defer.resolve res | ||
| .fail (e) => | ||
| if recoverableError(e) | ||
| Q.delay @_calculateDelay(remainingAttempts) | ||
| .then (i) => | ||
| @_repeat(remainingAttempts - 1, options, defer, e) | ||
| else | ||
| defer.reject e | ||
| .done() | ||
|
|
||
| _calculateDelay: (attemptsLeft) -> | ||
| if @_timeoutType is 'constant' | ||
| @_timeout | ||
| else if @_timeoutType is 'variable' | ||
| tried = @_attempts - attemptsLeft - 1 | ||
| (@_timeout * tried) + _.random(50, @_timeout) | ||
| else | ||
| throw new Error("Unsupported timeout type: #{@_timeoutType}") | ||
|
|
||
| exports.Repeater = Repeater | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| Q = require 'q' | ||
| _ = require 'underscore' | ||
| _.mixin require('underscore.string').exports() | ||
|
|
||
| {Repeater} = require '../../lib/helpers/repeater' | ||
|
|
||
| describe 'Repeater', -> | ||
| it 'should repeat task until it returns some successful result', (done) -> | ||
| repeated = 0 | ||
|
|
||
| new Repeater | ||
| attempts: 10 | ||
| timeout: 0 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will set @_timeout = options.timeout or 100If you allow to pass @_options = _.defaults options,
attempts: 10
timeout: 100
timeoutType: 'variable'
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. :( yeah, you are right... |
||
| .execute | ||
| recoverableError: (e) -> e.message is 'foo' | ||
| task: -> | ||
| repeated += 1 | ||
|
|
||
| if repeated < 3 | ||
| Q.reject(new Error('foo')) | ||
| else | ||
| Q("Success") | ||
| .then (res) -> | ||
| expect(repeated).toEqual 3 | ||
| expect(res).toEqual "Success" | ||
| done() | ||
| .fail (error) -> | ||
| done(error) | ||
|
|
||
| it 'should boubble up unrecoverable errors', (done) -> | ||
| repeated = 0 | ||
|
|
||
| new Repeater | ||
| attempts: 10 | ||
| timeout: 0 | ||
| .execute | ||
| recoverableError: (e) -> e.message is 'foo' | ||
| task: -> | ||
| repeated += 1 | ||
|
|
||
| if repeated < 3 | ||
| Q.reject(new Error('foo')) | ||
| else if repeated is 3 | ||
| Q.reject(new Error('baz')) | ||
| else | ||
| Q("Success") | ||
| .then (res) -> | ||
| done("Error was not produced.") | ||
| .fail (error) -> | ||
| expect(repeated).toEqual 3 | ||
| expect(error.message).toEqual "baz" | ||
| done() | ||
|
|
||
| it 'should boubble up an error after provided number of attempts', (done) -> | ||
| repeated = 0 | ||
|
|
||
| new Repeater | ||
| attempts: 3 | ||
| timeout: 0 | ||
| .execute | ||
| recoverableError: (e) -> e.message is 'foo' | ||
| task: -> | ||
| repeated += 1 | ||
| Q.reject(new Error('foo')) | ||
| .then (res) -> | ||
| done("Error was not produced.") | ||
| .fail (error) -> | ||
| expect(repeated).toEqual 3 | ||
| expect(_.startsWith(error.message, 'Unsuccessful after 3 attempts')).toBe true | ||
| done() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would like to see more test cases:
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain this concept a little better? It's not really clear...