class Google::Cloud::Env::LazyValue

def initialize retries: nil, &block

Parameters:
  • block (Proc) -- A block that can be called to attempt to compute
  • retries (Retries) -- A retry manager. The default is a retry
def initialize retries: nil, &block
  @retries = retries || Retries.new
  @compute_handler = block
  raise ArgumentError, "missing compute handler block" unless block
  # Internally implemented by a state machine, protected by a mutex that
  # ensures state transitions are consistent. The states themselves are
  # implicit in the values of the various instance variables. The
  # following are the major states:
  #
  # 1. **Pending** The value is not known and needs to be computed.
  #     @retries.finished? is false.
  #     @value is nil.
  #     @error is nil if no previous attempt has yet been made to
  #         compute the value, or set to the error that resulted from
  #         the most recent attempt.
  #     @expires_at is set to the monotonic time of the end of the
  #         current retry delay, or nil if the next computation attempt
  #         should happen immediately at the next access.
  #     @computing_thread is nil.
  #     @compute_notify is nil.
  #     @backfill_notify is set if currently backfilling, otherwise nil.
  #     From this state, calling #get will start computation (first
  #     waiting on @backfill_notify if present). Calling #expire! will
  #     have no effect.
  #
  # 2. **Computing** One thread has initiated computation. All other
  #     threads will be blocked (waiting on @compute_notify) until the
  #     computing thread finishes.
  #     @retries.finished? is false.
  #     @value and @error are nil.
  #     @expires_at is set to the monotonic time when computing started.
  #     @computing_thread is set to the thread that is computing.
  #     @compute_notify is set.
  #     @backfill_notify is nil.
  #     From this state, calling #get will cause the thread to wait
  #     (on @compute_notify) for the computing thread to complete.
  #     Calling #expire! will have no effect.
  #     When the computing thread finishes, it will transition either
  #     to Finished if the computation was successful or failed with
  #     no more retries, or back to Pending if computation failed with
  #     at least one retry remaining. It might also set @backfill_notify
  #     if other threads are waiting for completion.
  #
  # 3. **Finished** Computation has succeeded, or has failed and no
  #     more retries remain.
  #     @retries.finished? is true.
  #     either @value or @error is set, and the other is nil, depending
  #         on whether the final state is success or failure. (If both
  #         are nil, it is considered a @value of nil.)
  #     @expires_at is set to the monotonic time of expiration, or nil
  #         if there is no expiration.
  #     @computing_thread is nil.
  #     @compute_notify is nil.
  #     @backfill_notify is set if currently backfilling, otherwise nil.
  #     From this state, calling #get will either return the result or
  #     raise the error. If the current time exceeds @expires_at,
  #     however, it will block on @backfill_notify (if present), and
  #     and then transition to Pending first, and proceed from there.
  #     Calling #expire! will block on @backfill_notify (if present)
  #     and then transition to Pending,
  #
  # @backfill_notify can be set in the Pending or Finished states. This
  # happens when threads that had been waiting on the previous
  # computation are still clearing out and returning their results.
  # Backfill must complete before the next computation attempt can be
  # started from the Pending state, or before an expiration can take
  # place from the Finished state. This prevents an "overlap" situation
  # where a thread that had been waiting for a previous computation,
  # isn't able to return the new result before some other thread starts
  # a new computation or expires the value. Note that it is okay for
  # #set! to be called during backfill; the threads still backfilling
  # will simply return the new value.
  #
  # Note: One might ask if it would be simpler to extend the mutex
  # across the entire computation, having it protect the computation
  # itself, instead of the current approach of having explicit compute
  # and backfill states with notifications and having the mutex protect
  # only the state transition. However, this would not have been able
  # to satisfy the requirement that we be able to detect whether a
  # thread asked for the value during another thread's computation,
  # and thus should "share" in that computation's result even if it's
  # a failure (rather than kicking off a retry). Additionally, we
  # consider it dangerous to have the computation block run inside a
  # mutex, because arbitrary code can run there which might result in
  # deadlocks.
  @mutex = Thread::Mutex.new
  # The evaluated, cached value, which could be nil.
  @value = nil
  # The last error encountered
  @error = nil
  # If non-nil, this is the CLOCK_MONOTONIC time when the current state
  # expires. If the state is finished, this is the time the current
  # value or error expires (while nil means it never expires). If the
  # state is pending, this is the time the wait period before the next
  # retry expires (and nil means there is no delay.) If the state is
  # computing, this is the time when computing started.
  @expires_at = nil
  # Set to a condition variable during computation. Broadcasts when the
  # computation is complete. Any threads wanting to get the value
  # during computation must wait on this first.
  @compute_notify = nil
  # Set to a condition variable during backfill. Broadcasts when the
  # last backfill thread is complete. Any threads wanting to expire the
  # cache or start a new computation during backfill must wait on this
  # first.
  @backfill_notify = nil
  # The number of threads waiting on backfill. Used to determine
  # whether to activate backfill_notify when a computation completes.
  @backfill_count = 0
  # The thread running the current computation. This is tested against
  # new requests to protect against deadlocks where a thread tries to
  # re-enter from its own computation. This is also tested when a
  # computation completes, to ensure that the computation is still
  # relevant (i.e. if #set! interrupts a computation, this is reset to
  # nil).
  @computing_thread = nil
end