@@ -3,6 +3,7 @@ defmodule CodexPooler.Upstreams.Reconciliation.PoolReconciliation do
33
44 import Ecto.Query
55
6+ alias CodexPooler.Jobs
67 alias CodexPooler.Pools.Pool
78 alias CodexPooler.Repo
89 alias CodexPooler.Upstreams.Auth.TokenRefresh
@@ -17,6 +18,7 @@ defmodule CodexPooler.Upstreams.Reconciliation.PoolReconciliation do
1718 @ eligible PoolUpstreamAssignment . eligible_status ( )
1819 @ health_active PoolUpstreamAssignment . active_health_status ( )
1920 @ account_quota_key "account"
21+ @ usage_auth_refresh_skew_seconds 5 * 60
2022 @ codex_usage_paths [
2123 "/api/codex/usage" ,
2224 "/backend-api/codex/usage" ,
@@ -204,7 +206,7 @@ defmodule CodexPooler.Upstreams.Reconciliation.PoolReconciliation do
204206 { :usage , identity , payload , windows }
205207
206208 { :error , { :upstream_status , status } } when status in [ 401 , 403 ] ->
207- retry_codex_usage_after_token_refresh ( identity , assignment , opts )
209+ maybe_retry_codex_usage_after_token_refresh ( identity , assignment , observed_at , opts )
208210
209211 _error ->
210212 :usage_unavailable
@@ -214,13 +216,62 @@ defmodule CodexPooler.Upstreams.Reconciliation.PoolReconciliation do
214216 end
215217 end
216218
219+ defp maybe_retry_codex_usage_after_token_refresh ( identity , assignment , observed_at , opts ) do
220+ if access_token_refresh_due_after_usage_auth_failure? ( identity , observed_at ) do
221+ retry_codex_usage_after_token_refresh ( identity , assignment , opts )
222+ else
223+ :usage_unavailable
224+ end
225+ end
226+
227+ defp access_token_refresh_due_after_usage_auth_failure? (
228+ % UpstreamIdentity { } = identity ,
229+ % DateTime { } = observed_at
230+ ) do
231+ case access_token_expires_at ( identity . metadata ) do
232+ { :ok , expires_at } ->
233+ refresh_at = DateTime . add ( observed_at , @ usage_auth_refresh_skew_seconds , :second )
234+ DateTime . compare ( expires_at , refresh_at ) in [ :lt , :eq ]
235+
236+ :unknown ->
237+ true
238+ end
239+ end
240+
241+ defp access_token_expires_at ( % { } = metadata ) do
242+ case metadata [ "access_token_expires_at" ] do
243+ expires_at when is_binary ( expires_at ) ->
244+ case DateTime . from_iso8601 ( expires_at ) do
245+ { :ok , parsed , _offset } -> { :ok , DateTime . truncate ( parsed , :microsecond ) }
246+ _invalid -> :unknown
247+ end
248+
249+ _value ->
250+ :unknown
251+ end
252+ end
253+
254+ defp access_token_expires_at ( _metadata ) , do: :unknown
255+
217256 defp retry_codex_usage_after_token_refresh ( identity , assignment , opts ) do
218- with { :ok , % { status: :active , identity: refreshed_identity } } <-
219- TokenRefresh . refresh_access_token ( identity ,
220- trigger_kind: "account_reconciliation" ,
221- receive_timeout: Keyword . get ( opts , :receive_timeout , 30_000 )
222- ) ,
223- { :ok , access_token } <- Secrets . decrypt_active_secret ( refreshed_identity , "access_token" ) ,
257+ case TokenRefresh . refresh_access_token ( identity ,
258+ trigger_kind: "account_reconciliation" ,
259+ receive_timeout: Keyword . get ( opts , :receive_timeout , 30_000 )
260+ ) do
261+ { :ok , % { status: :active , identity: refreshed_identity } } ->
262+ fetch_codex_usage_after_successful_token_refresh ( refreshed_identity , assignment , opts )
263+
264+ { :ok , % { status: :refresh_failed , retryable?: true , identity: failed_identity } } ->
265+ maybe_enqueue_account_reconciliation_token_refresh_recovery ( failed_identity )
266+ :auth_unavailable
267+
268+ _unavailable ->
269+ :auth_unavailable
270+ end
271+ end
272+
273+ defp fetch_codex_usage_after_successful_token_refresh ( refreshed_identity , assignment , opts ) do
274+ with { :ok , access_token } <- Secrets . decrypt_active_secret ( refreshed_identity , "access_token" ) ,
224275 observed_at <- now ( ) ,
225276 { :ok , payload , _url , windows } <-
226277 fetch_codex_usage_payload (
@@ -236,6 +287,26 @@ defmodule CodexPooler.Upstreams.Reconciliation.PoolReconciliation do
236287 end
237288 end
238289
290+ defp maybe_enqueue_account_reconciliation_token_refresh_recovery (
291+ % UpstreamIdentity { } = failed_identity
292+ ) do
293+ if account_reconciliation_refresh_failure? ( failed_identity ) do
294+ case Jobs . enqueue_token_refresh ( failed_identity ,
295+ trigger_kind: "account_reconciliation_recovery"
296+ ) do
297+ { :ok , % Oban.Job { } } -> :ok
298+ { :error , _reason } -> :ok
299+ end
300+ end
301+ end
302+
303+ defp account_reconciliation_refresh_failure? ( % UpstreamIdentity { } = identity ) do
304+ case identity . metadata [ "token_refresh" ] do
305+ % { "status" => "failed" , "trigger_kind" => "account_reconciliation" } -> true
306+ _metadata -> false
307+ end
308+ end
309+
239310 defp fetch_codex_usage_payload ( identity , assignment , access_token , observed_at , opts ) do
240311 base = upstream_usage_base_url ( identity , assignment )
241312 timeout = Keyword . get ( opts , :receive_timeout , 30_000 )
0 commit comments