class Google::Cloud::Env::LazyValue
def initialize retries: nil, &block
-
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