statelydb 0.14.0 → 0.15.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a723737fdce6e8ad2bec8937cfd79f6d968df0fc3e9e747c64e18af9ce769601
4
- data.tar.gz: 1f0279a8b9507f4cffacc360ab82e2289471a40de39bd3891f0c7db5c63877c6
3
+ metadata.gz: f1d7563d7f40d84bdc1ef3dacff41707ed1d915b638223d33b488ca4c7de531f
4
+ data.tar.gz: dd682b92fbc100505e15820c5415acadc9defc6bc2f2d0dad5b5797deddbf24b
5
5
  SHA512:
6
- metadata.gz: 815835070a91366b18948afe145eb548ae4f6ecbc6559ed3b8193288cd1084b7ac1a416264b0769afea93f4a8c01e17b0474facb46fe90460b0b3db803466428
7
- data.tar.gz: e0c75f31911432f2544c4097fbc0c7a1615624257e80495aef9e5b3a0dddc92dd9341683d24416c4a32ab19b8bd34a2d79abb577781d75ee41b55a02f2e2e11c
6
+ metadata.gz: c0f27aa770fdf0c7b3386ac37862226cb4b5215766d265e47c82323b2157966e6ce0d87b5e4081d8d9dcd549f45cd0f6527203b0025b0e6c62ce0780c81dae74
7
+ data.tar.gz: de86db0a2a0227e6bec231d11c378bffff42fa7911ccc0874bd3ed97fae7f73d0ac29ce55b31817f75bfa82c0b63daa79dabd28276a66e17e2748770450f6243
@@ -30,16 +30,19 @@ module StatelyDB
30
30
  # @param [String] client_secret The StatelyDB client secret credential
31
31
  # @param [String] client_id The StatelyDB client ID credential
32
32
  # @param [String] access_key The StatelyDB access key credential
33
+ # @param [Float] base_retry_backoff_secs The base retry backoff in seconds
33
34
  def initialize(
34
35
  origin: "https://oauth.stately.cloud",
35
36
  audience: "api.stately.cloud",
36
37
  client_secret: ENV.fetch("STATELY_CLIENT_SECRET", nil),
37
38
  client_id: ENV.fetch("STATELY_CLIENT_ID", nil),
38
- access_key: ENV.fetch("STATELY_ACCESS_KEY", nil)
39
+ access_key: ENV.fetch("STATELY_ACCESS_KEY", nil),
40
+ base_retry_backoff_secs: 1
39
41
  )
40
42
  super()
41
43
  @actor = Async::Actor.new(Actor.new(origin: origin, audience: audience,
42
- client_secret: client_secret, client_id: client_id, access_key: access_key))
44
+ client_secret: client_secret, client_id: client_id, access_key: access_key,
45
+ base_retry_backoff_secs: base_retry_backoff_secs))
43
46
  # this initialization cannot happen in the constructor because it is async and must run on the event loop
44
47
  # which is not available in the constructor
45
48
  @actor.init
@@ -64,18 +67,23 @@ module StatelyDB
64
67
  # @param [String] audience The OAuth Audience for the token
65
68
  # @param [String] client_secret The StatelyDB client secret credential
66
69
  # @param [String] client_id The StatelyDB client ID credential
70
+ # @param [String] access_key The StatelyDB access key credential
71
+ # @param [Float] base_retry_backoff_secs The base retry backoff in seconds
67
72
  def initialize(
68
73
  origin:,
69
74
  audience:,
70
75
  client_secret:,
71
76
  client_id:,
72
- access_key:
77
+ access_key:,
78
+ base_retry_backoff_secs:
73
79
  )
74
80
  super()
75
81
 
76
82
  @token_fetcher = nil
77
83
  if !access_key.nil?
78
- @token_fetcher = StatelyDB::Common::Auth::StatelyAccessTokenFetcher.new(origin: origin, access_key: access_key)
84
+ @token_fetcher = StatelyDB::Common::Auth::StatelyAccessTokenFetcher.new(
85
+ origin: origin, access_key: access_key, base_retry_backoff_secs: base_retry_backoff_secs
86
+ )
79
87
  elsif !client_secret.nil? && !client_id.nil?
80
88
  @token_fetcher = StatelyDB::Common::Auth::Auth0TokenFetcher.new(origin: origin, audience: audience,
81
89
  client_secret: client_secret, client_id: client_id)
@@ -58,8 +58,6 @@ module StatelyDB
58
58
  body = JSON.dump({ "client_id" => @client_id, client_secret: @client_secret, audience: @audience,
59
59
  grant_type: DEFAULT_GRANT_TYPE })
60
60
  Sync do
61
- # TODO: Wrap this in a retry loop and parse errors like we
62
- # do in the Go SDK.
63
61
  response = @client.post("/oauth/token", headers, body)
64
62
  raise "Auth request failed" if response.status != 200
65
63
 
@@ -77,11 +75,22 @@ module StatelyDB
77
75
 
78
76
  # StatelyAccessTokenFetcher is a TokenFetcher that fetches tokens from the StatelyDB API
79
77
  class StatelyAccessTokenFetcher < TokenFetcher
78
+ NON_RETRYABLE_ERRORS = [
79
+ GRPC::Core::StatusCodes::UNAUTHENTICATED,
80
+ GRPC::Core::StatusCodes::PERMISSION_DENIED,
81
+ GRPC::Core::StatusCodes::NOT_FOUND,
82
+ GRPC::Core::StatusCodes::UNIMPLEMENTED,
83
+ GRPC::Core::StatusCodes::INVALID_ARGUMENT
84
+ ].freeze
85
+ RETRY_ATTEMPTS = 10
86
+
80
87
  # @param [String] origin The origin of the OAuth server
81
88
  # @param [String] access_key The StatelyDB access key credential
82
- def initialize(origin:, access_key:)
89
+ # @param [Float] base_retry_backoff_secs The base backoff time in seconds
90
+ def initialize(origin:, access_key:, base_retry_backoff_secs:)
83
91
  super()
84
92
  @access_key = access_key
93
+ @base_retry_backoff_secs = base_retry_backoff_secs
85
94
  @channel = Common::Net.new_channel(endpoint: origin)
86
95
  error_interceptor = Common::ErrorInterceptor.new
87
96
  @stub = Stately::Auth::AuthService::Stub.new(nil, nil, channel_override: @channel,
@@ -91,14 +100,45 @@ module StatelyDB
91
100
  # Fetch a new token from the StatelyDB API
92
101
  # @return [TokenResult] The fetched TokenResult
93
102
  def fetch
94
- resp = @stub.get_auth_token(Stately::Auth::GetAuthTokenRequest.new(access_key: @access_key))
95
- TokenResult.new(token: resp.auth_token, expires_in_secs: resp.expires_in_s)
103
+ RETRY_ATTEMPTS.times do |i|
104
+ resp = @stub.get_auth_token(Stately::Auth::GetAuthTokenRequest.new(access_key: @access_key))
105
+ return TokenResult.new(token: resp.auth_token, expires_in_secs: resp.expires_in_s)
106
+ rescue StatelyDB::Error => e
107
+ # raise if it's the final attempt or if the error is not retryable
108
+ raise e unless self.class.retryable_error?(e) && i < RETRY_ATTEMPTS - 1
109
+
110
+ # exponential backoff
111
+ sleep(backoff(i, @base_retry_backoff_secs))
112
+ end
96
113
  end
97
114
 
98
115
  def close
99
116
  @channel&.close
100
117
  end
118
+
119
+ # Check if an error is retryable
120
+ # @param [StatelyDB::Error] err The error to check
121
+ # @return [Boolean] True if the error is retryable
122
+ def self.retryable_error?(err)
123
+ !NON_RETRYABLE_ERRORS.include?(err.code)
124
+ end
101
125
  end
102
126
  end
103
127
  end
104
128
  end
129
+
130
+ # backoff returns a duration to wait before retrying a request. `attempt` is
131
+ # the current attempt number, starting from 0 (e.g. the first attempt is 0,
132
+ # then 1, then 2...).
133
+ #
134
+ # @param [Integer] attempt The current attempt number
135
+ # @param [Float] base_backoff The base backoff time in seconds
136
+ # @return [Float] The duration in seconds to wait before retrying
137
+ def backoff(attempt, base_backoff)
138
+ # Double the base backoff time per attempt, starting with 1
139
+ exp = 2**attempt
140
+ # Add a full jitter to the backoff time, from no wait to 100% of the exponential backoff.
141
+ # See https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
142
+ jitter = rand
143
+ (exp * jitter * base_backoff)
144
+ end
data/sig/statelydb.rbi CHANGED
@@ -636,11 +636,22 @@ module StatelyDB
636
636
 
637
637
  # StatelyAccessTokenFetcher is a TokenFetcher that fetches tokens from the StatelyDB API
638
638
  class StatelyAccessTokenFetcher < StatelyDB::Common::Auth::TokenFetcher
639
+ NON_RETRYABLE_ERRORS = T.let([
640
+ GRPC::Core::StatusCodes::UNAUTHENTICATED,
641
+ GRPC::Core::StatusCodes::PERMISSION_DENIED,
642
+ GRPC::Core::StatusCodes::NOT_FOUND,
643
+ GRPC::Core::StatusCodes::UNIMPLEMENTED,
644
+ GRPC::Core::StatusCodes::INVALID_ARGUMENT
645
+ ].freeze, T.untyped)
646
+ RETRY_ATTEMPTS = T.let(10, T.untyped)
647
+
639
648
  # _@param_ `origin` — The origin of the OAuth server
640
649
  #
641
650
  # _@param_ `access_key` — The StatelyDB access key credential
642
- sig { params(origin: String, access_key: String).void }
643
- def initialize(origin:, access_key:); end
651
+ #
652
+ # _@param_ `base_retry_backoff_secs` — The base backoff time in seconds
653
+ sig { params(origin: String, access_key: String, base_retry_backoff_secs: Float).void }
654
+ def initialize(origin:, access_key:, base_retry_backoff_secs:); end
644
655
 
645
656
  # Fetch a new token from the StatelyDB API
646
657
  #
@@ -650,6 +661,14 @@ module StatelyDB
650
661
 
651
662
  sig { returns(T.untyped) }
652
663
  def close; end
664
+
665
+ # Check if an error is retryable
666
+ #
667
+ # _@param_ `err` — The error to check
668
+ #
669
+ # _@return_ — True if the error is retryable
670
+ sig { params(err: StatelyDB::Error).returns(T::Boolean) }
671
+ def self.retryable_error?(err); end
653
672
  end
654
673
 
655
674
  # TokenProvider is an abstract base class that should be extended
@@ -682,16 +701,19 @@ module StatelyDB
682
701
  # _@param_ `client_id` — The StatelyDB client ID credential
683
702
  #
684
703
  # _@param_ `access_key` — The StatelyDB access key credential
704
+ #
705
+ # _@param_ `base_retry_backoff_secs` — The base retry backoff in seconds
685
706
  sig do
686
707
  params(
687
708
  origin: String,
688
709
  audience: String,
689
710
  client_secret: String,
690
711
  client_id: String,
691
- access_key: String
712
+ access_key: String,
713
+ base_retry_backoff_secs: Float
692
714
  ).void
693
715
  end
694
- def initialize(origin: "https://oauth.stately.cloud", audience: "api.stately.cloud", client_secret: ENV.fetch("STATELY_CLIENT_SECRET", nil), client_id: ENV.fetch("STATELY_CLIENT_ID", nil), access_key: ENV.fetch("STATELY_ACCESS_KEY", nil)); end
716
+ def initialize(origin: "https://oauth.stately.cloud", audience: "api.stately.cloud", client_secret: ENV.fetch("STATELY_CLIENT_SECRET", nil), client_id: ENV.fetch("STATELY_CLIENT_ID", nil), access_key: ENV.fetch("STATELY_ACCESS_KEY", nil), base_retry_backoff_secs: 1); end
695
717
 
696
718
  # Close the token provider and kill any background operations
697
719
  # This just invokes the close method on the actor which should do the cleanup
@@ -714,16 +736,21 @@ module StatelyDB
714
736
  # _@param_ `client_secret` — The StatelyDB client secret credential
715
737
  #
716
738
  # _@param_ `client_id` — The StatelyDB client ID credential
739
+ #
740
+ # _@param_ `access_key` — The StatelyDB access key credential
741
+ #
742
+ # _@param_ `base_retry_backoff_secs` — The base retry backoff in seconds
717
743
  sig do
718
744
  params(
719
745
  origin: String,
720
746
  audience: String,
721
747
  client_secret: String,
722
748
  client_id: String,
723
- access_key: T.untyped
749
+ access_key: String,
750
+ base_retry_backoff_secs: Float
724
751
  ).void
725
752
  end
726
- def initialize(origin:, audience:, client_secret:, client_id:, access_key:); end
753
+ def initialize(origin:, audience:, client_secret:, client_id:, access_key:, base_retry_backoff_secs:); end
727
754
 
728
755
  # Initialize the actor. This runs on the actor thread which means
729
756
  # we can dispatch async operations here.
data/sig/statelydb.rbs CHANGED
@@ -550,10 +550,15 @@ module StatelyDB
550
550
 
551
551
  # StatelyAccessTokenFetcher is a TokenFetcher that fetches tokens from the StatelyDB API
552
552
  class StatelyAccessTokenFetcher < StatelyDB::Common::Auth::TokenFetcher
553
+ NON_RETRYABLE_ERRORS: untyped
554
+ RETRY_ATTEMPTS: untyped
555
+
553
556
  # _@param_ `origin` — The origin of the OAuth server
554
557
  #
555
558
  # _@param_ `access_key` — The StatelyDB access key credential
556
- def initialize: (origin: String, access_key: String) -> void
559
+ #
560
+ # _@param_ `base_retry_backoff_secs` — The base backoff time in seconds
561
+ def initialize: (origin: String, access_key: String, base_retry_backoff_secs: Float) -> void
557
562
 
558
563
  # Fetch a new token from the StatelyDB API
559
564
  #
@@ -561,6 +566,13 @@ module StatelyDB
561
566
  def fetch: () -> TokenResult
562
567
 
563
568
  def close: () -> untyped
569
+
570
+ # Check if an error is retryable
571
+ #
572
+ # _@param_ `err` — The error to check
573
+ #
574
+ # _@return_ — True if the error is retryable
575
+ def self.retryable_error?: (StatelyDB::Error err) -> bool
564
576
  end
565
577
 
566
578
  # TokenProvider is an abstract base class that should be extended
@@ -591,12 +603,15 @@ module StatelyDB
591
603
  # _@param_ `client_id` — The StatelyDB client ID credential
592
604
  #
593
605
  # _@param_ `access_key` — The StatelyDB access key credential
606
+ #
607
+ # _@param_ `base_retry_backoff_secs` — The base retry backoff in seconds
594
608
  def initialize: (
595
609
  ?origin: String,
596
610
  ?audience: String,
597
611
  ?client_secret: String,
598
612
  ?client_id: String,
599
- ?access_key: String
613
+ ?access_key: String,
614
+ ?base_retry_backoff_secs: Float
600
615
  ) -> void
601
616
 
602
617
  # Close the token provider and kill any background operations
@@ -618,12 +633,17 @@ module StatelyDB
618
633
  # _@param_ `client_secret` — The StatelyDB client secret credential
619
634
  #
620
635
  # _@param_ `client_id` — The StatelyDB client ID credential
636
+ #
637
+ # _@param_ `access_key` — The StatelyDB access key credential
638
+ #
639
+ # _@param_ `base_retry_backoff_secs` — The base retry backoff in seconds
621
640
  def initialize: (
622
641
  origin: String,
623
642
  audience: String,
624
643
  client_secret: String,
625
644
  client_id: String,
626
- access_key: untyped
645
+ access_key: String,
646
+ base_retry_backoff_secs: Float
627
647
  ) -> void
628
648
 
629
649
  # Initialize the actor. This runs on the actor thread which means
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: statelydb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stately Cloud, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-12 00:00:00.000000000 Z
11
+ date: 2024-12-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async