google-cloud-env 1.6.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -3
- data/README.md +15 -16
- data/lib/google/cloud/env/compute_metadata.rb +875 -0
- data/lib/google/cloud/env/compute_smbios.rb +138 -0
- data/lib/google/cloud/env/file_system.rb +125 -0
- data/lib/google/cloud/env/lazy_value.rb +1003 -0
- data/lib/google/cloud/env/variables.rb +76 -0
- data/lib/google/cloud/env/version.rb +5 -1
- data/lib/google/cloud/env.rb +197 -170
- metadata +15 -150
@@ -0,0 +1,1003 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2023 Google LLC
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
require "English"
|
18
|
+
|
19
|
+
module Google
|
20
|
+
module Cloud
|
21
|
+
class Env
|
22
|
+
##
|
23
|
+
# @private
|
24
|
+
#
|
25
|
+
# A lazy value box with thread-safe memoization. The first time accessed
|
26
|
+
# it will call a given block to compute its value, and will cache that
|
27
|
+
# value. Subsequent requests will return the cached value.
|
28
|
+
#
|
29
|
+
# At most one thread will be allowed to run the computation; if another
|
30
|
+
# thread is already in the middle of a computation, any new threads
|
31
|
+
# requesting the value will wait until the existing computation is
|
32
|
+
# complete, and will use that computation's result rather than kicking
|
33
|
+
# off their own computation.
|
34
|
+
#
|
35
|
+
# If a computation fails with an exception, that exception will also be
|
36
|
+
# memoized and reraised on subsequent accesses. A LazyValue can also be
|
37
|
+
# configured so subsequent accesses will retry the computation if the
|
38
|
+
# previous computation failed. The maximum number of retries is
|
39
|
+
# configurable, as is the retry "interval", i.e. the time since the last
|
40
|
+
# failure before an access will retry the computation.
|
41
|
+
#
|
42
|
+
# By default, a computation's memoized value (or final error after
|
43
|
+
# retries have been exhausted) is maintained for the lifetime of the Ruby
|
44
|
+
# process. However, a computation can also cause its result (or error) to
|
45
|
+
# expire after a specified number of seconds, forcing a recomputation on
|
46
|
+
# the next access following expiration, by calling
|
47
|
+
# {LazyValue.expiring_value} or {LazyValue.raise_expiring_error}.
|
48
|
+
#
|
49
|
+
# We keep this private for now so we can move it in the future if we need
|
50
|
+
# it to be available to other libraries. Currently it should not be used
|
51
|
+
# outside of Google::Cloud::Env.
|
52
|
+
#
|
53
|
+
class LazyValue
|
54
|
+
class << self
|
55
|
+
##
|
56
|
+
# Creates a special object that can be returned from a computation to
|
57
|
+
# indicate that a value expires after the given number of seconds.
|
58
|
+
# Any access after the expiration will cause a recomputation.
|
59
|
+
#
|
60
|
+
# @param lifetime [Numeric] timeout in seconds
|
61
|
+
# @param value [Object] the computation result
|
62
|
+
#
|
63
|
+
def expiring_value lifetime, value
|
64
|
+
return value unless lifetime
|
65
|
+
ExpiringValue.new lifetime, value
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Raise an error that, if it is the final result (i.e. retries have
|
70
|
+
# been exhausted), will expire after the given number of seconds. Any
|
71
|
+
# access after the expiration will cause a recomputation. If retries
|
72
|
+
# will not have been exhausted, expiration is ignored.
|
73
|
+
#
|
74
|
+
# The error can be specified as an exception object, a string (in
|
75
|
+
# which case a RuntimeError will be raised), or a class that descends
|
76
|
+
# from Exception (in which case an error of that type will be
|
77
|
+
# created, and passed any additional args given).
|
78
|
+
#
|
79
|
+
# @param lifetime [Numeric] timeout in seconds
|
80
|
+
# @param error [String,Exception,Class] the error to raise
|
81
|
+
# @param args [Array] any arguments to pass to an error constructor
|
82
|
+
#
|
83
|
+
def raise_expiring_error lifetime, error, *args
|
84
|
+
raise error unless lifetime
|
85
|
+
raise ExpiringError, lifetime if error.equal? $ERROR_INFO
|
86
|
+
if error.is_a?(Class) && error.ancestors.include?(Exception)
|
87
|
+
error = error.new(*args)
|
88
|
+
elsif !error.is_a? Exception
|
89
|
+
error = RuntimeError.new error.to_s
|
90
|
+
end
|
91
|
+
begin
|
92
|
+
raise error
|
93
|
+
rescue error.class
|
94
|
+
raise ExpiringError, lifetime
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
##
|
100
|
+
# Create a LazyValue.
|
101
|
+
#
|
102
|
+
# You must pass a block that will be called to compute the value the
|
103
|
+
# first time it is accessed. The block should evaluate to the desired
|
104
|
+
# value, or raise an exception on error. To specify a value that
|
105
|
+
# expires, use {LazyValue.expiring_value}. To raise an exception that
|
106
|
+
# expires, use {LazyValue.raise_expiring_error}.
|
107
|
+
#
|
108
|
+
# You can optionally pass a retry manager, which controls how
|
109
|
+
# subsequent accesses might try calling the block again if a compute
|
110
|
+
# attempt fails with an exception. A retry manager should either be an
|
111
|
+
# instance of {Retries} or an object that duck types it.
|
112
|
+
#
|
113
|
+
# @param retries [Retries] A retry manager. The default is a retry
|
114
|
+
# manager that tries only once.
|
115
|
+
# @param block [Proc] A block that can be called to attempt to compute
|
116
|
+
# the value.
|
117
|
+
#
|
118
|
+
def initialize retries: nil, &block
|
119
|
+
@retries = retries || Retries.new
|
120
|
+
@compute_handler = block
|
121
|
+
raise ArgumentError, "missing compute handler block" unless block
|
122
|
+
|
123
|
+
# Internally implemented by a state machine, protected by a mutex that
|
124
|
+
# ensures state transitions are consistent. The states themselves are
|
125
|
+
# implicit in the values of the various instance variables. The
|
126
|
+
# following are the major states:
|
127
|
+
#
|
128
|
+
# 1. **Pending** The value is not known and needs to be computed.
|
129
|
+
# @retries.finished? is false.
|
130
|
+
# @value is nil.
|
131
|
+
# @error is nil if no previous attempt has yet been made to
|
132
|
+
# compute the value, or set to the error that resulted from
|
133
|
+
# the most recent attempt.
|
134
|
+
# @expires_at is set to the monotonic time of the end of the
|
135
|
+
# current retry delay, or nil if the next computation attempt
|
136
|
+
# should happen immediately at the next access.
|
137
|
+
# @computing_thread is nil.
|
138
|
+
# @compute_notify is nil.
|
139
|
+
# @backfill_notify is set if currently backfilling, otherwise nil.
|
140
|
+
# From this state, calling #get will start computation (first
|
141
|
+
# waiting on @backfill_notify if present). Calling #expire! will
|
142
|
+
# have no effect.
|
143
|
+
#
|
144
|
+
# 2. **Computing** One thread has initiated computation. All other
|
145
|
+
# threads will be blocked (waiting on @compute_notify) until the
|
146
|
+
# computing thread finishes.
|
147
|
+
# @retries.finished? is false.
|
148
|
+
# @value and @error are nil.
|
149
|
+
# @expires_at is set to the monotonic time when computing started.
|
150
|
+
# @computing_thread is set to the thread that is computing.
|
151
|
+
# @compute_notify is set.
|
152
|
+
# @backfill_notify is nil.
|
153
|
+
# From this state, calling #get will cause the thread to wait
|
154
|
+
# (on @compute_notify) for the computing thread to complete.
|
155
|
+
# Calling #expire! will have no effect.
|
156
|
+
# When the computing thread finishes, it will transition either
|
157
|
+
# to Finished if the computation was successful or failed with
|
158
|
+
# no more retries, or back to Pending if computation failed with
|
159
|
+
# at least one retry remaining. It might also set @backfill_notify
|
160
|
+
# if other threads are waiting for completion.
|
161
|
+
#
|
162
|
+
# 3. **Finished** Computation has succeeded, or has failed and no
|
163
|
+
# more retries remain.
|
164
|
+
# @retries.finished? is true.
|
165
|
+
# either @value or @error is set, and the other is nil, depending
|
166
|
+
# on whether the final state is success or failure. (If both
|
167
|
+
# are nil, it is considered a @value of nil.)
|
168
|
+
# @expires_at is set to the monotonic time of expiration, or nil
|
169
|
+
# if there is no expiration.
|
170
|
+
# @computing_thread is nil.
|
171
|
+
# @compute_notify is nil.
|
172
|
+
# @backfill_notify is set if currently backfilling, otherwise nil.
|
173
|
+
# From this state, calling #get will either return the result or
|
174
|
+
# raise the error. If the current time exceeds @expires_at,
|
175
|
+
# however, it will block on @backfill_notify (if present), and
|
176
|
+
# and then transition to Pending first, and proceed from there.
|
177
|
+
# Calling #expire! will block on @backfill_notify (if present)
|
178
|
+
# and then transition to Pending,
|
179
|
+
#
|
180
|
+
# @backfill_notify can be set in the Pending or Finished states. This
|
181
|
+
# happens when threads that had been waiting on the previous
|
182
|
+
# computation are still clearing out and returning their results.
|
183
|
+
# Backfill must complete before the next computation attempt can be
|
184
|
+
# started from the Pending state, or before an expiration can take
|
185
|
+
# place from the Finished state. This prevents an "overlap" situation
|
186
|
+
# where a thread that had been waiting for a previous computation,
|
187
|
+
# isn't able to return the new result before some other thread starts
|
188
|
+
# a new computation or expires the value. Note that it is okay for
|
189
|
+
# #set! to be called during backfill; the threads still backfilling
|
190
|
+
# will simply return the new value.
|
191
|
+
#
|
192
|
+
# Note: One might ask if it would be simpler to extend the mutex
|
193
|
+
# across the entire computation, having it protect the computation
|
194
|
+
# itself, instead of the current approach of having explicit compute
|
195
|
+
# and backfill states with notifications and having the mutex protect
|
196
|
+
# only the state transition. However, this would not have been able
|
197
|
+
# to satisfy the requirement that we be able to detect whether a
|
198
|
+
# thread asked for the value during another thread's computation,
|
199
|
+
# and thus should "share" in that computation's result even if it's
|
200
|
+
# a failure (rather than kicking off a retry). Additionally, we
|
201
|
+
# consider it dangerous to have the computation block run inside a
|
202
|
+
# mutex, because arbitrary code can run there which might result in
|
203
|
+
# deadlocks.
|
204
|
+
@mutex = Thread::Mutex.new
|
205
|
+
# The evaluated, cached value, which could be nil.
|
206
|
+
@value = nil
|
207
|
+
# The last error encountered
|
208
|
+
@error = nil
|
209
|
+
# If non-nil, this is the CLOCK_MONOTONIC time when the current state
|
210
|
+
# expires. If the state is finished, this is the time the current
|
211
|
+
# value or error expires (while nil means it never expires). If the
|
212
|
+
# state is pending, this is the time the wait period before the next
|
213
|
+
# retry expires (and nil means there is no delay.) If the state is
|
214
|
+
# computing, this is the time when computing started.
|
215
|
+
@expires_at = nil
|
216
|
+
# Set to a condition variable during computation. Broadcasts when the
|
217
|
+
# computation is complete. Any threads wanting to get the value
|
218
|
+
# during computation must wait on this first.
|
219
|
+
@compute_notify = nil
|
220
|
+
# Set to a condition variable during backfill. Broadcasts when the
|
221
|
+
# last backfill thread is complete. Any threads wanting to expire the
|
222
|
+
# cache or start a new computation during backfill must wait on this
|
223
|
+
# first.
|
224
|
+
@backfill_notify = nil
|
225
|
+
# The number of threads waiting on backfill. Used to determine
|
226
|
+
# whether to activate backfill_notify when a computation completes.
|
227
|
+
@backfill_count = 0
|
228
|
+
# The thread running the current computation. This is tested against
|
229
|
+
# new requests to protect against deadlocks where a thread tries to
|
230
|
+
# re-enter from its own computation. This is also tested when a
|
231
|
+
# computation completes, to ensure that the computation is still
|
232
|
+
# relevant (i.e. if #set! interrupts a computation, this is reset to
|
233
|
+
# nil).
|
234
|
+
@computing_thread = nil
|
235
|
+
end
|
236
|
+
|
237
|
+
##
|
238
|
+
# Returns the value. This will either return the value or raise an
|
239
|
+
# error indicating failure to compute the value.
|
240
|
+
#
|
241
|
+
# If the value was previously cached, it will return that cached value,
|
242
|
+
# otherwise it will either run the computation to try to determine the
|
243
|
+
# value, or wait for another thread that is already running the
|
244
|
+
# computation. Thus, this method could block.
|
245
|
+
#
|
246
|
+
# Any arguments passed will be forwarded to the block if called, but
|
247
|
+
# are ignored if a cached value is returned.
|
248
|
+
#
|
249
|
+
# @return [Object] the value
|
250
|
+
# @raise [Exception] if an error happened while computing the value
|
251
|
+
#
|
252
|
+
def get *extra_args
|
253
|
+
@mutex.synchronize do
|
254
|
+
# Wait for any backfill to complete, and handle expiration first
|
255
|
+
# because it might change the state.
|
256
|
+
wait_backfill
|
257
|
+
do_expire if should_expire?
|
258
|
+
# Main state handling
|
259
|
+
if @retries.finished?
|
260
|
+
# finished state: return value or error
|
261
|
+
return cached_value
|
262
|
+
elsif !@compute_notify.nil?
|
263
|
+
# computing state: wait for the computing thread to finish then
|
264
|
+
# return its result
|
265
|
+
wait_compute
|
266
|
+
return cached_value
|
267
|
+
else
|
268
|
+
# pending state
|
269
|
+
cur_time = Process.clock_gettime Process::CLOCK_MONOTONIC
|
270
|
+
# waiting for the next retry: return current error
|
271
|
+
raise @error if @expires_at && cur_time < @expires_at
|
272
|
+
# no delay: compute in the current thread
|
273
|
+
enter_compute cur_time
|
274
|
+
# and continue below
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
# Gets here if we just transitioned from pending to compute
|
279
|
+
perform_compute extra_args
|
280
|
+
end
|
281
|
+
|
282
|
+
##
|
283
|
+
# This method calls {#get} repeatedly until a final result is available
|
284
|
+
# or retries have exhausted.
|
285
|
+
#
|
286
|
+
# Note: this method spins on {#get}, although honoring any retry delay.
|
287
|
+
# Thus, it is best to call this only if retries are limited or a retry
|
288
|
+
# delay has been configured.
|
289
|
+
#
|
290
|
+
# @param extra_args [Array] extra arguments to pass to the block
|
291
|
+
# @param transient_errors [Array<Class>] An array of exception classes
|
292
|
+
# that will be treated as transient and will allow await to
|
293
|
+
# continue retrying. Exceptions omitted from this list will be
|
294
|
+
# treated as fatal errors and abort the call. Default is
|
295
|
+
# `[StandardError]`.
|
296
|
+
# @param max_tries [Integer,nil] The maximum number of times this will
|
297
|
+
# call {#get} before giving up, or nil for a potentially unlimited
|
298
|
+
# number of attempts. Default is 1.
|
299
|
+
# @param max_time [Numeric,nil] The maximum time in seconds this will
|
300
|
+
# spend before giving up, or nil (the default) for a potentially
|
301
|
+
# unlimited timeout.
|
302
|
+
# @param delay_epsilon [Numeric] An extra delay in seconds to ensure
|
303
|
+
# that retries happen after the retry delay period
|
304
|
+
#
|
305
|
+
# @return [Object] the value
|
306
|
+
# @raise [Exception] if a fatal error happened, or retries have been
|
307
|
+
# exhausted.
|
308
|
+
#
|
309
|
+
def await *extra_args, transient_errors: nil, max_tries: 1, max_time: nil, delay_epsilon: 0.0001
|
310
|
+
transient_errors ||= [StandardError]
|
311
|
+
transient_errors = Array transient_errors
|
312
|
+
expiry_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) + max_time if max_time
|
313
|
+
begin
|
314
|
+
get(*extra_args)
|
315
|
+
rescue *transient_errors
|
316
|
+
# A snapshot of the state. It is possible that another thread has
|
317
|
+
# changed this state since we received the error. This is okay
|
318
|
+
# because our specification for this method is conservative:
|
319
|
+
# whatever we return will have been correct at some point.
|
320
|
+
state = internal_state
|
321
|
+
# Don't retry unless we're in a state where retries can happen.
|
322
|
+
raise if [:failed, :success].include? state[0]
|
323
|
+
if max_tries
|
324
|
+
# Handle retry countdown
|
325
|
+
max_tries -= 1
|
326
|
+
raise unless max_tries.positive?
|
327
|
+
end
|
328
|
+
# Determine the next delay
|
329
|
+
delay = determine_await_retry_delay state, expiry_time, delay_epsilon
|
330
|
+
# nil means we've exceeded the max time
|
331
|
+
raise if delay.nil?
|
332
|
+
sleep delay if delay.positive?
|
333
|
+
retry
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
##
|
338
|
+
# Returns the current low-level state immediately without waiting for
|
339
|
+
# computation. Returns a 3-tuple (i.e. a 3-element array) in which the
|
340
|
+
# first element is a symbol indicating the overall state, as described
|
341
|
+
# below, and the second and third elements are set accordingly.
|
342
|
+
#
|
343
|
+
# States (the first tuple element) are:
|
344
|
+
# * `:pending` - The value has not been computed, or previous
|
345
|
+
# computation attempts have failed but there are retries pending. The
|
346
|
+
# second element will be the most recent error, or nil if no
|
347
|
+
# computation attempt has yet happened. The third element will be the
|
348
|
+
# monotonic time of the end of the current retry delay, or nil if
|
349
|
+
# there will be no delay.
|
350
|
+
# * `:computing` - A thread is currently computing the value. The
|
351
|
+
# second element is nil. The third elements is the monotonic time
|
352
|
+
# when the computation started.
|
353
|
+
# * `:success` - The computation is finished, and the value is returned
|
354
|
+
# in the second element. The third element may be a numeric value
|
355
|
+
# indicating the expiration monotonic time, or nil for no expiration.
|
356
|
+
# * `:failed` - The computation failed finally and no more retries will
|
357
|
+
# be done. The error is returned in the second element. The third
|
358
|
+
# element may be a numeric value indicating the expiration monotonic
|
359
|
+
# time, or nil for no expiration.
|
360
|
+
#
|
361
|
+
# Future updates may add array elements without warning. Callers should
|
362
|
+
# be prepared to ignore additional unexpected elements.
|
363
|
+
#
|
364
|
+
# @return [Array]
|
365
|
+
#
|
366
|
+
def internal_state
|
367
|
+
@mutex.synchronize do
|
368
|
+
if @retries.finished?
|
369
|
+
if @error
|
370
|
+
[:failed, @error, @expires_at]
|
371
|
+
else
|
372
|
+
[:success, @value, @expires_at]
|
373
|
+
end
|
374
|
+
elsif @compute_notify.nil?
|
375
|
+
[:pending, @error, @expires_at]
|
376
|
+
else
|
377
|
+
[:computing, nil, @expires_at]
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
##
|
383
|
+
# Force this cache to expire immediately, if computation is complete.
|
384
|
+
# Any cached value will be cleared, the retry count is reset, and the
|
385
|
+
# next access will call the compute block as if it were the first
|
386
|
+
# access. Returns true if this took place. Has no effect and returns
|
387
|
+
# false if the computation is not yet complete (i.e. if a thread is
|
388
|
+
# currently computing, or if the last attempt failed and retries have
|
389
|
+
# not yet been exhausted.)
|
390
|
+
#
|
391
|
+
# @return [true,false] whether the cache was expired
|
392
|
+
#
|
393
|
+
def expire!
|
394
|
+
@mutex.synchronize do
|
395
|
+
wait_backfill
|
396
|
+
return false unless @retries.finished?
|
397
|
+
do_expire
|
398
|
+
true
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
##
|
403
|
+
# Set the cache value explicitly and immediately. If a computation is
|
404
|
+
# in progress, it is "detached" and its result will no longer be
|
405
|
+
# considered.
|
406
|
+
#
|
407
|
+
# @param value [Object] the value to set
|
408
|
+
# @param lifetime [Numeric] the lifetime until expiration in seconds,
|
409
|
+
# or nil (the default) for no expiration.
|
410
|
+
# @return [Object] the value
|
411
|
+
#
|
412
|
+
def set! value, lifetime: nil
|
413
|
+
@mutex.synchronize do
|
414
|
+
@value = value
|
415
|
+
@expires_at = determine_expiry lifetime
|
416
|
+
@error = nil
|
417
|
+
@retries.finish!
|
418
|
+
if @compute_notify.nil?
|
419
|
+
enter_backfill
|
420
|
+
leave_compute
|
421
|
+
end
|
422
|
+
value
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
private
|
427
|
+
|
428
|
+
##
|
429
|
+
# @private
|
430
|
+
# Internal type signaling a value with an expiration
|
431
|
+
#
|
432
|
+
class ExpiringValue
|
433
|
+
def initialize lifetime, value
|
434
|
+
@lifetime = lifetime
|
435
|
+
@value = value
|
436
|
+
end
|
437
|
+
|
438
|
+
attr_reader :lifetime
|
439
|
+
attr_reader :value
|
440
|
+
end
|
441
|
+
|
442
|
+
##
|
443
|
+
# @private
|
444
|
+
# Internal type signaling an error with an expiration.
|
445
|
+
#
|
446
|
+
class ExpiringError < StandardError
|
447
|
+
def initialize lifetime
|
448
|
+
super()
|
449
|
+
@lifetime = lifetime
|
450
|
+
end
|
451
|
+
|
452
|
+
attr_reader :lifetime
|
453
|
+
end
|
454
|
+
|
455
|
+
##
|
456
|
+
# @private
|
457
|
+
# Perform computation, and transition state on completion.
|
458
|
+
# This must be called from outside the mutex.
|
459
|
+
# Returns the final value, or raises the final error.
|
460
|
+
#
|
461
|
+
def perform_compute extra_args
|
462
|
+
value = @compute_handler.call(*extra_args)
|
463
|
+
@mutex.synchronize do
|
464
|
+
handle_success value
|
465
|
+
end
|
466
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
467
|
+
@mutex.synchronize do
|
468
|
+
handle_failure e
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
##
|
473
|
+
# @private
|
474
|
+
# Either return the cached value or raise the cached error.
|
475
|
+
# This must be called from within the mutex.
|
476
|
+
#
|
477
|
+
def cached_value
|
478
|
+
raise @error if @error
|
479
|
+
@value
|
480
|
+
end
|
481
|
+
|
482
|
+
##
|
483
|
+
# @private
|
484
|
+
# Determine whether we should expire a cached value and compute a new
|
485
|
+
# one. Happens in the Finished state if @expires_at is in the past.
|
486
|
+
# This must be called from within the mutex.
|
487
|
+
#
|
488
|
+
def should_expire?
|
489
|
+
@retries.finished? && @expires_at && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= @expires_at
|
490
|
+
end
|
491
|
+
|
492
|
+
##
|
493
|
+
# @private
|
494
|
+
# Reset this cache, transitioning to the Pending state and resetting
|
495
|
+
# the retry count.
|
496
|
+
# This must be called from within the mutex.
|
497
|
+
#
|
498
|
+
def do_expire
|
499
|
+
@retries.reset!
|
500
|
+
@value = @error = @expires_at = nil
|
501
|
+
end
|
502
|
+
|
503
|
+
##
|
504
|
+
# @private
|
505
|
+
# Wait for backfill to complete if it is in progress, otherwise just
|
506
|
+
# return immediately.
|
507
|
+
# This must be called from within the mutex.
|
508
|
+
#
|
509
|
+
def wait_backfill
|
510
|
+
@backfill_notify.wait @mutex while @backfill_notify
|
511
|
+
end
|
512
|
+
|
513
|
+
##
|
514
|
+
# @private
|
515
|
+
# Wait for computation to complete.
|
516
|
+
# Also adds the current thread to the backfill list, ensuring that the
|
517
|
+
# computing thread will enter the backfill phase on completion. Once
|
518
|
+
# computation is done, also checks whether the current thread is the
|
519
|
+
# last one to backfill, and if so, turns off backfill mode.
|
520
|
+
# This must be called from within the mutex.
|
521
|
+
#
|
522
|
+
def wait_compute
|
523
|
+
if Thread.current.equal? @computing_thread
|
524
|
+
raise ThreadError, "deadlock: tried to call LazyValue#get from its own computation"
|
525
|
+
end
|
526
|
+
@backfill_count += 1
|
527
|
+
begin
|
528
|
+
@compute_notify.wait @mutex
|
529
|
+
ensure
|
530
|
+
@backfill_count -= 1
|
531
|
+
leave_backfill
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
##
|
536
|
+
# @private
|
537
|
+
# Initializes compute mode.
|
538
|
+
# This must be called from within the mutex.
|
539
|
+
#
|
540
|
+
def enter_compute cur_time
|
541
|
+
@computing_thread = Thread.current
|
542
|
+
@compute_notify = Thread::ConditionVariable.new
|
543
|
+
@expires_at = cur_time
|
544
|
+
@value = @error = nil
|
545
|
+
end
|
546
|
+
|
547
|
+
##
|
548
|
+
# @private
|
549
|
+
# Finishes compute mode, notifying threads waiting on it.
|
550
|
+
# This must be called from within the mutex.
|
551
|
+
#
|
552
|
+
def leave_compute
|
553
|
+
@computing_thread = nil
|
554
|
+
@compute_notify.broadcast
|
555
|
+
@compute_notify = nil
|
556
|
+
end
|
557
|
+
|
558
|
+
##
|
559
|
+
# @private
|
560
|
+
# Checks for any threads that need backfill, and if so triggers
|
561
|
+
# backfill mode.
|
562
|
+
# This must be called from within the mutex.
|
563
|
+
#
|
564
|
+
def enter_backfill
|
565
|
+
return unless @backfill_count.positive?
|
566
|
+
@backfill_notify = Thread::ConditionVariable.new
|
567
|
+
end
|
568
|
+
|
569
|
+
##
|
570
|
+
# @private
|
571
|
+
# Checks whether all threads are done with backfill, and if so notifies
|
572
|
+
# threads waiting for backfill to finish.
|
573
|
+
# This must be called from within the mutex.
|
574
|
+
#
|
575
|
+
def leave_backfill
|
576
|
+
return unless @backfill_count.zero?
|
577
|
+
@backfill_notify.broadcast
|
578
|
+
@backfill_notify = nil
|
579
|
+
end
|
580
|
+
|
581
|
+
##
|
582
|
+
# @private
|
583
|
+
# Sets state to reflect a successful computation (as long as this
|
584
|
+
# computation wasn't interrupted by someone calling #set!).
|
585
|
+
# Then returns the computed value.
|
586
|
+
# This must be called from within the mutex.
|
587
|
+
#
|
588
|
+
def handle_success value
|
589
|
+
expires_at = nil
|
590
|
+
if value.is_a? ExpiringValue
|
591
|
+
expires_at = determine_expiry value.lifetime
|
592
|
+
value = value.value
|
593
|
+
end
|
594
|
+
if Thread.current.equal? @computing_thread
|
595
|
+
@retries.finish!
|
596
|
+
@error = nil
|
597
|
+
@value = value
|
598
|
+
@expires_at = expires_at
|
599
|
+
enter_backfill
|
600
|
+
leave_compute
|
601
|
+
end
|
602
|
+
value
|
603
|
+
end
|
604
|
+
|
605
|
+
##
|
606
|
+
# @private
|
607
|
+
# Sets state to reflect a failed computation (as long as this
|
608
|
+
# computation wasn't interrupted by someone calling #set!).
|
609
|
+
# Then raises the error.
|
610
|
+
# This must be called from within the mutex.
|
611
|
+
#
|
612
|
+
def handle_failure error
|
613
|
+
expires_at = nil
|
614
|
+
if error.is_a? ExpiringError
|
615
|
+
expires_at = determine_expiry error.lifetime
|
616
|
+
error = error.cause
|
617
|
+
end
|
618
|
+
if Thread.current.equal? @computing_thread
|
619
|
+
retry_delay = @retries.next start_time: @expires_at
|
620
|
+
@value = nil
|
621
|
+
@error = error
|
622
|
+
@expires_at =
|
623
|
+
if retry_delay.nil?
|
624
|
+
# No more retries; use the expiration for the error
|
625
|
+
expires_at
|
626
|
+
elsif retry_delay.positive?
|
627
|
+
determine_expiry retry_delay
|
628
|
+
end
|
629
|
+
enter_backfill
|
630
|
+
leave_compute
|
631
|
+
end
|
632
|
+
raise error
|
633
|
+
end
|
634
|
+
|
635
|
+
##
|
636
|
+
# @private
|
637
|
+
# Determines the delay until the next retry during an await
|
638
|
+
#
|
639
|
+
def determine_await_retry_delay state, expiry_time, delay_epsilon
|
640
|
+
cur_time = Process.clock_gettime Process::CLOCK_MONOTONIC
|
641
|
+
next_run_time =
|
642
|
+
if state[0] == :pending && state[2]
|
643
|
+
# Run at end of the current retry delay, plus an epsilon,
|
644
|
+
# if in pending state
|
645
|
+
state[2] + delay_epsilon
|
646
|
+
else
|
647
|
+
# Default to run immediately otherwise
|
648
|
+
cur_time
|
649
|
+
end
|
650
|
+
# Signal nil if we're past the max time
|
651
|
+
return nil if expiry_time && next_run_time > expiry_time
|
652
|
+
# No delay if we're already past the time we want to run
|
653
|
+
return 0 if next_run_time < cur_time
|
654
|
+
next_run_time - cur_time
|
655
|
+
end
|
656
|
+
|
657
|
+
##
|
658
|
+
# @private
|
659
|
+
# Determines the expires_at value in monotonic time, given a lifetime.
|
660
|
+
#
|
661
|
+
def determine_expiry lifetime
|
662
|
+
lifetime ? Process.clock_gettime(Process::CLOCK_MONOTONIC) + lifetime : nil
|
663
|
+
end
|
664
|
+
end
|
665
|
+
|
666
|
+
##
|
667
|
+
# @private
|
668
|
+
#
|
669
|
+
# This expands on {LazyValue} by providing a lazy key-value dictionary.
|
670
|
+
# Each key uses a separate LazyValue; hence multiple keys can be in the
|
671
|
+
# process of computation concurrently and independently.
|
672
|
+
#
|
673
|
+
# We keep this private for now so we can move it in the future if we need
|
674
|
+
# it to be available to other libraries. Currently it should not be used
|
675
|
+
# outside of Google::Cloud::Env.
|
676
|
+
#
|
677
|
+
class LazyDict
|
678
|
+
##
|
679
|
+
# Create a LazyDict.
|
680
|
+
#
|
681
|
+
# You must pass a block that will be called to compute the value the
|
682
|
+
# first time it is accessed. The block takes the key as an argument and
|
683
|
+
# should evaluate to the value for that key, or raise an exception on
|
684
|
+
# error. To specify a value that expires, use
|
685
|
+
# {LazyValue.expiring_value}. To raise an exception that expires, use
|
686
|
+
# {LazyValue.raise_expiring_error}.
|
687
|
+
#
|
688
|
+
# You can optionally pass a retry manager, which controls how
|
689
|
+
# subsequent accesses might try calling the block again if a compute
|
690
|
+
# attempt fails with an exception. A retry manager should either be an
|
691
|
+
# instance of {Retries} or an object that duck types it.
|
692
|
+
#
|
693
|
+
# @param retries [Retries,Proc] A retry manager. The default is a retry
|
694
|
+
# manager that tries only once. You can provide either a static
|
695
|
+
# retry manager or a Proc that returns a retry manager.
|
696
|
+
# @param block [Proc] A block that can be called to attempt to compute
|
697
|
+
# the value given the key.
|
698
|
+
#
|
699
|
+
def initialize retries: nil, &block
|
700
|
+
@retries = retries
|
701
|
+
@compute_handler = block
|
702
|
+
@key_values = {}
|
703
|
+
@mutex = Thread::Mutex.new
|
704
|
+
end
|
705
|
+
|
706
|
+
##
|
707
|
+
# Returns the value for the given key. This will either return the
|
708
|
+
# value or raise an error indicating failure to compute the value. If
|
709
|
+
# the value was previously cached, it will return that cached value,
|
710
|
+
# otherwise it will either run the computation to try to determine the
|
711
|
+
# value, or wait for another thread that is already running the
|
712
|
+
# computation.
|
713
|
+
#
|
714
|
+
# Any arguments beyond the initial key argument will be passed to the
|
715
|
+
# block if it is called, but are ignored if a cached value is returned.
|
716
|
+
#
|
717
|
+
# @param key [Object] the key
|
718
|
+
# @param extra_args [Array] extra arguments to pass to the block
|
719
|
+
# @return [Object] the value
|
720
|
+
# @raise [Exception] if an error happened while computing the value
|
721
|
+
#
|
722
|
+
def get key, *extra_args
|
723
|
+
lookup_key(key).get key, *extra_args
|
724
|
+
end
|
725
|
+
alias [] get
|
726
|
+
|
727
|
+
##
|
728
|
+
# This method calls {#get} repeatedly until a final result is available
|
729
|
+
# or retries have exhausted.
|
730
|
+
#
|
731
|
+
# Note: this method spins on {#get}, although honoring any retry delay.
|
732
|
+
# Thus, it is best to call this only if retries are limited or a retry
|
733
|
+
# delay has been configured.
|
734
|
+
#
|
735
|
+
# @param key [Object] the key
|
736
|
+
# @param extra_args [Array] extra arguments to pass to the block
|
737
|
+
# @param transient_errors [Array<Class>] An array of exception classes
|
738
|
+
# that will be treated as transient and will allow await to
|
739
|
+
# continue retrying. Exceptions omitted from this list will be
|
740
|
+
# treated as fatal errors and abort the call. Default is
|
741
|
+
# `[StandardError]`.
|
742
|
+
# @param max_tries [Integer,nil] The maximum number of times this will
|
743
|
+
# call {#get} before giving up, or nil for a potentially unlimited
|
744
|
+
# number of attempts. Default is 1.
|
745
|
+
# @param max_time [Numeric,nil] The maximum time in seconds this will
|
746
|
+
# spend before giving up, or nil (the default) for a potentially
|
747
|
+
# unlimited timeout.
|
748
|
+
#
|
749
|
+
# @return [Object] the value
|
750
|
+
# @raise [Exception] if a fatal error happened, or retries have been
|
751
|
+
# exhausted.
|
752
|
+
#
|
753
|
+
def await key, *extra_args, transient_errors: nil, max_tries: 1, max_time: nil
|
754
|
+
lookup_key(key).await key, *extra_args,
|
755
|
+
transient_errors: transient_errors,
|
756
|
+
max_tries: max_tries,
|
757
|
+
max_time: max_time
|
758
|
+
end
|
759
|
+
|
760
|
+
##
|
761
|
+
# Returns the current low-level state for the given key. Does not block
|
762
|
+
# for computation. See {LazyValue#internal_state} for details.
|
763
|
+
#
|
764
|
+
# @param key [Object] the key
|
765
|
+
# @return [Array] the low-level state
|
766
|
+
#
|
767
|
+
def internal_state key
|
768
|
+
lookup_key(key).internal_state
|
769
|
+
end
|
770
|
+
|
771
|
+
##
|
772
|
+
# Force the cache for the given key to expire immediately, if
|
773
|
+
# computation is complete.
|
774
|
+
#
|
775
|
+
# Any cached value will be cleared, the retry count is reset, and the
|
776
|
+
# next access will call the compute block as if it were the first
|
777
|
+
# access. Returns true if this took place. Has no effect and returns
|
778
|
+
# false if the computation is not yet complete (i.e. if a thread is
|
779
|
+
# currently computing, or if the last attempt failed and retries have
|
780
|
+
# not yet been exhausted.)
|
781
|
+
#
|
782
|
+
# @param key [Object] the key
|
783
|
+
# @return [true,false] whether the cache was expired
|
784
|
+
#
|
785
|
+
def expire! key
|
786
|
+
lookup_key(key).expire!
|
787
|
+
end
|
788
|
+
|
789
|
+
##
|
790
|
+
# Force the values for all keys to expire immediately.
|
791
|
+
#
|
792
|
+
# @return [Array<Object>] A list of keys that were expired. A key is
|
793
|
+
# *not* included if its computation is not yet complete (i.e. if a
|
794
|
+
# thread is currently computing, or if the last attempt failed and
|
795
|
+
# retries have not yet been exhausted.)
|
796
|
+
#
|
797
|
+
def expire_all!
|
798
|
+
all_expired = []
|
799
|
+
@mutex.synchronize do
|
800
|
+
@key_values.each do |key, value|
|
801
|
+
all_expired << key if value.expire!
|
802
|
+
end
|
803
|
+
end
|
804
|
+
all_expired
|
805
|
+
end
|
806
|
+
|
807
|
+
##
|
808
|
+
# Set the cache value for the given key explicitly and immediately.
|
809
|
+
# If a computation is in progress, it is "detached" and its result will
|
810
|
+
# no longer be considered.
|
811
|
+
#
|
812
|
+
# @param key [Object] the key
|
813
|
+
# @param value [Object] the value to set
|
814
|
+
# @param lifetime [Numeric] the lifetime until expiration in seconds,
|
815
|
+
# or nil (the default) for no expiration.
|
816
|
+
# @return [Object] the value
|
817
|
+
#
|
818
|
+
def set! key, value, lifetime: nil
|
819
|
+
lookup_key(key).set! value, lifetime: lifetime
|
820
|
+
end
|
821
|
+
|
822
|
+
private
|
823
|
+
|
824
|
+
##
|
825
|
+
# @private
|
826
|
+
# Ensures that exactly one LazyValue exists for the given key, and
|
827
|
+
# returns it.
|
828
|
+
#
|
829
|
+
def lookup_key key
|
830
|
+
# Optimization: check for key existence and return quickly without
|
831
|
+
# grabbing the mutex. This works because keys are never deleted.
|
832
|
+
return @key_values[key] if @key_values.key? key
|
833
|
+
|
834
|
+
@mutex.synchronize do
|
835
|
+
if @key_values.key? key
|
836
|
+
@key_values[key]
|
837
|
+
else
|
838
|
+
retries =
|
839
|
+
if @retries.respond_to? :reset_dup
|
840
|
+
@retries.reset_dup
|
841
|
+
elsif @retries.respond_to? :call
|
842
|
+
@retries.call
|
843
|
+
end
|
844
|
+
@key_values[key] = LazyValue.new retries: retries, &@compute_handler
|
845
|
+
end
|
846
|
+
end
|
847
|
+
end
|
848
|
+
end
|
849
|
+
|
850
|
+
##
|
851
|
+
# @private
|
852
|
+
#
|
853
|
+
# A simple retry manager with optional delay and backoff. It retries
|
854
|
+
# until either a configured maximum number of attempts has been
|
855
|
+
# reached, or a configurable total time has elapsed since the first
|
856
|
+
# failure.
|
857
|
+
#
|
858
|
+
# This class is not thread-safe by itself. Access should be protected
|
859
|
+
# by an external mutex.
|
860
|
+
#
|
861
|
+
# We keep this private for now so we can move it in the future if we need
|
862
|
+
# it to be available to other libraries. Currently it should not be used
|
863
|
+
# outside of Google::Cloud::Env.
|
864
|
+
#
|
865
|
+
class Retries
|
866
|
+
##
|
867
|
+
# Create and initialize a retry manager.
|
868
|
+
#
|
869
|
+
# @param max_tries [Integer,nil] Maximum number of attempts before we
|
870
|
+
# give up altogether, or nil for no maximum. Default is 1,
|
871
|
+
# indicating one attempt and no retries.
|
872
|
+
# @param max_time [Numeric,nil] The maximum amount of time in seconds
|
873
|
+
# until we give up altogether, or nil for no maximum. Default is
|
874
|
+
# nil.
|
875
|
+
# @param initial_delay [Numeric] Initial delay between attempts, in
|
876
|
+
# seconds. Default is 0.
|
877
|
+
# @param max_delay [Numeric,nil] Maximum delay between attempts, in
|
878
|
+
# seconds, or nil for no max. Default is nil.
|
879
|
+
# @param delay_multiplier [Numeric] Multipler applied to the delay
|
880
|
+
# between attempts. Default is 1 for no change.
|
881
|
+
# @param delay_adder [Numeric] Value added to the delay between
|
882
|
+
# attempts. Default is 0 for no change.
|
883
|
+
# @param delay_includes_time_elapsed [true,false] Whether to deduct any
|
884
|
+
# time already elapsed from the retry delay. Default is false.
|
885
|
+
#
|
886
|
+
def initialize max_tries: 1,
|
887
|
+
max_time: nil,
|
888
|
+
initial_delay: 0,
|
889
|
+
max_delay: nil,
|
890
|
+
delay_multiplier: 1,
|
891
|
+
delay_adder: 0,
|
892
|
+
delay_includes_time_elapsed: false
|
893
|
+
@max_tries = max_tries&.to_i
|
894
|
+
raise ArgumentError, "max_tries must be positive" if @max_tries && !@max_tries.positive?
|
895
|
+
@max_time = max_time
|
896
|
+
raise ArgumentError, "max_time must be positive" if @max_time && !@max_time.positive?
|
897
|
+
@initial_delay = initial_delay
|
898
|
+
raise ArgumentError, "initial_delay must be nonnegative" if @initial_delay&.negative?
|
899
|
+
@max_delay = max_delay
|
900
|
+
raise ArgumentError, "max_delay must be nonnegative" if @max_delay&.negative?
|
901
|
+
@delay_multiplier = delay_multiplier
|
902
|
+
@delay_adder = delay_adder
|
903
|
+
@delay_includes_time_elapsed = delay_includes_time_elapsed
|
904
|
+
reset!
|
905
|
+
end
|
906
|
+
|
907
|
+
##
|
908
|
+
# Create a duplicate in the reset state
|
909
|
+
#
|
910
|
+
# @return [Retries]
|
911
|
+
#
|
912
|
+
def reset_dup
|
913
|
+
Retries.new max_tries: @max_tries,
|
914
|
+
max_time: @max_time,
|
915
|
+
initial_delay: @initial_delay,
|
916
|
+
max_delay: @max_delay,
|
917
|
+
delay_multiplier: @delay_multiplier,
|
918
|
+
delay_adder: @delay_adder,
|
919
|
+
delay_includes_time_elapsed: @delay_includes_time_elapsed
|
920
|
+
end
|
921
|
+
|
922
|
+
##
|
923
|
+
# Returns true if the retry limit has been reached.
|
924
|
+
#
|
925
|
+
# @return [true,false]
|
926
|
+
#
|
927
|
+
def finished?
|
928
|
+
@current_delay.nil?
|
929
|
+
end
|
930
|
+
|
931
|
+
##
|
932
|
+
# Reset to the initial attempt.
|
933
|
+
#
|
934
|
+
# @return [self]
|
935
|
+
#
|
936
|
+
def reset!
|
937
|
+
@current_delay = :reset
|
938
|
+
self
|
939
|
+
end
|
940
|
+
|
941
|
+
##
|
942
|
+
# Cause the retry limit to be reached immediately.
|
943
|
+
#
|
944
|
+
# @return [self]
|
945
|
+
#
|
946
|
+
def finish!
|
947
|
+
@current_delay = nil
|
948
|
+
self
|
949
|
+
end
|
950
|
+
|
951
|
+
##
|
952
|
+
# Advance to the next attempt.
|
953
|
+
#
|
954
|
+
# Returns nil if the retry limit has been reached. Otherwise, returns
|
955
|
+
# the delay in seconds until the next retry (0 for no delay). Raises an
|
956
|
+
# error if the previous call already returned nil.
|
957
|
+
#
|
958
|
+
# @param start_time [Numeric,nil] Optional start time in monotonic time
|
959
|
+
# units. Used if delay_includes_time_elapsed is set.
|
960
|
+
# @return [Numeric,nil]
|
961
|
+
#
|
962
|
+
def next start_time: nil
|
963
|
+
raise "no tries remaining" if finished?
|
964
|
+
cur_time = Process.clock_gettime Process::CLOCK_MONOTONIC
|
965
|
+
if @current_delay == :reset
|
966
|
+
setup_first_retry cur_time
|
967
|
+
else
|
968
|
+
advance_delay
|
969
|
+
end
|
970
|
+
advance_retry cur_time
|
971
|
+
adjusted_delay start_time, cur_time
|
972
|
+
end
|
973
|
+
|
974
|
+
private
|
975
|
+
|
976
|
+
def setup_first_retry cur_time
|
977
|
+
@tries_remaining = @max_tries
|
978
|
+
@deadline = @max_time ? cur_time + @max_time : nil
|
979
|
+
@current_delay = @initial_delay
|
980
|
+
end
|
981
|
+
|
982
|
+
def advance_delay
|
983
|
+
@current_delay = (@delay_multiplier * @current_delay) + @delay_adder
|
984
|
+
@current_delay = @max_delay if @max_delay && @current_delay > @max_delay
|
985
|
+
end
|
986
|
+
|
987
|
+
def advance_retry cur_time
|
988
|
+
@tries_remaining -= 1 if @tries_remaining
|
989
|
+
@current_delay = nil if @tries_remaining&.zero? || (@deadline && cur_time + @current_delay > @deadline)
|
990
|
+
end
|
991
|
+
|
992
|
+
def adjusted_delay start_time, cur_time
|
993
|
+
delay = @current_delay
|
994
|
+
if @delay_includes_time_elapsed && start_time && delay
|
995
|
+
delay -= cur_time - start_time
|
996
|
+
delay = 0 if delay.negative?
|
997
|
+
end
|
998
|
+
delay
|
999
|
+
end
|
1000
|
+
end
|
1001
|
+
end
|
1002
|
+
end
|
1003
|
+
end
|