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.
@@ -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