google-cloud-env 1.5.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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