Class: BlockRepeater::Repeater

Inherits:
Object
  • Object
show all
Includes:
RepeaterMethods
Defined in:
lib/block_repeater/repeater.rb

Overview

The class which governs when to stop repeating based on condition or timeout

Constant Summary collapse

@@default_exceptions =
[]

Constants included from RepeaterMethods

RepeaterMethods::UNTIL_METHOD_REGEX

Class Method Summary collapse

Instance Method Summary collapse

Methods included from RepeaterMethods

#call_if_method_responsive, #method_missing, #respond_to_missing?

Constructor Details

#initialize(manual_repeat: true, **kwargs, &block) ⇒ Repeater

Prepare the Repeater to take the initial block to be repeated

Parameters:

  • manual_repeat (defaults to: true)
    • Determines whether the repeat method is called manually, used by the Repeatable module

  • **kwargs
    • Capture other arguments in the situation where repeat isn’t being called manually

  • &block
    • The block of code to be repeated



21
22
23
24
25
26
# File 'lib/block_repeater/repeater.rb', line 21

def initialize(manual_repeat: true, **kwargs, &block)
  @manual_repeat = manual_repeat
  @repeater_arguments = kwargs
  @repeat_block = block
  @anticipated_exceptions = []
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method in the class RepeaterMethods

Class Method Details

.default_catch(exceptions: [], behaviour: :defer, &block) ⇒ Object

Same as #catch but defines default behaviours shared by all BlockRepeater instances except that there is no default exception type, it must be defined



125
126
127
# File 'lib/block_repeater/repeater.rb', line 125

def self.default_catch(exceptions: [], behaviour: :defer, &block)
  @@default_exceptions << ExceptionResponse.new(types: exceptions, behaviour: behaviour, &block)
end

Instance Method Details

#backoff(timeout: 10, initial_wait: 0.1, multiplier: 2) ⇒ Object

Retry a call whilst exponentially increasing the wait time between each iteration until a timeout is reached

Parameters:

  • `timeout` (Integer)

    is the max amount wait of time you wish to try the action

  • `initial_wait` (Integer)

    is the initial wait time to retry the action

  • `multiplier` (Integer)

    is the rate at which you increase the wait time between each iteration

Raises:

  • (StandardError)


79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/block_repeater/repeater.rb', line 79

def backoff(timeout: 10, initial_wait: 0.1, multiplier: 2)
  raise StandardError, 'Multiplier cannot be less than 1.1' if multiplier < 1.1

  condition_met = nil
  result = nil

  current_sleep_seconds = initial_wait
  start_time = Time.now

  until current_sleep_seconds >= timeout
    result = @repeat_block.call
    condition_met = @condition_block.call(result) if @condition_block

    duration = Time.now - start_time

    break if condition_met || duration > timeout

    # calculating exponential increase
    current_sleep_seconds *= multiplier
    # how long the total duration will be after the next sleep
    projected_duration = current_sleep_seconds + duration
    # if projected duration exceeds the timeout, reduce time for next sleep to allow one final call
    current_sleep_seconds = (timeout - duration) if projected_duration > timeout

    sleep(current_sleep_seconds)
  end
  result
end

#catch(exceptions: [StandardError], behaviour: :defer, &block) ⇒ Object

Determine how to respond to exceptions raised while repeating, must be called before #until

Parameters:

  • &block
    • Code to execute when an exception is encountered

  • exceptions (defaults to: [StandardError])
    • Which exceptions are being handled by this block, defaults to StandardError

  • behaviour (defaults to: :defer)
    • After encountering the exception how should the repeater behave:

    :stop - cease repeating, execute the given block :continue - execute the given block but keep repeating :defer - execute the block only if the exception still occurs after all repeat attempts



117
118
119
120
# File 'lib/block_repeater/repeater.rb', line 117

def catch(exceptions: [StandardError], behaviour: :defer, &block)
  @anticipated_exceptions << ExceptionResponse.new(types: exceptions, behaviour: behaviour, &block)
  self
end

#repeat(times: 25, delay: 0.2, **_) ⇒ Object

Repeat a block until either the defined timeout is met or the condition block returns true

Parameters:

  • times (defaults to: 25)
    • How many times to attempt to execute the main block

  • delay (defaults to: 0.2)
    • The amount of time to wait between each attempt

  • **_
    • Capture any extra keyword arguments and discard them

Returns:

  • The result of calling the main block the final time



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/block_repeater/repeater.rb', line 35

def repeat(times: 25, delay: 0.2, **_)
  result, @condition_met, deferred_exception = nil
  anticipated_exception_types = @anticipated_exceptions.map(&:types).flatten
  default_exception_types = @@default_exceptions.map(&:types).flatten
  exception_types = anticipated_exception_types + default_exception_types

  times.times do
    begin
      result = @repeat_block.call
      @condition_met = @condition_block.call(result) if @condition_block
      deferred_exception = nil
    rescue *exception_types => e
      exceptions = if anticipated_exception_types.any? do |ex|
        e.class <= ex
      end
                     @anticipated_exceptions
                   else
                     @@default_exceptions
                   end
      matched_response = exceptions.detect { |expected| expected.types.any? { |ex| e.class <= ex } }
      if matched_response.behaviour == :defer
        deferred_exception = matched_response
        deferred_exception.actual = e
      else
        matched_response.execute(e)
      end

      break if matched_response.behaviour == :stop
    end

    break if @condition_met

    sleep delay
  end

  deferred_exception&.execute

  result
end

#until(&block) ⇒ Object

Set the block which determines if the main block should stop being executed

Parameters:

  • &block
    • The block of code which determines the target condition of the main block

Returns:

  • Either return the repeater object if the repeat method is being called manually or return the result of calling the repeat method



135
136
137
138
139
140
141
142
# File 'lib/block_repeater/repeater.rb', line 135

def until(&block)
  @condition_block = block
  if @manual_repeat
    self
  else
    repeat(**@repeater_arguments)
  end
end