codexLB-up 1.25.0b10__py3-none-any.whl

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.
Files changed (827) hide show
  1. app/__init__.py +10 -0
  2. app/admin_cli.py +494 -0
  3. app/cli.py +339 -0
  4. app/codex_sessions_retag.py +487 -0
  5. app/core/__init__.py +0 -0
  6. app/core/audit/__init__.py +0 -0
  7. app/core/audit/service.py +216 -0
  8. app/core/audit/types.py +94 -0
  9. app/core/auth/__init__.py +236 -0
  10. app/core/auth/api_key_cache.py +65 -0
  11. app/core/auth/dashboard_access.py +429 -0
  12. app/core/auth/dashboard_mode.py +171 -0
  13. app/core/auth/dashboard_session_ttl.py +74 -0
  14. app/core/auth/dashboard_users_cache.py +120 -0
  15. app/core/auth/dependencies.py +710 -0
  16. app/core/auth/external_identity.py +78 -0
  17. app/core/auth/guardian.py +374 -0
  18. app/core/auth/models.py +49 -0
  19. app/core/auth/providers/__init__.py +129 -0
  20. app/core/auth/providers/oidc.py +838 -0
  21. app/core/auth/providers/registry.py +118 -0
  22. app/core/auth/refresh.py +394 -0
  23. app/core/auth/step_up.py +117 -0
  24. app/core/auth/totp.py +81 -0
  25. app/core/balancer/__init__.py +71 -0
  26. app/core/balancer/logic.py +1612 -0
  27. app/core/balancer/rendezvous_hash.py +21 -0
  28. app/core/balancer/types.py +21 -0
  29. app/core/bootstrap.py +172 -0
  30. app/core/cache/invalidation.py +426 -0
  31. app/core/clients/__init__.py +0 -0
  32. app/core/clients/codex.py +996 -0
  33. app/core/clients/codex_version.py +187 -0
  34. app/core/clients/codex_version_snapshot.py +2 -0
  35. app/core/clients/files.py +534 -0
  36. app/core/clients/headers.py +29 -0
  37. app/core/clients/http.py +511 -0
  38. app/core/clients/model_fetcher.py +276 -0
  39. app/core/clients/native_egress.py +1304 -0
  40. app/core/clients/oauth.py +477 -0
  41. app/core/clients/proxy.py +6089 -0
  42. app/core/clients/proxy_websocket.py +1411 -0
  43. app/core/clients/rate_limit_reset_credits.py +412 -0
  44. app/core/clients/stream_errors.py +12 -0
  45. app/core/clients/thread_cache_identity.py +347 -0
  46. app/core/clients/usage.py +467 -0
  47. app/core/clock.py +170 -0
  48. app/core/config/__init__.py +0 -0
  49. app/core/config/background_jobs.py +73 -0
  50. app/core/config/context_window_overrides.py +166 -0
  51. app/core/config/dashboard_overrides.py +145 -0
  52. app/core/config/inheritable.py +47 -0
  53. app/core/config/key_fingerprint.py +115 -0
  54. app/core/config/settings.py +872 -0
  55. app/core/config/settings_cache.py +100 -0
  56. app/core/config/spool_retention.py +217 -0
  57. app/core/config/tiers.py +174 -0
  58. app/core/conversation_archive.py +568 -0
  59. app/core/crypto.py +37 -0
  60. app/core/draining.py +11 -0
  61. app/core/errors.py +365 -0
  62. app/core/exceptions.py +155 -0
  63. app/core/handlers/__init__.py +3 -0
  64. app/core/handlers/exceptions.py +478 -0
  65. app/core/http_protocol.py +176 -0
  66. app/core/http_protocol_httptools.py +204 -0
  67. app/core/ingress_limits.py +17 -0
  68. app/core/metadata_store.py +18 -0
  69. app/core/metrics/__init__.py +0 -0
  70. app/core/metrics/middleware.py +74 -0
  71. app/core/metrics/prometheus.py +604 -0
  72. app/core/middleware/__init__.py +25 -0
  73. app/core/middleware/api_firewall.py +111 -0
  74. app/core/middleware/app_version.py +37 -0
  75. app/core/middleware/dashboard_auth_proxy.py +55 -0
  76. app/core/middleware/dashboard_csrf.py +177 -0
  77. app/core/middleware/dashboard_gzip.py +41 -0
  78. app/core/middleware/dashboard_overrides.py +59 -0
  79. app/core/middleware/firewall_cache.py +91 -0
  80. app/core/middleware/inflight.py +126 -0
  81. app/core/middleware/multipart_content_encoding.py +129 -0
  82. app/core/middleware/path_rewrite.py +148 -0
  83. app/core/middleware/request_body_limit.py +196 -0
  84. app/core/middleware/request_decompression.py +209 -0
  85. app/core/middleware/request_id.py +72 -0
  86. app/core/middleware/required_capability_http.py +116 -0
  87. app/core/middleware/trusted_proxy_headers.py +36 -0
  88. app/core/multipart.py +237 -0
  89. app/core/multipart_fields.py +91 -0
  90. app/core/openai/__init__.py +0 -0
  91. app/core/openai/chat_requests.py +493 -0
  92. app/core/openai/chat_responses.py +931 -0
  93. app/core/openai/contracts.py +46 -0
  94. app/core/openai/exceptions.py +24 -0
  95. app/core/openai/host_models.py +14 -0
  96. app/core/openai/images.py +413 -0
  97. app/core/openai/message_coercion.py +376 -0
  98. app/core/openai/model_refresh_scheduler.py +423 -0
  99. app/core/openai/model_registry.py +997 -0
  100. app/core/openai/model_registry_store.py +548 -0
  101. app/core/openai/models.py +172 -0
  102. app/core/openai/parsing.py +97 -0
  103. app/core/openai/requests.py +1980 -0
  104. app/core/openai/strict_schema.py +199 -0
  105. app/core/openai/tool_call_safety.py +99 -0
  106. app/core/openai/v1_requests.py +187 -0
  107. app/core/plan_types.py +90 -0
  108. app/core/prestop.py +202 -0
  109. app/core/rate_limiter/__init__.py +0 -0
  110. app/core/rate_limiter/db_rate_limiter.py +215 -0
  111. app/core/request_locality.py +473 -0
  112. app/core/resilience/__init__.py +0 -0
  113. app/core/resilience/backpressure.py +57 -0
  114. app/core/resilience/bulkhead.py +202 -0
  115. app/core/resilience/circuit_breaker.py +150 -0
  116. app/core/resilience/degradation.py +55 -0
  117. app/core/resilience/loop_lag_monitor.py +60 -0
  118. app/core/resilience/memory_monitor.py +137 -0
  119. app/core/resilience/network_recovery.py +256 -0
  120. app/core/resilience/overload.py +98 -0
  121. app/core/resilience/toggles.py +114 -0
  122. app/core/retention/__init__.py +0 -0
  123. app/core/retention/job.py +240 -0
  124. app/core/retention/scheduler.py +69 -0
  125. app/core/runtime_logging.py +601 -0
  126. app/core/scheduling/__init__.py +0 -0
  127. app/core/scheduling/leader_election.py +993 -0
  128. app/core/scheduling/leader_election_handle.py +25 -0
  129. app/core/server.py +140 -0
  130. app/core/shutdown.py +326 -0
  131. app/core/socket_peer.py +22 -0
  132. app/core/startup.py +41 -0
  133. app/core/timeout_invariants.py +347 -0
  134. app/core/tracing/__init__.py +0 -0
  135. app/core/tracing/otel.py +207 -0
  136. app/core/types.py +6 -0
  137. app/core/upstream_proxy/__init__.py +13 -0
  138. app/core/upstream_proxy/cache.py +130 -0
  139. app/core/upstream_proxy/resolver.py +189 -0
  140. app/core/upstream_proxy/types.py +104 -0
  141. app/core/usage/__init__.py +407 -0
  142. app/core/usage/depletion.py +109 -0
  143. app/core/usage/live_hub.py +41 -0
  144. app/core/usage/live_snapshots.py +174 -0
  145. app/core/usage/logs.py +212 -0
  146. app/core/usage/metadata_scheduler.py +118 -0
  147. app/core/usage/models.py +54 -0
  148. app/core/usage/pricing.py +576 -0
  149. app/core/usage/pricing_catalog.py +241 -0
  150. app/core/usage/pricing_snapshot.json +821 -0
  151. app/core/usage/quota.py +133 -0
  152. app/core/usage/refresh_policy.py +21 -0
  153. app/core/usage/refresh_scheduler.py +713 -0
  154. app/core/usage/reset_credits_refresh_scheduler.py +435 -0
  155. app/core/usage/types.py +168 -0
  156. app/core/utils/__init__.py +0 -0
  157. app/core/utils/json_guards.py +18 -0
  158. app/core/utils/locks.py +33 -0
  159. app/core/utils/masking.py +20 -0
  160. app/core/utils/proxy_env.py +104 -0
  161. app/core/utils/request_id.py +65 -0
  162. app/core/utils/retry.py +77 -0
  163. app/core/utils/shared_future.py +172 -0
  164. app/core/utils/sse.py +286 -0
  165. app/core/utils/time.py +24 -0
  166. app/db/__init__.py +0 -0
  167. app/db/account_identity_lock.py +56 -0
  168. app/db/alembic/__init__.py +1 -0
  169. app/db/alembic/env.py +70 -0
  170. app/db/alembic/revision_ids.py +43 -0
  171. app/db/alembic/versions/20260213_000000_base_schema.py +248 -0
  172. app/db/alembic/versions/20260213_000100_normalize_account_plan_types.py +45 -0
  173. app/db/alembic/versions/20260213_000200_add_request_logs_reasoning_effort.py +45 -0
  174. app/db/alembic/versions/20260213_000300_add_accounts_reset_at.py +45 -0
  175. app/db/alembic/versions/20260213_000400_add_accounts_chatgpt_account_id.py +45 -0
  176. app/db/alembic/versions/20260213_000500_add_dashboard_settings.py +70 -0
  177. app/db/alembic/versions/20260213_000600_add_dashboard_settings_totp.py +64 -0
  178. app/db/alembic/versions/20260213_000700_add_dashboard_settings_password.py +59 -0
  179. app/db/alembic/versions/20260213_000800_add_api_keys.py +87 -0
  180. app/db/alembic/versions/20260214_000000_add_api_key_limits.py +143 -0
  181. app/db/alembic/versions/20260215_000000_add_idx_logs_requested_at.py +38 -0
  182. app/db/alembic/versions/20260218_000000_add_api_key_usage_reservations.py +117 -0
  183. app/db/alembic/versions/20260218_000100_add_import_without_overwrite_and_drop_accounts_email_unique.py +198 -0
  184. app/db/alembic/versions/20260225_000000_add_dashboard_settings_routing_strategy.py +53 -0
  185. app/db/alembic/versions/20260228_020000_align_api_key_limit_enum_types.py +104 -0
  186. app/db/alembic/versions/20260228_030000_add_api_firewall_allowlist.py +42 -0
  187. app/db/alembic/versions/20260306_000000_add_request_logs_service_tier.py +45 -0
  188. app/db/alembic/versions/20260307_000000_add_api_key_enforcement_fields.py +56 -0
  189. app/db/alembic/versions/20260308_000000_add_sqlite_performance_indexes.py +41 -0
  190. app/db/alembic/versions/20260309_000000_add_additional_usage_history.py +71 -0
  191. app/db/alembic/versions/20260309_000000_request_logs_nullable_account_id.py +28 -0
  192. app/db/alembic/versions/20260310_000000_add_request_logs_transport.py +45 -0
  193. app/db/alembic/versions/20260310_000000_fix_postgresql_enum_value_casing.py +79 -0
  194. app/db/alembic/versions/20260310_120000_add_sticky_session_kinds_and_affinity_ttl.py +110 -0
  195. app/db/alembic/versions/20260312_000000_add_additional_usage_quota_key.py +193 -0
  196. app/db/alembic/versions/20260312_000000_split_sticky_sessions_primary_key_by_kind.py +96 -0
  197. app/db/alembic/versions/20260312_010000_merge_additional_usage_and_sticky_session_heads.py +25 -0
  198. app/db/alembic/versions/20260312_020000_merge_request_logs_transport_and_feature_heads.py +25 -0
  199. app/db/alembic/versions/20260312_020000_merge_request_logs_transport_and_sticky_heads.py +25 -0
  200. app/db/alembic/versions/20260312_120000_add_dashboard_upstream_stream_transport.py +62 -0
  201. app/db/alembic/versions/20260313_024500_merge_request_log_transport_heads.py +27 -0
  202. app/db/alembic/versions/20260319_100937_increase_prompt_cache_ttl_to_1800s.py +73 -0
  203. app/db/alembic/versions/20260319_111500_merge_prompt_cache_ttl_and_upstream_transport_heads.py +25 -0
  204. app/db/alembic/versions/20260319_183000_normalize_sqlite_account_status_casing.py +48 -0
  205. app/db/alembic/versions/20260320_000000_add_request_log_requested_actual_tiers.py +51 -0
  206. app/db/alembic/versions/20260321_190000_tighten_dashboard_db_indexes.py +64 -0
  207. app/db/alembic/versions/20260321_210000_merge_request_log_tiers_and_dashboard_index_heads.py +25 -0
  208. app/db/alembic/versions/20260325_000000_add_request_log_cost.py +193 -0
  209. app/db/alembic/versions/20260328_000000_add_rate_limit_attempts.py +48 -0
  210. app/db/alembic/versions/20260328_120000_add_request_log_ttft.py +37 -0
  211. app/db/alembic/versions/20260328_130000_add_audit_logs_table.py +45 -0
  212. app/db/alembic/versions/20260328_140000_add_scheduler_leader_table.py +39 -0
  213. app/db/alembic/versions/20260330_000000_add_cache_locality_settings.py +69 -0
  214. app/db/alembic/versions/20260330_010000_merge_scheduler_leader_and_cache_locality_heads.py +25 -0
  215. app/db/alembic/versions/20260330_020000_add_bridge_ring_members.py +46 -0
  216. app/db/alembic/versions/20260401_000000_add_api_key_enforced_service_tier.py +52 -0
  217. app/db/alembic/versions/20260401_000000_add_cache_invalidation.py +41 -0
  218. app/db/alembic/versions/20260401_020000_merge_cache_invalidation_and_api_key_service_tier_heads.py +22 -0
  219. app/db/alembic/versions/20260402_000000_switch_dashboard_routing_default_to_capacity_weighted.py +66 -0
  220. app/db/alembic/versions/20260403_000000_add_credit_api_key_limit_values.py +44 -0
  221. app/db/alembic/versions/20260406_000000_add_api_key_account_assignments.py +57 -0
  222. app/db/alembic/versions/20260406_010000_add_api_key_assignment_scope_flag.py +75 -0
  223. app/db/alembic/versions/20260407_000000_add_bridge_gateway_safe_mode.py +51 -0
  224. app/db/alembic/versions/20260407_010000_merge_api_key_assignment_and_bridge_gateway_heads.py +25 -0
  225. app/db/alembic/versions/20260408_000000_switch_import_without_overwrite_default_to_true.py +73 -0
  226. app/db/alembic/versions/20260408_010000_merge_import_without_overwrite_and_assignment_heads.py +25 -0
  227. app/db/alembic/versions/20260409_000000_add_http_bridge_sessions.py +130 -0
  228. app/db/alembic/versions/20260409_000000_switch_sticky_threads_and_prefer_earlier_reset_defaults_to_true.py +89 -0
  229. app/db/alembic/versions/20260409_010000_add_dashboard_settings_bootstrap_token.py +57 -0
  230. app/db/alembic/versions/20260409_010000_merge_http_bridge_and_import_without_overwrite_heads.py +25 -0
  231. app/db/alembic/versions/20260409_020000_fix_http_bridge_last_seen_index.py +46 -0
  232. app/db/alembic/versions/20260410_000000_backfill_pristine_dashboard_settings_defaults.py +77 -0
  233. app/db/alembic/versions/20260410_030000_restore_import_without_overwrite_default_true.py +73 -0
  234. app/db/alembic/versions/20260410_040000_merge_dashboard_defaults_and_import_default_heads.py +25 -0
  235. app/db/alembic/versions/20260413_000000_add_accounts_blocked_at.py +44 -0
  236. app/db/alembic/versions/20260415_160000_add_automations_tables.py +189 -0
  237. app/db/alembic/versions/20260415_160000_add_request_logs_response_lookup_index.py +67 -0
  238. app/db/alembic/versions/20260417_000000_add_request_log_plan_type.py +44 -0
  239. app/db/alembic/versions/20260418_190000_add_automation_runs_cycle_key.py +87 -0
  240. app/db/alembic/versions/20260419_000000_add_automation_run_cycle_metadata.py +56 -0
  241. app/db/alembic/versions/20260419_020000_add_automation_run_cycles_snapshot_tables.py +342 -0
  242. app/db/alembic/versions/20260421_000000_add_dashboard_session_ttl_setting.py +51 -0
  243. app/db/alembic/versions/20260421_120000_merge_request_log_lookup_and_plan_type_heads.py +25 -0
  244. app/db/alembic/versions/20260421_130000_merge_automation_and_request_log_heads.py +25 -0
  245. app/db/alembic/versions/20260422_103000_normalize_legacy_automation_run_cycle_keys.py +648 -0
  246. app/db/alembic/versions/20260423_120000_add_api_key_limit_reset_at_index.py +43 -0
  247. app/db/alembic/versions/20260424_000000_merge_dashboard_session_ttl_and_request_log_heads.py +25 -0
  248. app/db/alembic/versions/20260426_000000_add_dashboard_relative_availability_settings.py +68 -0
  249. app/db/alembic/versions/20260427_130000_add_warmup_model_and_request_kind.py +75 -0
  250. app/db/alembic/versions/20260506_000000_add_account_routing_policy.py +37 -0
  251. app/db/alembic/versions/20260509_000000_add_api_key_traffic_class.py +37 -0
  252. app/db/alembic/versions/20260509_010000_add_additional_quota_routing_policies.py +37 -0
  253. app/db/alembic/versions/20260513_000000_add_accounts_alias.py +44 -0
  254. app/db/alembic/versions/20260513_000000_add_api_key_apply_to_codex_model.py +59 -0
  255. app/db/alembic/versions/20260514_000000_add_request_logs_api_key_time_index.py +43 -0
  256. app/db/alembic/versions/20260515_000000_add_prefer_earlier_reset_window.py +47 -0
  257. app/db/alembic/versions/20260515_000000_soft_delete_request_logs_on_account_delete.py +83 -0
  258. app/db/alembic/versions/20260515_020000_add_split_sticky_budget_thresholds.py +62 -0
  259. app/db/alembic/versions/20260516_000000_add_sqlite_hot_path_indexes.py +75 -0
  260. app/db/alembic/versions/20260517_000000_merge_sqlite_hot_path_and_request_log_delete_heads.py +24 -0
  261. app/db/alembic/versions/20260518_000000_add_http_bridge_durable_input_prefix.py +44 -0
  262. app/db/alembic/versions/20260518_010000_merge_http_bridge_and_sqlite_recovery_heads.py +25 -0
  263. app/db/alembic/versions/20260518_120000_add_dashboard_guest_access.py +57 -0
  264. app/db/alembic/versions/20260520_000000_merge_api_key_and_http_bridge_heads.py +26 -0
  265. app/db/alembic/versions/20260520_010000_add_request_logs_api_key_account_index.py +45 -0
  266. app/db/alembic/versions/20260520_010000_merge_automation_and_api_key_heads.py +26 -0
  267. app/db/alembic/versions/20260520_030000_add_quota_planner.py +194 -0
  268. app/db/alembic/versions/20260520_040000_merge_quota_planner_and_request_log_index_heads.py +24 -0
  269. app/db/alembic/versions/20260521_000000_add_account_security_work_authorized.py +46 -0
  270. app/db/alembic/versions/20260521_000000_merge_dashboard_guest_and_api_key_heads.py +26 -0
  271. app/db/alembic/versions/20260521_000000_merge_prefer_reset_window_head.py +26 -0
  272. app/db/alembic/versions/20260521_000000_merge_warmup_and_request_log_index_heads.py +25 -0
  273. app/db/alembic/versions/20260522_000000_add_limit_warmup_trigger.py +195 -0
  274. app/db/alembic/versions/20260524_000000_merge_accounts_alias_and_reset_window_heads.py +24 -0
  275. app/db/alembic/versions/20260525_000000_add_usage_raw_window_latest_index.py +31 -0
  276. app/db/alembic/versions/20260526_000000_add_request_log_failure_metadata.py +100 -0
  277. app/db/alembic/versions/20260531_000000_add_account_workspace_identity.py +39 -0
  278. app/db/alembic/versions/20260531_000000_add_single_account_routing.py +42 -0
  279. app/db/alembic/versions/20260601_000000_merge_relative_availability_and_usage_raw_heads.py +24 -0
  280. app/db/alembic/versions/20260601_010000_add_api_key_usage_sections.py +58 -0
  281. app/db/alembic/versions/20260601_010000_add_routing_policy_persistence.py +67 -0
  282. app/db/alembic/versions/20260601_010000_merge_security_work_and_relative_availability_heads.py +25 -0
  283. app/db/alembic/versions/20260601_010000_merge_split_sticky_and_relative_usage_heads.py +25 -0
  284. app/db/alembic/versions/20260601_010000_merge_warmup_and_relative_availability_heads.py +25 -0
  285. app/db/alembic/versions/20260601_020000_merge_additional_quota_routing_and_relative_availability_heads.py +25 -0
  286. app/db/alembic/versions/20260601_020000_merge_quota_planner_and_relative_availability_heads.py +25 -0
  287. app/db/alembic/versions/20260601_020000_merge_reset_window_and_relative_availability_heads.py +25 -0
  288. app/db/alembic/versions/20260601_020000_merge_routing_policy_and_split_threshold_heads.py +24 -0
  289. app/db/alembic/versions/20260601_020000_merge_warmup_and_request_log_failure_heads.py +25 -0
  290. app/db/alembic/versions/20260601_030000_merge_single_account_and_routing_quota_heads.py +29 -0
  291. app/db/alembic/versions/20260601_040000_merge_security_work_and_routing_consolidation_heads.py +25 -0
  292. app/db/alembic/versions/20260601_050000_merge_quota_planner_and_routing_consolidation_heads.py +25 -0
  293. app/db/alembic/versions/20260602_050000_add_upstream_proxy_routing.py +164 -0
  294. app/db/alembic/versions/20260602_060000_merge_account_workspace_and_failure_heads.py +25 -0
  295. app/db/alembic/versions/20260602_070000_add_request_log_useragent_fields.py +86 -0
  296. app/db/alembic/versions/20260602_070000_merge_quota_planner_and_account_workspace_heads.py +25 -0
  297. app/db/alembic/versions/20260602_080000_merge_upstream_proxy_and_quota_planner_heads.py +25 -0
  298. app/db/alembic/versions/20260602_080000_merge_useragent_and_upstream_proxy_heads.py +24 -0
  299. app/db/alembic/versions/20260603_000000_add_weekly_pace_working_days.py +52 -0
  300. app/db/alembic/versions/20260603_000000_free_account_monthly_window.py +60 -0
  301. app/db/alembic/versions/20260604_000000_add_reauth_required_account_status.py +159 -0
  302. app/db/alembic/versions/20260604_010000_merge_automations_and_reauth_status_heads.py +25 -0
  303. app/db/alembic/versions/20260604_010000_merge_dashboard_guest_and_reauth_status_heads.py +25 -0
  304. app/db/alembic/versions/20260604_010000_merge_useragent_and_reauth_heads.py +24 -0
  305. app/db/alembic/versions/20260607_000000_merge_weekly_monthly_useragent_heads.py +28 -0
  306. app/db/alembic/versions/20260608_000000_add_hide_upstream_quota_from_api_keys.py +50 -0
  307. app/db/alembic/versions/20260611_000000_add_request_log_archive_request_id.py +47 -0
  308. app/db/alembic/versions/20260611_000000_merge_automations_and_weekly_monthly_useragent_heads.py +26 -0
  309. app/db/alembic/versions/20260611_000000_merge_dashboard_guest_and_weekly_useragent_heads.py +26 -0
  310. app/db/alembic/versions/20260611_010000_add_request_log_client_ip.py +57 -0
  311. app/db/alembic/versions/20260611_020000_merge_request_log_archive_and_client_ip_heads.py +26 -0
  312. app/db/alembic/versions/20260611_030000_merge_dashboard_guest_and_request_log_client_ip_heads.py +26 -0
  313. app/db/alembic/versions/20260613_000000_add_accounts_codex_installation_id.py +57 -0
  314. app/db/alembic/versions/20260613_010000_merge_installation_id_and_dashboard_weekly_heads.py +26 -0
  315. app/db/alembic/versions/20260615_000000_merge_automations_and_dashboard_guest_heads.py +26 -0
  316. app/db/alembic/versions/20260623_000000_add_limit_warmup_exhausted_threshold.py +51 -0
  317. app/db/alembic/versions/20260626_000000_add_http_downstream_transport_policy.py +46 -0
  318. app/db/alembic/versions/20260626_010000_add_request_logs_upstream_transport.py +31 -0
  319. app/db/alembic/versions/20260629_000000_add_dashboard_query_hot_path_indexes.py +53 -0
  320. app/db/alembic/versions/20260630_000000_add_limit_warmup_staggered_idle.py +47 -0
  321. app/db/alembic/versions/20260630_000000_merge_request_log_client_ip_and_dashboard_hot_path_heads.py +26 -0
  322. app/db/alembic/versions/20260630_010000_merge_warmup_and_request_log_dashboard_heads.py +26 -0
  323. app/db/alembic/versions/20260630_020000_merge_automations_and_main_heads.py +26 -0
  324. app/db/alembic/versions/20260630_020000_merge_warmup_threshold_and_main_heads.py +26 -0
  325. app/db/alembic/versions/20260630_030000_add_automation_run_model_snapshot.py +59 -0
  326. app/db/alembic/versions/20260630_040000_merge_automation_snapshot_and_warmup_threshold_heads.py +26 -0
  327. app/db/alembic/versions/20260630_050000_add_automation_run_prompt_snapshot.py +52 -0
  328. app/db/alembic/versions/20260701_000000_add_weekly_pace_smoothing_minutes.py +51 -0
  329. app/db/alembic/versions/20260702_000000_add_model_sources.py +176 -0
  330. app/db/alembic/versions/20260705_000000_add_model_source_audio_transcriptions.py +50 -0
  331. app/db/alembic/versions/20260705_000000_harden_dashboard_session_ttl.py +62 -0
  332. app/db/alembic/versions/20260706_000000_add_model_source_audio_per_minute.py +43 -0
  333. app/db/alembic/versions/20260707_000000_merge_model_sources_and_session_ttl.py +24 -0
  334. app/db/alembic/versions/20260707_010000_merge_automations_and_model_sources_heads.py +24 -0
  335. app/db/alembic/versions/20260707_020000_add_automation_job_account_scope.py +125 -0
  336. app/db/alembic/versions/20260709_000000_add_ttft_phase_observability.py +66 -0
  337. app/db/alembic/versions/20260711_000000_add_dashboard_prohibit_fast_mode.py +42 -0
  338. app/db/alembic/versions/20260711_010000_add_account_chatgpt_user_id.py +40 -0
  339. app/db/alembic/versions/20260711_020000_add_dashboard_account_concurrency_caps.py +71 -0
  340. app/db/alembic/versions/20260711_030000_add_limit_warmup_idle_threshold.py +51 -0
  341. app/db/alembic/versions/20260712_010000_add_account_usage_rollups.py +56 -0
  342. app/db/alembic/versions/20260712_020000_add_api_key_usage_rollups.py +53 -0
  343. app/db/alembic/versions/20260713_020000_add_model_registry_snapshot.py +42 -0
  344. app/db/alembic/versions/20260713_040000_add_account_refresh_claims.py +42 -0
  345. app/db/alembic/versions/20260713_040000_add_replica_guardrails.py +64 -0
  346. app/db/alembic/versions/20260713_070000_add_reset_credit_redeem_tables.py +58 -0
  347. app/db/alembic/versions/20260714_000000_add_oauth_flow_states.py +63 -0
  348. app/db/alembic/versions/20260715_000000_add_request_log_queue_latency.py +44 -0
  349. app/db/alembic/versions/20260716_000000_add_oauth_device_flow_slots.py +41 -0
  350. app/db/alembic/versions/20260716_010000_add_dashboard_retention_settings.py +48 -0
  351. app/db/alembic/versions/20260716_010000_add_reset_credit_display_settings.py +73 -0
  352. app/db/alembic/versions/20260717_000000_add_http_bridge_retry_circuits.py +47 -0
  353. app/db/alembic/versions/20260717_000000_merge_retention_and_reset_credit_display_heads.py +26 -0
  354. app/db/alembic/versions/20260717_000000_optimize_dashboard_hot_path_indexes.py +162 -0
  355. app/db/alembic/versions/20260717_000001_merge_retry_circuits_and_dashboard_indexes.py +22 -0
  356. app/db/alembic/versions/20260720_000000_add_request_log_conversation_id.py +57 -0
  357. app/db/alembic/versions/20260722_000000_backfill_request_log_useragent_families.py +40 -0
  358. app/db/alembic/versions/20260724_000000_add_request_usage_time_rollups.py +176 -0
  359. app/db/alembic/versions/20260724_000000_merge_request_log_schema_heads.py +33 -0
  360. app/db/alembic/versions/20260725_000000_add_http_bridge_pending_tool_calls.py +43 -0
  361. app/db/alembic/versions/20260726_000000_add_account_plan_downgrade_observations.py +44 -0
  362. app/db/alembic/versions/20260726_000000_repair_request_usage_rollups_after_merge.py +37 -0
  363. app/db/alembic/versions/20260727_000000_add_sticky_session_continuity_abandoned_at.py +45 -0
  364. app/db/alembic/versions/20260728_000000_merge_pending_tool_calls_and_rollup_repair_heads.py +31 -0
  365. app/db/alembic/versions/20260730_000000_add_api_key_fair_share_threshold.py +44 -0
  366. app/db/alembic/versions/20260730_000000_add_http_bridge_recovery_attempts.py +60 -0
  367. app/db/alembic/versions/20260731_000000_add_capability_lineage_markers.py +38 -0
  368. app/db/alembic/versions/20260803_000000_merge_http_bridge_recovery_and_capability_lineage_heads.py +30 -0
  369. app/db/alembic/versions/20260804_000000_add_http_bridge_operations.py +63 -0
  370. app/db/alembic/versions/20260804_000001_add_global_http_bridge_operation_fingerprint.py +53 -0
  371. app/db/alembic/versions/20260804_230000_add_request_log_connection_request_kind.py +43 -0
  372. app/db/alembic/versions/20260805_000000_add_http_bridge_operation_spool.py +75 -0
  373. app/db/alembic/versions/20260805_000001_finalize_http_bridge_operation_spool.py +65 -0
  374. app/db/alembic/versions/20260806_000000_add_additional_usage_alias_probe_indexes.py +84 -0
  375. app/db/alembic/versions/20260806_000000_add_anonymous_telemetry.py +49 -0
  376. app/db/alembic/versions/20260806_010000_add_conversation_presence_rollup.py +72 -0
  377. app/db/alembic/versions/20260806_020000_add_usage_history_bulk_covering_indexes.py +99 -0
  378. app/db/alembic/versions/20260806_030000_add_api_key_allowed_reasoning_efforts.py +65 -0
  379. app/db/alembic/versions/20260806_120000_add_http_bridge_owner_process_epoch.py +57 -0
  380. app/db/alembic/versions/20260807_000000_merge_http_bridge_operation_spool_and_latest_main.py +24 -0
  381. app/db/alembic/versions/20260808_000000_tune_usage_history_autovacuum.py +54 -0
  382. app/db/alembic/versions/20260810_000000_add_http_bridge_recovery_dispatch_count.py +44 -0
  383. app/db/alembic/versions/20260811_000000_add_hourly_rollup_cancelled_count.py +92 -0
  384. app/db/alembic/versions/20260812_000000_merge_recovery_dispatch_and_hourly_cancelled_heads.py +25 -0
  385. app/db/alembic/versions/20260812_120000_add_sticky_abandonment_scope.py +43 -0
  386. app/db/alembic/versions/20260813_000000_add_file_account_pins.py +40 -0
  387. app/db/alembic/versions/20260816_000000_add_account_pending_deletion.py +81 -0
  388. app/db/alembic/versions/20260816_000000_add_model_source_embeddings.py +50 -0
  389. app/db/alembic/versions/20260821_000000_add_retry_circuit_admission_generation.py +49 -0
  390. app/db/alembic/versions/20260826_000000_add_http_bridge_event_chunks.py +107 -0
  391. app/db/alembic/versions/20260828_000000_add_accounts_chatgpt_identity_index.py +55 -0
  392. app/db/alembic/versions/20260830_000000_add_quota_warmup_claim_expiry.py +83 -0
  393. app/db/alembic/versions/20260908_000000_add_guest_session_generation.py +37 -0
  394. app/db/alembic/versions/20260908_000000_add_subscription_overflow.py +140 -0
  395. app/db/alembic/versions/20260908_000000_replace_upstream_stream_transport_default_sentinel.py +50 -0
  396. app/db/alembic/versions/20260908_020000_merge_overflow_transport_heads.py +27 -0
  397. app/db/alembic/versions/20260909_000000_add_dashboard_roles.py +72 -0
  398. app/db/alembic/versions/20260909_010000_add_dashboard_users.py +183 -0
  399. app/db/alembic/versions/20260909_020000_reproject_compat_admin_credentials.py +105 -0
  400. app/db/alembic/versions/20260909_030000_add_audit_actor_columns.py +103 -0
  401. app/db/alembic/versions/20260909_030000_dashboard_resilience_toggle_settings.py +63 -0
  402. app/db/alembic/versions/20260909_040000_add_dashboard_user_invites.py +54 -0
  403. app/db/alembic/versions/20260909_040000_dashboard_timeout_settings.py +68 -0
  404. app/db/alembic/versions/20260909_050000_dashboard_routing_overload_settings.py +65 -0
  405. app/db/alembic/versions/20260909_060000_add_report_rollup.py +74 -0
  406. app/db/alembic/versions/20260909_070000_automation_run_claim_budget.py +50 -0
  407. app/db/alembic/versions/20260909_080000_dashboard_stream_bridge_budgets.py +62 -0
  408. app/db/alembic/versions/20260909_090000_dashboard_background_job_toggles.py +63 -0
  409. app/db/alembic/versions/20260909_100000_dashboard_codex_prewarm.py +49 -0
  410. app/db/alembic/versions/20260909_110000_model_context_window_overrides.py +45 -0
  411. app/db/alembic/versions/20260909_120000_dashboard_conversation_archive.py +48 -0
  412. app/db/alembic/versions/20260909_130000_add_request_logs_live_facet_indexes.py +96 -0
  413. app/db/alembic/versions/20260910_000000_add_totp_required_for_admin_role.py +44 -0
  414. app/db/alembic/versions/20260910_000000_request_logs_missing_cost_index.py +52 -0
  415. app/db/alembic/versions/20260910_010000_add_dashboard_auth_providers.py +134 -0
  416. app/db/alembic/versions/20260910_010000_dashboard_spool_retention.py +52 -0
  417. app/db/alembic/versions/20260910_020000_add_dashboard_role_mappings.py +74 -0
  418. app/db/alembic/versions/20260910_143000_request_log_affinity.py +28 -0
  419. app/db/alembic/versions/20260910_180000_merge_affinity_guest_heads.py +17 -0
  420. app/db/alembic/versions/20260910_200000_merge_affinity_identity_heads.py +17 -0
  421. app/db/alembic/versions/20260910_220000_merge_affinity_invite_heads.py +17 -0
  422. app/db/alembic/versions/20260911_000000_model_source_pins_kind_expires_index.py +43 -0
  423. app/db/alembic/versions/20260911_010000_merge_pin_index_and_affinity_heads.py +17 -0
  424. app/db/alembic/versions/20260911_020000_add_http_bridge_terminal_append_phase.py +54 -0
  425. app/db/alembic/versions/20260911_030000_add_local_login_policy.py +48 -0
  426. app/db/alembic/versions/20260911_040000_add_thread_cache_identity_mode.py +48 -0
  427. app/db/alembic/versions/20260911_060000_add_bridge_session_continuity_abandonment.py +40 -0
  428. app/db/alembic/versions/20260912_000000_merge_thread_cache_and_bridge_retirement_heads.py +22 -0
  429. app/db/alembic/versions/20260912_010000_drop_legacy_dashboard_credentials.py +140 -0
  430. app/db/alembic/versions/20260913_000000_add_oidc_provider_flow.py +134 -0
  431. app/db/alembic/versions/20260914_000000_add_scim_tokens.py +78 -0
  432. app/db/alembic/versions/20260914_000000_drop_subscription_overflow_schema.py +132 -0
  433. app/db/alembic/versions/20260918_000000_merge_scim_and_overflow_heads.py +17 -0
  434. app/db/alembic/versions/__init__.py +1 -0
  435. app/db/backup.py +64 -0
  436. app/db/migrate.py +1249 -0
  437. app/db/migration_lock.py +160 -0
  438. app/db/migration_url.py +18 -0
  439. app/db/models.py +3100 -0
  440. app/db/recover.py +244 -0
  441. app/db/session.py +1295 -0
  442. app/db/snapshot.py +46 -0
  443. app/db/sqlite_lock_retry.py +190 -0
  444. app/db/sqlite_utils.py +549 -0
  445. app/dependencies.py +462 -0
  446. app/main.py +1240 -0
  447. app/modules/__init__.py +0 -0
  448. app/modules/accounts/__init__.py +0 -0
  449. app/modules/accounts/api.py +426 -0
  450. app/modules/accounts/auth_manager.py +1439 -0
  451. app/modules/accounts/background_repository.py +160 -0
  452. app/modules/accounts/deletion.py +540 -0
  453. app/modules/accounts/mappers.py +654 -0
  454. app/modules/accounts/refresh_claims.py +352 -0
  455. app/modules/accounts/repository.py +1679 -0
  456. app/modules/accounts/schemas.py +308 -0
  457. app/modules/accounts/service.py +809 -0
  458. app/modules/accounts/usage_rollup.py +486 -0
  459. app/modules/accounts/usage_rollup_scheduler.py +75 -0
  460. app/modules/accounts/usage_time_rollup.py +1037 -0
  461. app/modules/accounts/usage_time_rollup_read.py +522 -0
  462. app/modules/api_keys/__init__.py +1 -0
  463. app/modules/api_keys/api.py +344 -0
  464. app/modules/api_keys/last_used_coalescer.py +216 -0
  465. app/modules/api_keys/limit_windows.py +41 -0
  466. app/modules/api_keys/repository.py +1200 -0
  467. app/modules/api_keys/reset_scheduler.py +85 -0
  468. app/modules/api_keys/schemas.py +135 -0
  469. app/modules/api_keys/service.py +2123 -0
  470. app/modules/audit/__init__.py +0 -0
  471. app/modules/audit/api.py +88 -0
  472. app/modules/audit/repository.py +74 -0
  473. app/modules/audit/schemas.py +33 -0
  474. app/modules/audit/service.py +71 -0
  475. app/modules/auth_providers/__init__.py +0 -0
  476. app/modules/auth_providers/api.py +127 -0
  477. app/modules/auth_providers/config.py +343 -0
  478. app/modules/auth_providers/repository.py +101 -0
  479. app/modules/auth_providers/schemas.py +88 -0
  480. app/modules/auth_providers/seed.py +105 -0
  481. app/modules/auth_providers/service.py +289 -0
  482. app/modules/automations/__init__.py +1 -0
  483. app/modules/automations/api.py +369 -0
  484. app/modules/automations/repository.py +1896 -0
  485. app/modules/automations/scheduler.py +107 -0
  486. app/modules/automations/schemas.py +158 -0
  487. app/modules/automations/service.py +2548 -0
  488. app/modules/cache_isolation_probe/__init__.py +0 -0
  489. app/modules/cache_isolation_probe/api.py +188 -0
  490. app/modules/cache_isolation_probe/prefix.py +144 -0
  491. app/modules/cache_isolation_probe/schemas.py +94 -0
  492. app/modules/cache_isolation_probe/sender.py +229 -0
  493. app/modules/cache_isolation_probe/service.py +525 -0
  494. app/modules/conversation_archive/__init__.py +1 -0
  495. app/modules/conversation_archive/api.py +101 -0
  496. app/modules/conversation_archive/schemas.py +28 -0
  497. app/modules/conversation_archive/service.py +165 -0
  498. app/modules/dashboard/__init__.py +1 -0
  499. app/modules/dashboard/api.py +106 -0
  500. app/modules/dashboard/builders.py +107 -0
  501. app/modules/dashboard/repository.py +304 -0
  502. app/modules/dashboard/schemas.py +148 -0
  503. app/modules/dashboard/service.py +534 -0
  504. app/modules/dashboard/timeframes.py +19 -0
  505. app/modules/dashboard/weekly_pace.py +617 -0
  506. app/modules/dashboard_auth/__init__.py +1 -0
  507. app/modules/dashboard_auth/api.py +1344 -0
  508. app/modules/dashboard_auth/oidc_api.py +753 -0
  509. app/modules/dashboard_auth/oidc_flows.py +330 -0
  510. app/modules/dashboard_auth/repository.py +452 -0
  511. app/modules/dashboard_auth/schemas.py +232 -0
  512. app/modules/dashboard_auth/service.py +1384 -0
  513. app/modules/dashboard_roles/__init__.py +0 -0
  514. app/modules/dashboard_roles/api.py +64 -0
  515. app/modules/dashboard_roles/repository.py +54 -0
  516. app/modules/dashboard_roles/schemas.py +29 -0
  517. app/modules/dashboard_roles/seed.py +80 -0
  518. app/modules/dashboard_roles/service.py +142 -0
  519. app/modules/dashboard_users/__init__.py +0 -0
  520. app/modules/dashboard_users/api.py +278 -0
  521. app/modules/dashboard_users/break_glass.py +169 -0
  522. app/modules/dashboard_users/credentials.py +18 -0
  523. app/modules/dashboard_users/identity_resolver.py +665 -0
  524. app/modules/dashboard_users/repository.py +725 -0
  525. app/modules/dashboard_users/schemas.py +117 -0
  526. app/modules/dashboard_users/service.py +1054 -0
  527. app/modules/firewall/__init__.py +1 -0
  528. app/modules/firewall/api.py +78 -0
  529. app/modules/firewall/repository.py +51 -0
  530. app/modules/firewall/schemas.py +28 -0
  531. app/modules/firewall/service.py +93 -0
  532. app/modules/fleet/__init__.py +1 -0
  533. app/modules/fleet/api.py +177 -0
  534. app/modules/fleet/mappers.py +56 -0
  535. app/modules/fleet/observability.py +372 -0
  536. app/modules/fleet/schemas.py +119 -0
  537. app/modules/health/__init__.py +0 -0
  538. app/modules/health/api.py +246 -0
  539. app/modules/health/schemas.py +27 -0
  540. app/modules/limit_warmup/__init__.py +1 -0
  541. app/modules/limit_warmup/repository.py +159 -0
  542. app/modules/limit_warmup/service.py +1001 -0
  543. app/modules/model_sources/__init__.py +1 -0
  544. app/modules/model_sources/api.py +99 -0
  545. app/modules/model_sources/catalog.py +307 -0
  546. app/modules/model_sources/forwarding.py +1435 -0
  547. app/modules/model_sources/projection.py +55 -0
  548. app/modules/model_sources/repository.py +201 -0
  549. app/modules/model_sources/schemas.py +79 -0
  550. app/modules/model_sources/selection.py +166 -0
  551. app/modules/model_sources/service.py +236 -0
  552. app/modules/oauth/__init__.py +0 -0
  553. app/modules/oauth/api.py +101 -0
  554. app/modules/oauth/repository.py +238 -0
  555. app/modules/oauth/schemas.py +49 -0
  556. app/modules/oauth/service.py +1196 -0
  557. app/modules/oauth/templates/oauth_success.html +122 -0
  558. app/modules/proxy/__init__.py +0 -0
  559. app/modules/proxy/_load_balancer/__init__.py +1 -0
  560. app/modules/proxy/_load_balancer/error_rate.py +116 -0
  561. app/modules/proxy/_load_balancer/latency_cohort.py +154 -0
  562. app/modules/proxy/_load_balancer/model_eligibility.py +250 -0
  563. app/modules/proxy/_load_balancer/opportunistic_admission.py +350 -0
  564. app/modules/proxy/_load_balancer/overload_backoff.py +394 -0
  565. app/modules/proxy/_load_balancer/sticky_selection.py +2417 -0
  566. app/modules/proxy/_load_balancer/throughput_cohort.py +233 -0
  567. app/modules/proxy/_load_balancer/ttft_cohort.py +214 -0
  568. app/modules/proxy/_load_balancer/tunables.py +97 -0
  569. app/modules/proxy/_load_balancer/types.py +96 -0
  570. app/modules/proxy/_load_balancer/unbound_selection.py +534 -0
  571. app/modules/proxy/_service/__init__.py +6 -0
  572. app/modules/proxy/_service/api_key_usage.py +745 -0
  573. app/modules/proxy/_service/clock_budget.py +38 -0
  574. app/modules/proxy/_service/codex_control.py +611 -0
  575. app/modules/proxy/_service/compact.py +2211 -0
  576. app/modules/proxy/_service/file_ops.py +749 -0
  577. app/modules/proxy/_service/http_bridge/__init__.py +3 -0
  578. app/modules/proxy/_service/http_bridge/accepted_replay.py +331 -0
  579. app/modules/proxy/_service/http_bridge/account_sessions.py +57 -0
  580. app/modules/proxy/_service/http_bridge/activity.py +178 -0
  581. app/modules/proxy/_service/http_bridge/helpers.py +3959 -0
  582. app/modules/proxy/_service/http_bridge/mixin.py +2435 -0
  583. app/modules/proxy/_service/http_bridge/owner_forwarding.py +609 -0
  584. app/modules/proxy/_service/http_bridge/protocol.py +167 -0
  585. app/modules/proxy/_service/http_bridge/proxy_failover.py +58 -0
  586. app/modules/proxy/_service/http_bridge/quarantine.py +443 -0
  587. app/modules/proxy/_service/http_bridge/request_submit.py +4582 -0
  588. app/modules/proxy/_service/http_bridge/retry_circuit.py +2442 -0
  589. app/modules/proxy/_service/http_bridge/service_stubs.py +492 -0
  590. app/modules/proxy/_service/http_bridge/session_registry.py +778 -0
  591. app/modules/proxy/_service/http_bridge/streaming.py +5328 -0
  592. app/modules/proxy/_service/http_bridge/upstream_events.py +4538 -0
  593. app/modules/proxy/_service/observability.py +410 -0
  594. app/modules/proxy/_service/rate_limit.py +361 -0
  595. app/modules/proxy/_service/realtime_live.py +655 -0
  596. app/modules/proxy/_service/refresh.py +154 -0
  597. app/modules/proxy/_service/request_log.py +585 -0
  598. app/modules/proxy/_service/response_create.py +930 -0
  599. app/modules/proxy/_service/streaming/__init__.py +3 -0
  600. app/modules/proxy/_service/streaming/helpers.py +1317 -0
  601. app/modules/proxy/_service/streaming/mixin.py +1100 -0
  602. app/modules/proxy/_service/streaming/protocol.py +32 -0
  603. app/modules/proxy/_service/streaming/retry.py +3762 -0
  604. app/modules/proxy/_service/support.py +2122 -0
  605. app/modules/proxy/_service/transcribe.py +434 -0
  606. app/modules/proxy/_service/warmup.py +483 -0
  607. app/modules/proxy/_service/websocket/__init__.py +3 -0
  608. app/modules/proxy/_service/websocket/helpers.py +2360 -0
  609. app/modules/proxy/_service/websocket/mixin.py +7285 -0
  610. app/modules/proxy/_service/websocket/protocol.py +74 -0
  611. app/modules/proxy/account_cache.py +229 -0
  612. app/modules/proxy/account_eligibility.py +87 -0
  613. app/modules/proxy/additional_model_limits.py +40 -0
  614. app/modules/proxy/affinity.py +830 -0
  615. app/modules/proxy/affinity_observation.py +101 -0
  616. app/modules/proxy/api.py +10254 -0
  617. app/modules/proxy/api_key_usage.py +113 -0
  618. app/modules/proxy/cap_partitioning.py +280 -0
  619. app/modules/proxy/capability_lineage.py +65 -0
  620. app/modules/proxy/capability_lineage_repository.py +82 -0
  621. app/modules/proxy/capability_routing.py +253 -0
  622. app/modules/proxy/continuity.py +112 -0
  623. app/modules/proxy/downstream_delivery.py +277 -0
  624. app/modules/proxy/durable_bridge_coordinator.py +1217 -0
  625. app/modules/proxy/durable_bridge_repository.py +4516 -0
  626. app/modules/proxy/durable_bridge_runtime.py +9 -0
  627. app/modules/proxy/durable_bridge_transcript_codec.py +122 -0
  628. app/modules/proxy/fair_share.py +131 -0
  629. app/modules/proxy/file_pin_repository.py +210 -0
  630. app/modules/proxy/helpers.py +439 -0
  631. app/modules/proxy/http_bridge_event_batcher.py +613 -0
  632. app/modules/proxy/http_bridge_forwarding.py +797 -0
  633. app/modules/proxy/http_continuation.py +76 -0
  634. app/modules/proxy/images_observability.py +62 -0
  635. app/modules/proxy/images_service.py +1054 -0
  636. app/modules/proxy/load_balancer.py +3016 -0
  637. app/modules/proxy/rate_limit_cache.py +46 -0
  638. app/modules/proxy/replay_safety.py +1063 -0
  639. app/modules/proxy/repo_bundle.py +31 -0
  640. app/modules/proxy/request_policy.py +1054 -0
  641. app/modules/proxy/ring_membership.py +251 -0
  642. app/modules/proxy/schemas.py +345 -0
  643. app/modules/proxy/selection_errors.py +41 -0
  644. app/modules/proxy/service.py +2600 -0
  645. app/modules/proxy/source_admission.py +125 -0
  646. app/modules/proxy/source_dispatch.py +999 -0
  647. app/modules/proxy/sticky_repository.py +831 -0
  648. app/modules/proxy/thread_anchors.py +491 -0
  649. app/modules/proxy/tool_call_dedupe.py +802 -0
  650. app/modules/proxy/types.py +54 -0
  651. app/modules/proxy/work_admission.py +121 -0
  652. app/modules/quota_planner/__init__.py +1 -0
  653. app/modules/quota_planner/api.py +276 -0
  654. app/modules/quota_planner/logic.py +743 -0
  655. app/modules/quota_planner/repository.py +722 -0
  656. app/modules/quota_planner/scheduler.py +212 -0
  657. app/modules/quota_planner/schemas.py +102 -0
  658. app/modules/quota_planner/warmup.py +693 -0
  659. app/modules/rate_limit_reset_credits/__init__.py +0 -0
  660. app/modules/rate_limit_reset_credits/api.py +601 -0
  661. app/modules/rate_limit_reset_credits/redeem_coordination.py +274 -0
  662. app/modules/rate_limit_reset_credits/store.py +114 -0
  663. app/modules/reports/__init__.py +1 -0
  664. app/modules/reports/api.py +134 -0
  665. app/modules/reports/cache.py +74 -0
  666. app/modules/reports/filters.py +27 -0
  667. app/modules/reports/repository.py +525 -0
  668. app/modules/reports/rollup.py +150 -0
  669. app/modules/reports/rollup_read.py +148 -0
  670. app/modules/reports/schemas.py +123 -0
  671. app/modules/reports/service.py +287 -0
  672. app/modules/reports/thread_identity.py +256 -0
  673. app/modules/request_logs/__init__.py +0 -0
  674. app/modules/request_logs/api.py +224 -0
  675. app/modules/request_logs/cost_backfill.py +154 -0
  676. app/modules/request_logs/mappers.py +97 -0
  677. app/modules/request_logs/repository.py +1765 -0
  678. app/modules/request_logs/schemas.py +145 -0
  679. app/modules/request_logs/service.py +348 -0
  680. app/modules/role_mappings/__init__.py +0 -0
  681. app/modules/role_mappings/api.py +206 -0
  682. app/modules/role_mappings/matching.py +55 -0
  683. app/modules/role_mappings/repository.py +106 -0
  684. app/modules/role_mappings/schemas.py +67 -0
  685. app/modules/role_mappings/service.py +334 -0
  686. app/modules/runtime/__init__.py +1 -0
  687. app/modules/runtime/api.py +18 -0
  688. app/modules/runtime/schemas.py +14 -0
  689. app/modules/runtime/service.py +218 -0
  690. app/modules/scim/__init__.py +1 -0
  691. app/modules/scim/api.py +194 -0
  692. app/modules/scim/dependencies.py +112 -0
  693. app/modules/scim/errors.py +77 -0
  694. app/modules/scim/management_api.py +156 -0
  695. app/modules/scim/repository.py +212 -0
  696. app/modules/scim/schemas.py +145 -0
  697. app/modules/scim/service.py +786 -0
  698. app/modules/scim/tokens.py +43 -0
  699. app/modules/settings/__init__.py +1 -0
  700. app/modules/settings/api.py +1677 -0
  701. app/modules/settings/repository.py +606 -0
  702. app/modules/settings/schemas.py +496 -0
  703. app/modules/settings/service.py +769 -0
  704. app/modules/shared/__init__.py +0 -0
  705. app/modules/shared/schemas.py +22 -0
  706. app/modules/sticky_sessions/__init__.py +1 -0
  707. app/modules/sticky_sessions/api.py +118 -0
  708. app/modules/sticky_sessions/cleanup_scheduler.py +487 -0
  709. app/modules/sticky_sessions/schemas.py +78 -0
  710. app/modules/sticky_sessions/service.py +196 -0
  711. app/modules/telemetry/__init__.py +5 -0
  712. app/modules/telemetry/api.py +115 -0
  713. app/modules/telemetry/clients.py +62 -0
  714. app/modules/telemetry/consent.py +115 -0
  715. app/modules/telemetry/scheduler.py +85 -0
  716. app/modules/telemetry/schemas.py +163 -0
  717. app/modules/telemetry/sender.py +221 -0
  718. app/modules/telemetry/snapshot.py +490 -0
  719. app/modules/usage/__init__.py +0 -0
  720. app/modules/usage/additional_quota_keys.py +379 -0
  721. app/modules/usage/api.py +45 -0
  722. app/modules/usage/background_repository.py +147 -0
  723. app/modules/usage/builders.py +559 -0
  724. app/modules/usage/depletion_service.py +277 -0
  725. app/modules/usage/live_ingest.py +356 -0
  726. app/modules/usage/mappers.py +29 -0
  727. app/modules/usage/plan_downgrade_observations.py +488 -0
  728. app/modules/usage/repository.py +1890 -0
  729. app/modules/usage/schemas.py +68 -0
  730. app/modules/usage/service.py +102 -0
  731. app/modules/usage/updater.py +1488 -0
  732. app/static/assets/Trans-Cj-J9mVA.js +1 -0
  733. app/static/assets/access-page-DHzZiXZi.js +1 -0
  734. app/static/assets/account-cost-donut-DGhfA4U8.js +1 -0
  735. app/static/assets/account-identifiers-CxbdLshZ.js +1 -0
  736. app/static/assets/account-status-DIsU5sUS.js +1 -0
  737. app/static/assets/account-trend-chart-BptlfwwQ.js +1 -0
  738. app/static/assets/accounts-page-Bbai8ift.js +3 -0
  739. app/static/assets/api-Dwa7XxPe.js +1 -0
  740. app/static/assets/api-client-dugX1Pti.js +55 -0
  741. app/static/assets/api-key-create-dialog-DnRm0oYN.js +1 -0
  742. app/static/assets/api-key-created-dialog-B6jkDG9f.js +1 -0
  743. app/static/assets/api-key-edit-dialog-Ck3Bnc3V.js +1 -0
  744. app/static/assets/api-trend-chart-BchCw7Hj.js +1 -0
  745. app/static/assets/apis-page-BvXz9c6i.js +2 -0
  746. app/static/assets/arrow-up-Dqh_KzJo.js +1 -0
  747. app/static/assets/arrow-up-down-LBRgVDGY.js +1 -0
  748. app/static/assets/automations-page-DQgNAJOR.js +3 -0
  749. app/static/assets/badge-DqMb9es0.js +1 -0
  750. app/static/assets/blur-email-HEvKagG2.js +1 -0
  751. app/static/assets/button-BjVmmiGW.js +1 -0
  752. app/static/assets/checkbox-apsl9vzk.js +1 -0
  753. app/static/assets/chevron-left-1d_pHVa5.js +1 -0
  754. app/static/assets/circle-alert-BS0xd_o1.js +1 -0
  755. app/static/assets/clsx-DB0hHKMi.js +1 -0
  756. app/static/assets/colors-BBy8GEtr.js +1 -0
  757. app/static/assets/copy-button-D7nAvOKR.js +1 -0
  758. app/static/assets/cost-per-day-chart-YdqlkZVM.js +1 -0
  759. app/static/assets/createLucideIcon-DBLzLNh2.js +1 -0
  760. app/static/assets/dashboard-page-CSdhBy0f.js +2 -0
  761. app/static/assets/distribution-metric-format-yZTy1ZEu.js +1 -0
  762. app/static/assets/donut-chart-Bxc69cra.js +1 -0
  763. app/static/assets/download-kQteIcsB.js +1 -0
  764. app/static/assets/ellipsis-B_5OgIre.js +1 -0
  765. app/static/assets/empty-state-JcA7tc8m.js +1 -0
  766. app/static/assets/es2015-bMH8YKZT.js +41 -0
  767. app/static/assets/es6-B264ni6W.js +69 -0
  768. app/static/assets/external-link-UVAOgMQ0.js +1 -0
  769. app/static/assets/i18n-BWqZ9Ua2.js +1 -0
  770. app/static/assets/inbox-B_RM2s1J.js +1 -0
  771. app/static/assets/index-DIpd264Z.js +23 -0
  772. app/static/assets/index-DMeLKRJx.css +2 -0
  773. app/static/assets/inherit-badge-QhTYVaU6.js +1 -0
  774. app/static/assets/invite-dialog-0CYh0GiQ.js +1 -0
  775. app/static/assets/iso-Bpe5Ihbb.js +1 -0
  776. app/static/assets/label-CGaLmLNf.js +1 -0
  777. app/static/assets/lazy-recharts-Pv8p9ArM.js +2 -0
  778. app/static/assets/loading-overlay-CzKQfHLI.js +1 -0
  779. app/static/assets/mini-quota-bar-cGT_t8Ae.js +1 -0
  780. app/static/assets/model-distribution-donut-BKjAfT48.js +1 -0
  781. app/static/assets/model-source-multi-select-D2d06Vc7.js +1 -0
  782. app/static/assets/multi-select-filter-KIA5qU6Z.js +1 -0
  783. app/static/assets/oauth-dialog-XeaGGDjS.js +1 -0
  784. app/static/assets/pagination-controls-D1Ypd0RU.js +1 -0
  785. app/static/assets/payload-CIYnGZz9.js +1 -0
  786. app/static/assets/queue-wait-chart-VoV7Yg9L.js +1 -0
  787. app/static/assets/report-chart-card-BXDliwcc.js +1 -0
  788. app/static/assets/reports-page-CGCJeMHG.js +3 -0
  789. app/static/assets/reset-credit-confirm-dialog-QOifTnVB.js +1 -0
  790. app/static/assets/rolldown-runtime-hePW80VL.js +1 -0
  791. app/static/assets/search-HfhqwERt.js +1 -0
  792. app/static/assets/select-Cl33bz-g.js +1 -0
  793. app/static/assets/settings-page-mZj01HYl.js +2 -0
  794. app/static/assets/shim-CZwyM4Q7.js +1 -0
  795. app/static/assets/skeleton-C_EUc4Rp.js +1 -0
  796. app/static/assets/sparkline-chart-DdNNsddM.js +1 -0
  797. app/static/assets/status-badge-CHHA5X6c.js +1 -0
  798. app/static/assets/switch-CzxgokcM.js +1 -0
  799. app/static/assets/table-DXaNSQ0G.js +1 -0
  800. app/static/assets/time-to-first-token-chart--68L0u_v.js +1 -0
  801. app/static/assets/tokens-per-day-chart-Cs82BFuE.js +1 -0
  802. app/static/assets/tokens-per-second-chart-AD2dfOn6.js +1 -0
  803. app/static/assets/totp-settings-CRHtxQ7j.js +1 -0
  804. app/static/assets/trash-D3KyWr1h.js +1 -0
  805. app/static/assets/use-account-quota-display-BPmrTBTC.js +1 -0
  806. app/static/assets/use-accounts-PWlVGw3z.js +1 -0
  807. app/static/assets/use-auth-CuDlW3iC.js +1 -0
  808. app/static/assets/use-chart-colors-DIu6tClI.js +1 -0
  809. app/static/assets/use-model-sources-Bul24JBu.js +1 -0
  810. app/static/assets/use-models-DUCPUuZg.js +1 -0
  811. app/static/assets/use-reduced-motion-hAm5r6_d.js +1 -0
  812. app/static/assets/useTranslation--56nHhlH.js +1 -0
  813. app/static/assets/useragent-distribution-donut-Dup6sf_n.js +1 -0
  814. app/static/assets/users-uIC3TIlE.js +1 -0
  815. app/static/assets/vendor-query-_bHZuxEx.js +1 -0
  816. app/static/assets/vendor-react-BVL0YZPW.js +9 -0
  817. app/static/favicon.svg +10 -0
  818. app/static/fonts/GeistSans-Variable.woff2 +0 -0
  819. app/static/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
  820. app/static/fonts/jetbrains-mono-latin.woff2 +0 -0
  821. app/static/index.html +29 -0
  822. codexlb_up-1.25.0b10.dist-info/METADATA +124 -0
  823. codexlb_up-1.25.0b10.dist-info/RECORD +827 -0
  824. codexlb_up-1.25.0b10.dist-info/WHEEL +4 -0
  825. codexlb_up-1.25.0b10.dist-info/entry_points.txt +3 -0
  826. codexlb_up-1.25.0b10.dist-info/licenses/LICENSE +21 -0
  827. config/additional_quota_registry.json +11 -0
@@ -0,0 +1,993 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import uuid
6
+ from collections.abc import Awaitable, Callable
7
+ from typing import Any, TypeVar
8
+
9
+ from sqlalchemy import Float, Result, bindparam, delete, text
10
+ from sqlalchemy.ext.asyncio import AsyncSession
11
+
12
+ from app.core.config.settings import get_settings
13
+ from app.db.models import SchedulerLeader
14
+ from app.db.session import get_background_session
15
+ from app.db.sqlite_lock_retry import is_sqlite_lock_error, sqlite_error_name
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ _T = TypeVar("_T")
20
+
21
+ _MAX_CONSECUTIVE_RENEW_ERRORS = 2
22
+
23
+ # How long a lease-loss (or shutdown) cancellation waits for the gated body to
24
+ # actually finish before detaching it. Bodies may legitimately shield in-flight
25
+ # singleton work (token/usage refresh singleflights) and drain it after
26
+ # cancellation; awaiting that unboundedly would pin ``run_if_leader`` for the
27
+ # duration of an upstream call after the lease is already gone.
28
+ _CANCEL_GRACE_SECONDS = 5.0
29
+
30
+ # How long ``release`` waits for previously detached gated bodies to finish
31
+ # before deleting the lease row. If a detached body is still draining after
32
+ # this grace the early release is skipped entirely: handing the lease to a
33
+ # follower while the old body may still act as leader would recreate the
34
+ # duplicate-singleton overlap, so the lease is left to expire after its TTL.
35
+ _RELEASE_DRAIN_GRACE_SECONDS = 5.0
36
+
37
+ # PostgreSQL evaluates the lease clock server-side so inter-replica wall-clock
38
+ # skew cannot steal a live lease. The stored expiry uses ``clock_timestamp()``
39
+ # (the actual statement-execution time) rather than ``now()`` /
40
+ # ``transaction_timestamp()`` (fixed at transaction start): a renewal or
41
+ # same-leader re-acquire that blocks on the ``scheduler_leader`` row lock must
42
+ # extend from the CURRENT time, not from a timestamp captured before it waited.
43
+ # Because the row lock serializes writers, ``clock_timestamp()`` is evaluated in
44
+ # commit order, so a slow writer that committed after a newer one can never
45
+ # write an earlier ``expires_at`` — the lease can only move forward. The
46
+ # conflict-update path recomputes ``clock_timestamp()`` in the ``DO UPDATE SET``
47
+ # clause rather than copying ``excluded.*``: the ``excluded`` row is the
48
+ # ``VALUES`` tuple, evaluated before the statement blocked on the row lock, so
49
+ # reusing it would stamp a pre-wait expiry and reintroduce the stale-clock race.
50
+ # The
51
+ # takeover predicate keeps the transaction snapshot clock (``now()``): the
52
+ # takeover decision is a single point-in-time read and staying on the snapshot
53
+ # is the conservative choice (a waiter never over-eagerly steals a lease that
54
+ # was refreshed while it was blocked on the lock).
55
+ #
56
+ # ``RETURNING`` reports the lease STILL REMAINING (``expires_at`` minus the
57
+ # database's OWN clock, ``clock_timestamp()``, in the same statement) so the
58
+ # caller can anchor its local monotonic deadline to the database's authoritative
59
+ # remaining lease rather than to a Python instant. Both the stored expiry and
60
+ # the returned ``db_now`` come from the one server clock, so the remaining is
61
+ # correct regardless of how long the statement waited on the row lock. A
62
+ # conflict no-op (another replica owns an unexpired lease) matches no row and
63
+ # RETURNS nothing, so the caller reads it as "not acquired". See ``try_acquire``
64
+ # for why anchoring a POST-statement monotonic instant to this remaining fixes
65
+ # both the backdate (a pre-statement instant seeds an already-expired deadline
66
+ # after a lock wait) and the outrun (a post-commit instant overshoots the DB
67
+ # expiry) failure modes at once.
68
+ _POSTGRES_ACQUIRE_SQL = text(
69
+ """
70
+ INSERT INTO scheduler_leader (id, leader_id, acquired_at, expires_at)
71
+ VALUES (1, :leader_id, clock_timestamp(), clock_timestamp() + make_interval(secs => :ttl))
72
+ ON CONFLICT (id) DO UPDATE SET
73
+ leader_id = excluded.leader_id,
74
+ acquired_at = clock_timestamp(),
75
+ expires_at = clock_timestamp() + make_interval(secs => :ttl)
76
+ WHERE scheduler_leader.expires_at < now() OR scheduler_leader.leader_id = :leader_id
77
+ RETURNING extract(epoch FROM (expires_at - clock_timestamp()))
78
+ """
79
+ ).bindparams(bindparam("ttl", type_=Float))
80
+
81
+ # The renewal is conditional on the lease still being unexpired at
82
+ # statement-execution time (``expires_at > clock_timestamp()``), matching the
83
+ # same clock domain as the ``SET`` expiry. Keying only on ``id`` + ``leader_id``
84
+ # is not enough: if a heartbeat is delayed past the lease deadline (event-loop
85
+ # stall, row-lock wait) the row can already have expired while no follower has
86
+ # claimed it yet, and an unconditional UPDATE would resurrect that dead lease —
87
+ # extending leader-gated work past the TTL and delaying/overlapping failover. A
88
+ # renewal that finds the lease already expired matches 0 rows, which the caller
89
+ # treats as lease loss and demotes on (see :meth:`renew`), letting the follower
90
+ # take the row cleanly. ``clock_timestamp()`` (not ``now()``) is used so a
91
+ # renewal that blocked on the row lock is judged against the CURRENT time, not
92
+ # the transaction-start snapshot captured before it waited. ``RETURNING`` yields
93
+ # the renewed lease's remaining (``expires_at`` minus the same server clock) so
94
+ # :meth:`renew` can re-anchor its local deadline to the database's authoritative
95
+ # remaining lease; a rowcount-0 no-match RETURNS nothing (read as lease loss).
96
+ _POSTGRES_RENEW_SQL = text(
97
+ """
98
+ UPDATE scheduler_leader
99
+ SET expires_at = clock_timestamp() + make_interval(secs => :ttl)
100
+ WHERE id = 1 AND leader_id = :leader_id AND expires_at > clock_timestamp()
101
+ RETURNING extract(epoch FROM (expires_at - clock_timestamp()))
102
+ """
103
+ ).bindparams(bindparam("ttl", type_=Float))
104
+
105
+
106
+ # SQLite evaluates the lease clock inside the statement using its OWN
107
+ # execution-time clock, mirroring the PostgreSQL ``clock_timestamp()`` fix
108
+ # above. A shared SQLite file implies a single host, so there is one clock
109
+ # domain — but a renewal or same-leader re-acquire can still sit behind SQLite's
110
+ # single-writer lock (``busy_timeout``, default ~30s) for far longer than the
111
+ # lease TTL (the minimum is 5s). Binding a Python ``now`` captured BEFORE
112
+ # ``session.execute`` would then evaluate the unexpired-lease guard AND the new
113
+ # ``expires_at`` against a STALE pre-wait instant once the write lock is finally
114
+ # acquired: a heartbeat that should have lost the lease could still match and
115
+ # extend the row, running leader-gated work past the real TTL and overlapping
116
+ # failover. Computing the clock in-SQL removes that gap — SQLite evaluates
117
+ # ``'now'`` once per ``sqlite3_step()`` (every ``strftime(..., 'now')`` in one
118
+ # statement shares that single instant, so the guard predicate and the ``SET``
119
+ # expiry stay in one clock domain) and only after the write lock is actually
120
+ # held, exactly like ``clock_timestamp()`` on PostgreSQL. ``strftime('%Y-%m-%d
121
+ # %H:%M:%f', 'now')`` renders a 3-digit-millisecond fractional field;
122
+ # ``|| '000'`` pads it to the 6-digit-microsecond WIDTH SQLAlchemy's
123
+ # ``DateTime`` persists, so the stored string stays lexicographically
124
+ # comparable with existing rows and with the acquire path. SQLite's ``'now'``
125
+ # is UTC, matching the naive-UTC wall clock SQLAlchemy writes for the tz-aware
126
+ # ``expires_at`` column, so no extra anchoring is needed. The takeover
127
+ # predicate necessarily shares this same execution-time clock (SQLite has no
128
+ # separate transaction-snapshot clock within a statement).
129
+ _SQLITE_NOW = "(strftime('%Y-%m-%d %H:%M:%f', 'now') || '000')"
130
+ _SQLITE_NOW_PLUS_TTL = "(strftime('%Y-%m-%d %H:%M:%f', 'now', '+' || :ttl || ' seconds') || '000')"
131
+ # Lease still remaining, in seconds, computed entirely on SQLite's own clock:
132
+ # ``expires_at`` (the string this module stores) minus ``'now'``, both evaluated
133
+ # in the SAME statement step so they share one instant. ``RETURNING`` this lets
134
+ # the caller anchor its local monotonic deadline to the database's authoritative
135
+ # remaining lease instead of a Python instant — see ``try_acquire`` — so the
136
+ # deadline neither backdates (a pre-statement Python instant seeds an
137
+ # already-expired deadline once the write lock is finally acquired after a long
138
+ # ``busy_timeout`` wait) nor outruns (a post-commit instant overshoots the row).
139
+ # ``julianday`` parses the 6-digit-microsecond-width string the acquire/renewal
140
+ # paths persist; the difference in fractional days is scaled to seconds.
141
+ _SQLITE_REMAINING = "((julianday(expires_at) - julianday('now')) * 86400.0)"
142
+
143
+ _SQLITE_ACQUIRE_SQL = text(
144
+ f"""
145
+ INSERT INTO scheduler_leader (id, leader_id, acquired_at, expires_at)
146
+ VALUES (1, :leader_id, {_SQLITE_NOW}, {_SQLITE_NOW_PLUS_TTL})
147
+ ON CONFLICT (id) DO UPDATE SET
148
+ leader_id = excluded.leader_id,
149
+ acquired_at = {_SQLITE_NOW},
150
+ expires_at = {_SQLITE_NOW_PLUS_TTL}
151
+ WHERE scheduler_leader.expires_at < {_SQLITE_NOW} OR scheduler_leader.leader_id = :leader_id
152
+ RETURNING {_SQLITE_REMAINING}
153
+ """
154
+ ).bindparams(bindparam("ttl", type_=Float))
155
+
156
+ # The SQLite renewal mirrors ``_POSTGRES_RENEW_SQL``: it extends ``expires_at``
157
+ # from the execution-time clock and is conditional on the lease still being
158
+ # unexpired at that same clock, so a heartbeat delayed past the deadline (event-
159
+ # loop stall, single-writer-lock wait) matches 0 rows instead of resurrecting a
160
+ # dead row that no follower has claimed yet. Shared by :meth:`renew` and the
161
+ # release/drain path (:meth:`_renew_lease_row`).
162
+ _SQLITE_RENEW_SQL = text(
163
+ f"""
164
+ UPDATE scheduler_leader
165
+ SET expires_at = {_SQLITE_NOW_PLUS_TTL}
166
+ WHERE id = 1 AND leader_id = :leader_id AND expires_at > {_SQLITE_NOW}
167
+ RETURNING {_SQLITE_REMAINING}
168
+ """
169
+ ).bindparams(bindparam("ttl", type_=Float))
170
+
171
+
172
+ def _dialect_name(session: AsyncSession) -> str:
173
+ return session.get_bind().dialect.name
174
+
175
+
176
+ def _returned_remaining(result: Result[Any]) -> float | None:
177
+ """Return the DB-computed remaining lease seconds, or ``None`` on no-match.
178
+
179
+ The acquire/renew statements ``RETURNING`` the affected row's remaining
180
+ lease (``expires_at`` minus the database's OWN clock, computed in the same
181
+ statement so both share one instant). A single row means the write took
182
+ effect and its value is the authoritative remaining lease the caller
183
+ anchors its local monotonic deadline to; no row means the statement matched
184
+ nothing — another replica owns the lease, or a guarded renewal found it
185
+ already expired — which the caller treats as "not acquired / lease lost".
186
+ Relying on the RETURNING row (not ``rowcount``) is also portable: SQLite's
187
+ driver reports ``rowcount`` as ``-1`` for ``RETURNING`` statements.
188
+ """
189
+ row = result.first()
190
+ if row is None:
191
+ return None
192
+ return float(row[0])
193
+
194
+
195
+ class LeaderElection:
196
+ def __init__(self, leader_id: str | None = None) -> None:
197
+ self._leader_id = leader_id or str(uuid.uuid4())
198
+ self._is_leader = False
199
+ # Monotonic (event-loop clock) estimate of when the lease we hold
200
+ # expires, set on every successful acquire/renew and cleared on
201
+ # authoritative loss. ``None`` means no lease is currently held. It is
202
+ # deliberately conservative: the database clock may grant slightly
203
+ # more, never less. A transient acquisition failure observed before
204
+ # this deadline passes must not demote an already-held lease.
205
+ self._lease_deadline: float | None = None
206
+ # Gated bodies that were detached after the cancellation grace and may
207
+ # still be draining shielded singleton work as the (former) leader.
208
+ self._detached_bodies: set[asyncio.Task[Any]] = set()
209
+ # Renewal tasks abandoned after their time-box elapsed. The heartbeat
210
+ # must not await a stalled renewal's (possibly hung) cancellation
211
+ # unwinding, so it drops the task here with a done callback that
212
+ # consumes the result. Keeping a strong reference prevents the loop
213
+ # from garbage-collecting a still-pending task mid-flight.
214
+ self._abandoned_renewals: set[asyncio.Task[bool]] = set()
215
+ # The single process-level lease-renewal keeper for the graceful-
216
+ # shutdown window. Started once at shutdown-begin (before ANY scheduler
217
+ # is stopped) and stopped by ``release``. Because schedulers are stopped
218
+ # one at a time and only the LAST one triggers ``release``, an earlier
219
+ # scheduler's ``stop()`` can detach a shielded gated body — which cancels
220
+ # that scheduler's own heartbeat — while later schedulers are still
221
+ # stopping and ``release`` has not begun its drain-renewal yet. This
222
+ # keeper owns lease renewal continuously across that whole gap so the DB
223
+ # lease can never expire while any detached/draining leader-gated body
224
+ # could still be acting as leader.
225
+ self._release_keeper: asyncio.Task[None] | None = None
226
+ self._release_keeper_stop: asyncio.Event | None = None
227
+
228
+ @property
229
+ def leader_id(self) -> str:
230
+ return self._leader_id
231
+
232
+ @property
233
+ def is_leader(self) -> bool:
234
+ return self._is_leader
235
+
236
+ async def try_acquire(self) -> bool:
237
+ settings = get_settings()
238
+ if not settings.leader_election_enabled:
239
+ self._is_leader = True
240
+ return True
241
+
242
+ ttl = settings.leader_election_ttl_seconds
243
+ loop = asyncio.get_running_loop()
244
+ try:
245
+ async with get_background_session() as session:
246
+ acquire_sql = _SQLITE_ACQUIRE_SQL if _dialect_name(session) == "sqlite" else _POSTGRES_ACQUIRE_SQL
247
+ result = await session.execute(
248
+ acquire_sql,
249
+ {"leader_id": self._leader_id, "ttl": ttl},
250
+ )
251
+ # The statement RETURNS the lease still remaining, measured on
252
+ # the database's OWN clock (``expires_at`` minus ``db_now`` in
253
+ # the same statement), or nothing when another replica owns the
254
+ # lease. Capture the monotonic instant right AFTER the statement
255
+ # returns and add the DB-remaining to it: the local deadline then
256
+ # tracks the database's authoritative expiry precisely. Anchoring
257
+ # a PRE-statement instant would backdate the deadline by however
258
+ # long the statement waited on the row / write lock (a lock wait
259
+ # can exceed a short TTL, seeding an already-expired deadline);
260
+ # anchoring after ``commit()`` would push it past the true DB
261
+ # expiry by the commit round-trip. Reading the DB-remaining after
262
+ # a post-statement instant avoids both.
263
+ remaining = _returned_remaining(result)
264
+ acquired_at = loop.time()
265
+ if remaining is None:
266
+ # No RETURNING row is an AUTHORITATIVE non-owner result:
267
+ # another replica owns an unexpired lease. Demote BEFORE
268
+ # committing — mirroring :meth:`renew`'s rowcount-0 path — so
269
+ # that a ``commit()`` failure on a flaky connection cannot
270
+ # re-raise into the generic ``except`` below, whose
271
+ # preservation branch (``_is_leader`` + unexpired deadline)
272
+ # would keep leadership and let ``run_if_leader`` run ANOTHER
273
+ # singleton body even though the database row is owned by a
274
+ # different leader. The non-owner verdict is captured before
275
+ # the commit and is authoritative regardless of whether the
276
+ # (functionally no-op) commit then succeeds. Only a genuine
277
+ # connection error observed BEFORE this authoritative row
278
+ # result may fall through to the preservation branch.
279
+ self._is_leader = False
280
+ self._lease_deadline = None
281
+ try:
282
+ await session.commit()
283
+ except Exception:
284
+ logger.warning(
285
+ "Leader lease acquisition observed a non-owner result (no row) but "
286
+ "the commit failed; treating the lease as not acquired leader_id=%s",
287
+ self._leader_id,
288
+ exc_info=True,
289
+ )
290
+ return False
291
+ await session.commit()
292
+ except Exception:
293
+ # A transient acquisition failure must not demote a lease this
294
+ # instance already holds and whose locally tracked deadline has
295
+ # not passed. The leader election is a shared singleton across
296
+ # every singleton scheduler, so one scheduler tick can call
297
+ # ``try_acquire`` while another scheduler's gated body is still
298
+ # the valid leader; clearing ``_is_leader`` here would make that
299
+ # body's next ``renew`` return ``False`` without touching the
300
+ # database and cancel otherwise-valid leader work. Demotion is
301
+ # reserved for an authoritative non-owner result (the no-row branch
302
+ # ABOVE, which returns before reaching here even if its commit fails)
303
+ # or a failure observed after the held lease has already expired.
304
+ if self._is_leader and self._lease_deadline is not None and loop.time() < self._lease_deadline:
305
+ logger.warning(
306
+ "Leader lease acquisition failed but the held lease is still valid; "
307
+ "preserving leadership leader_id=%s",
308
+ self._leader_id,
309
+ exc_info=True,
310
+ )
311
+ return True
312
+ logger.warning("Leader election failed, defaulting to non-leader", exc_info=True)
313
+ self._is_leader = False
314
+ self._lease_deadline = None
315
+ return False
316
+
317
+ # A no-row (authoritative non-owner) result already returned False above,
318
+ # so ``remaining`` is the DB-RETURNED remaining lease of a row this
319
+ # statement wrote. Anchor the local deadline to it and promote.
320
+ self._is_leader = True
321
+ self._lease_deadline = acquired_at + remaining
322
+ return True
323
+
324
+ async def renew(self) -> bool:
325
+ """Extend the held lease; demote when the lease is no longer ours.
326
+
327
+ Raises on database errors so callers can distinguish a lost lease
328
+ (returns ``False``) from a transient renewal failure.
329
+ """
330
+ if not self._is_leader:
331
+ return False
332
+
333
+ settings = get_settings()
334
+ if not settings.leader_election_enabled:
335
+ return True
336
+
337
+ ttl = settings.leader_election_ttl_seconds
338
+ loop = asyncio.get_running_loop()
339
+ async with get_background_session() as session:
340
+ # Both dialects evaluate the unexpired-lease guard and the new expiry
341
+ # on the database's own execution-time clock, so a renewal delayed
342
+ # past the deadline (event-loop stall, row/write-lock wait) matches 0
343
+ # rows instead of resurrecting a row whose ``expires_at`` has already
344
+ # passed but no follower has yet claimed. A no-match RETURNS no row
345
+ # and falls through to the lease-loss demotion below.
346
+ renew_sql = _SQLITE_RENEW_SQL if _dialect_name(session) == "sqlite" else _POSTGRES_RENEW_SQL
347
+ result = await session.execute(
348
+ renew_sql,
349
+ {"leader_id": self._leader_id, "ttl": ttl},
350
+ )
351
+ # The statement RETURNS the renewed lease's remaining (on the
352
+ # database's OWN clock) or nothing. Read it BEFORE the commit: a
353
+ # no-match means another replica now owns the lease (or the row had
354
+ # expired), an authoritative verdict independent of whether the
355
+ # (functionally no-op) commit then succeeds. Capture the monotonic
356
+ # instant right AFTER the statement returns to anchor the renewed
357
+ # deadline against the DB-remaining below.
358
+ remaining = _returned_remaining(result)
359
+ renew_anchor = loop.time()
360
+ if remaining is None:
361
+ # Demote on the authoritative lease loss BEFORE committing, so a
362
+ # ``commit()`` failure on a flaky connection cannot re-raise out
363
+ # of ``renew()`` and be misread by the heartbeat as a transient
364
+ # renewal error. A transient error would keep the gated body
365
+ # running as a believed-leader until another error or the local
366
+ # deadline; a definitive rowcount-0 loss must demote and cancel
367
+ # the body immediately.
368
+ self._is_leader = False
369
+ self._lease_deadline = None
370
+ try:
371
+ await session.commit()
372
+ except Exception:
373
+ logger.warning(
374
+ "Leader lease renewal observed lease loss (rowcount 0) but the commit "
375
+ "failed; treating the lease as lost leader_id=%s",
376
+ self._leader_id,
377
+ exc_info=True,
378
+ )
379
+ return False
380
+ await session.commit()
381
+
382
+ # Extend the locally tracked deadline so a concurrent acquire that hits a
383
+ # transient error keeps preserving leadership for the full renewed lease,
384
+ # not just the original acquisition window. Anchor the POST-statement
385
+ # monotonic instant to the DB-returned remaining so the deadline tracks
386
+ # the database's authoritative expiry — never backdated by a lock wait,
387
+ # never outrunning the row.
388
+ self._lease_deadline = renew_anchor + remaining
389
+ return True
390
+
391
+ async def release(self) -> bool:
392
+ """Delete the lease row we hold and report whether release completed.
393
+
394
+ Bodies detached after the cancellation grace may still be draining
395
+ shielded singleton work as the former leader, so the early release
396
+ first waits up to ``_RELEASE_DRAIN_GRACE_SECONDS`` for them WHILE it
397
+ keeps renewing the lease on the heartbeat cadence. A body detached on
398
+ the graceful-shutdown path is still the rightful leader, and with a
399
+ short TTL (the minimum is 5s) the DB lease would otherwise expire
400
+ during the drain wait — a follower could then acquire it and run the
401
+ same singleton work concurrently with the still-draining body. Renewing
402
+ while waiting keeps the lease alive for as long as the detached body may
403
+ still act as leader. If any body is still draining after the grace, the
404
+ row is left in place — the lease then expires after its TTL, roughly one
405
+ more TTL past the last renewal, after which the body is treated as
406
+ abandoned — because handing it to a follower while old gated work still
407
+ runs would recreate the duplicate-singleton overlap the lease exists to
408
+ prevent.
409
+
410
+ Renewal ownership is handed off from the shutdown keeper (see
411
+ :meth:`start_release_keeper`, started at shutdown-begin so the lease is
412
+ renewed continuously through the whole scheduler stop sequence). The
413
+ keeper is stopped FIRST so exactly one owner renews at a time: the keeper
414
+ renewed within the last interval, so the lease is still valid across the
415
+ synchronous handoff, and :meth:`_drain_detached_bodies` then renews on
416
+ the same cadence while it waits. Stopping the keeper before the drain
417
+ (rather than after) keeps a single renewal owner rather than two writers
418
+ racing on the row.
419
+
420
+ Failure to release must never block shutdown; the lease then simply
421
+ expires after the TTL.
422
+ """
423
+ self._is_leader = False
424
+ self._lease_deadline = None
425
+ settings = get_settings()
426
+ if not settings.leader_election_enabled:
427
+ await self._stop_release_keeper()
428
+ return True
429
+ await self._stop_release_keeper()
430
+ if not await self._drain_detached_bodies():
431
+ logger.warning(
432
+ "Skipping early leader lease release: detached leader-gated work is still "
433
+ "draining after %.1fs; the lease will expire after its TTL",
434
+ _RELEASE_DRAIN_GRACE_SECONDS,
435
+ )
436
+ return False
437
+ try:
438
+ async with get_background_session() as session:
439
+ await session.execute(
440
+ delete(SchedulerLeader).where(
441
+ SchedulerLeader.id == 1,
442
+ SchedulerLeader.leader_id == self._leader_id,
443
+ )
444
+ )
445
+ await session.commit()
446
+ except Exception as exc:
447
+ # A transient SQLite write-lock failure while deleting the row is
448
+ # not a release failure: the row is simply left in place and the
449
+ # lease expires after its TTL, exactly the fallback the caller
450
+ # already tolerates. Swallow it at DEBUG so shutdown never spams
451
+ # warnings on SQLite write contention; surface anything else as
452
+ # before. Shutdown is one-shot — there is no next release — so this
453
+ # site deliberately does NOT retry; ``sqlite_errorname`` is logged
454
+ # so an occurrence still says which mechanism held the slot.
455
+ if is_sqlite_lock_error(exc):
456
+ logger.debug(
457
+ "Leader lease release contended on a locked database; leaving the row "
458
+ "to expire after its TTL leader_id=%s sqlite_errorname=%s",
459
+ self._leader_id,
460
+ sqlite_error_name(exc),
461
+ exc_info=True,
462
+ )
463
+ return False
464
+ logger.warning("Failed to release scheduler leader lease", exc_info=True)
465
+ return False
466
+ return True
467
+
468
+ def start_release_keeper(self) -> None:
469
+ """Start the single lease-renewal keeper for the graceful-shutdown window.
470
+
471
+ Call this once at shutdown-begin, BEFORE stopping any scheduler. From
472
+ that moment until :meth:`release` stops it, one owner renews the
473
+ ``scheduler_leader`` row on the ``max(1, ttl // 3)`` cadence, closing the
474
+ cross-scheduler stop-sequence gap: schedulers are stopped one at a time
475
+ and only the last one triggers ``release``, so an earlier scheduler's
476
+ ``stop()`` can detach a shielded leader-gated body — which cancels that
477
+ scheduler's own heartbeat — while later schedulers are still stopping and
478
+ ``release`` has not begun its own drain-renewal. Without this keeper the
479
+ DB lease could expire in that window (with the minimum 5s TTL, a later
480
+ scheduler that takes >= the TTL to drain is enough) while the detached
481
+ body still runs as leader, letting a follower acquire the lease and run
482
+ the same singleton work concurrently.
483
+
484
+ Idempotent; a no-op when leader election is disabled. The keeper renews
485
+ keyed on our ``leader_id`` (a harmless rowcount-0 no-op if this process
486
+ never held the lease or a follower already took it over), so it is safe
487
+ to start unconditionally at shutdown-begin regardless of current
488
+ leadership.
489
+ """
490
+ if not get_settings().leader_election_enabled:
491
+ return
492
+ if self._release_keeper is not None and not self._release_keeper.done():
493
+ return
494
+ stop = asyncio.Event()
495
+ self._release_keeper_stop = stop
496
+ self._release_keeper = asyncio.create_task(self._run_release_keeper(stop))
497
+
498
+ async def _run_release_keeper(self, stop: asyncio.Event) -> None:
499
+ """Renew the held lease row on the ttl/3 cadence until asked to stop.
500
+
501
+ This is the SINGLE renewal owner for the shutdown window that precedes
502
+ ``release``. It renews BEFORE each wait so the lease is extended as soon
503
+ as the keeper starts, and it consults neither ``_is_leader`` nor the
504
+ locally tracked deadline: ``release`` clears both up-front, yet the row
505
+ is still ours while detached bodies drain. Renewal errors are swallowed
506
+ by :meth:`_renew_lease_row`; shutdown must always be able to proceed.
507
+ """
508
+ renew_interval = max(1, get_settings().leader_election_ttl_seconds // 3)
509
+ try:
510
+ while not stop.is_set():
511
+ await self._renew_lease_row()
512
+ try:
513
+ await asyncio.wait_for(stop.wait(), timeout=renew_interval)
514
+ except TimeoutError:
515
+ pass
516
+ except asyncio.CancelledError:
517
+ pass
518
+
519
+ async def _stop_release_keeper(self) -> None:
520
+ """Stop the shutdown lease-renewal keeper if it is running.
521
+
522
+ Signals the keeper to exit and awaits it so renewal ownership passes
523
+ cleanly to ``release``'s own bounded drain-renewal with no overlapping
524
+ renewer. Cancels as a fallback so a keeper wedged inside a database call
525
+ cannot pin shutdown; the outer release deadline (``app/main.py``'s
526
+ ``_release_leader_lease_within``) abandons the whole release if even that
527
+ stalls, so shutdown always proceeds.
528
+ """
529
+ keeper = self._release_keeper
530
+ stop = self._release_keeper_stop
531
+ self._release_keeper = None
532
+ self._release_keeper_stop = None
533
+ if keeper is None:
534
+ return
535
+ if stop is not None:
536
+ stop.set()
537
+ keeper.cancel()
538
+ try:
539
+ await keeper
540
+ except asyncio.CancelledError:
541
+ pass
542
+ except Exception:
543
+ logger.warning("Scheduler leader lease keeper failed during shutdown", exc_info=True)
544
+
545
+ async def run_if_leader(self, fn: Callable[[], Awaitable[_T]]) -> _T | None:
546
+ """Run ``fn`` only while holding the leader lease.
547
+
548
+ Heartbeats the lease every ``max(1, ttl // 3)`` seconds while the body
549
+ runs, except that each sleep is capped so the renewal it schedules
550
+ BEGINS with enough budget to FINISH before the locally tracked lease
551
+ deadline: the sleep is bounded by ``remaining - renew_timeout`` (clamped
552
+ to ``[0, renew_interval]``) so that ``sleep + renew_timeout`` lands at or
553
+ before the deadline. A lease seeded from a preserved acquire may have
554
+ less than a renew interval — even less than a whole renewal time-box —
555
+ left; sleeping the full interval (or the whole remaining) would start
556
+ ``renew()`` at/after the deadline, so its time-box would run while the
557
+ database row has already expired and the gated body would keep acting as
558
+ leader for up to ``renew_timeout`` past the DB expiry while a follower is
559
+ free to acquire the row. Each renewal attempt is time-boxed to
560
+ ``ttl / 6``, and the wait is additionally bounded so it never extends
561
+ past the local deadline: a renewal still in flight at the deadline is
562
+ abandoned and demoted rather than left running, so the gated body is
563
+ cancelled the instant the deadline is reached with no successful
564
+ renewal. Two consecutive failed attempts, or any failed attempt at/after
565
+ the locally tracked lease deadline, demote the holder no later than the
566
+ lease TTL — EXCEPT when a concurrent heartbeat on this shared
567
+ ``LeaderElection`` has advanced ``self._lease_deadline`` past this
568
+ heartbeat's local deadline: because the election is a shared singleton
569
+ across every scheduler, clearing the shared leadership on one
570
+ heartbeat's local transient errors would cancel a sibling's still-valid
571
+ leader work and make its next ``renew`` return ``False`` even though the
572
+ DB lease is freshly held. In that case this heartbeat adopts the
573
+ sibling's fresher deadline and keeps renewing; the shared flag is cleared
574
+ only on an authoritative rowcount-0 loss or a genuinely expired shared
575
+ lease. The time-box is enforced with ``asyncio.wait`` (not
576
+ ``asyncio.wait_for``) so a renewal whose cancellation cleanup itself
577
+ stalls — e.g. a blocked rollback in session teardown — cannot pin the
578
+ heartbeat past the timeout: once the timeout elapses the attempt is
579
+ abandoned and counted as an error immediately, without awaiting the
580
+ renewal coroutine's unwinding. On lease loss
581
+ the body is cancelled and awaited for at most
582
+ ``_CANCEL_GRACE_SECONDS``; a body still draining shielded work after
583
+ the grace is detached (its outcome is logged from a done callback),
584
+ so ``run_if_leader`` itself returns within the grace of the loss.
585
+
586
+ When ``run_if_leader`` is instead cancelled externally (graceful
587
+ shutdown) while the lease is still held, the heartbeat keeps renewing
588
+ the lease until the gated body has drained (bounded by the cancel
589
+ grace); the heartbeat is stopped only after the body exits, so a body
590
+ that honours cancellation slower than the remaining lease TTL cannot let
591
+ the DB lease expire while it still runs as leader. If the body still has
592
+ not exited when the cancel grace elapses it is detached (and the
593
+ heartbeat stops), but the lease is not abandoned: ``release`` keeps
594
+ renewing it on the same cadence while it waits for the detached body to
595
+ drain, so the DB lease still cannot expire while a detached body may act
596
+ as leader.
597
+
598
+ Returns the body's result, or ``None`` when this replica is not
599
+ leader or the body was cancelled due to lease loss.
600
+ """
601
+ if not await self.try_acquire():
602
+ return None
603
+
604
+ settings = get_settings()
605
+ if not settings.leader_election_enabled:
606
+ return await fn()
607
+
608
+ ttl = settings.leader_election_ttl_seconds
609
+ renew_interval = max(1, ttl // 3)
610
+ renew_timeout = max(1.0, ttl / 6)
611
+ loop = asyncio.get_running_loop()
612
+ # Local monotonic estimate of when the lease we hold expires; extended
613
+ # on every successful renewal. This is deliberately conservative: the
614
+ # database clock may grant slightly more, never less.
615
+ #
616
+ # Seed it from the instance's DB-confirmed deadline rather than a fresh
617
+ # ``loop.time() + ttl``. ``try_acquire`` may have returned ``True``
618
+ # WITHOUT extending the database ``expires_at`` — the "preserve active
619
+ # leadership on a transient acquire error" path keeps an already-held
620
+ # lease alive but performs no DB write, so it leaves ``_lease_deadline``
621
+ # at the last value a real acquire/renew confirmed. Resetting to a full
622
+ # TTL here would let the local deadline drift PAST the true DB lease
623
+ # expiry, so the heartbeat could keep believing it leads after a
624
+ # follower has legitimately taken over the row. ``_lease_deadline`` is
625
+ # only ``None`` in the disabled escape hatch (handled above), so the
626
+ # fallback is defensive only.
627
+ lease_deadline = self._lease_deadline if self._lease_deadline is not None else loop.time() + ttl
628
+ # ``ensure_future`` accepts any awaitable (``create_task`` requires a
629
+ # coroutine), wrapping it in a task so it can be cancelled on lease loss.
630
+ body_task: asyncio.Task[_T] = asyncio.ensure_future(fn())
631
+ lease_lost = False
632
+
633
+ async def _heartbeat() -> None:
634
+ nonlocal lease_deadline, lease_lost
635
+ consecutive_errors = 0
636
+ inflight: asyncio.Task[bool] | None = None
637
+ try:
638
+ while True:
639
+ # Never sleep past the locally tracked lease deadline. A
640
+ # preserved acquire (a transient acquire error that kept an
641
+ # already-held lease) seeds ``lease_deadline`` from the last
642
+ # DB-confirmed expiry, which may be less than a full
643
+ # ``renew_interval`` out; sleeping the whole interval would
644
+ # keep the gated body running after the database row has
645
+ # expired, letting a follower acquire and run the same
646
+ # singleton work concurrently. Bound each sleep (especially
647
+ # the first) by the time remaining, and demote immediately
648
+ # when the deadline has already passed rather than sleeping.
649
+ # ``run_if_leader`` is a SHARED singleton across every
650
+ # scheduler, so a concurrent heartbeat on the SAME
651
+ # ``LeaderElection`` instance may have renewed the row and
652
+ # advanced ``self._lease_deadline`` past this heartbeat's
653
+ # local view. Adopt that later shared deadline before judging
654
+ # expiry so a stale local deadline cannot demote (and cancel
655
+ # a sibling's still-valid leader work) while the DB lease is
656
+ # freshly held by the same process. ``self._lease_deadline``
657
+ # only ever advances via a real DB-confirmed acquire/renewal,
658
+ # so adopting it can never outrun the true lease.
659
+ if self._lease_deadline is not None and self._lease_deadline > lease_deadline:
660
+ lease_deadline = self._lease_deadline
661
+ remaining = lease_deadline - loop.time()
662
+ if remaining <= 0:
663
+ self._is_leader = False
664
+ self._lease_deadline = None
665
+ lease_lost = True
666
+ return
667
+ # Begin the renewal with enough budget to FINISH before the
668
+ # deadline: cap the sleep so ``sleep + renew_timeout`` lands
669
+ # at or before ``lease_deadline``. A preserved acquire can
670
+ # leave less than one renew interval — even less than a whole
671
+ # renewal time-box — on the lease; sleeping the full interval
672
+ # (or the whole remaining) would start ``renew()`` at/after
673
+ # the deadline, so its time-box would run while the database
674
+ # row has already expired and the gated body would keep
675
+ # acting as leader for up to ``renew_timeout`` past the DB
676
+ # expiry (a follower is free to acquire the row in that gap).
677
+ # When already within a time-box of the deadline the sleep is
678
+ # 0 and the renewal fires immediately.
679
+ sleep_budget = min(float(renew_interval), max(0.0, remaining - renew_timeout))
680
+ await asyncio.sleep(sleep_budget)
681
+ inflight = asyncio.ensure_future(self.renew())
682
+ # Time-box the attempt to ``renew_timeout``, but never wait
683
+ # PAST the local deadline: a renewal still in flight at the
684
+ # deadline is abandoned and demoted rather than left to keep
685
+ # the body running as leader past the lease expiry. Bounding
686
+ # the wait this way guarantees the heartbeat wakes AT the
687
+ # deadline (not a time-box later) so the gated body is
688
+ # cancelled the instant the deadline is reached with no
689
+ # successful renewal. ``asyncio.wait`` returns on the timeout
690
+ # without cancelling or awaiting ``inflight``, so a renewal
691
+ # whose cancellation cleanup itself hangs cannot pin the
692
+ # heartbeat past the time-box the way ``asyncio.wait_for``
693
+ # (which awaits the cancelled coroutine's unwinding) would.
694
+ renew_wait = min(renew_timeout, max(0.0, lease_deadline - loop.time()))
695
+ done, _ = await asyncio.wait({inflight}, timeout=renew_wait)
696
+ renew_task = inflight
697
+ inflight = None
698
+ try:
699
+ if renew_task not in done:
700
+ # Renewal stalled past its time-box. Abandon it
701
+ # without awaiting the unwind and treat the lease as
702
+ # at risk on the timeout deadline, not whenever the
703
+ # renewal finally returns.
704
+ self._abandon_renewal(renew_task)
705
+ raise TimeoutError("leader lease renewal timed out")
706
+ renewed = renew_task.result()
707
+ except Exception:
708
+ consecutive_errors += 1
709
+ logger.warning(
710
+ "Leader lease renewal errored or timed out consecutive_errors=%s",
711
+ consecutive_errors,
712
+ exc_info=True,
713
+ )
714
+ if consecutive_errors < _MAX_CONSECUTIVE_RENEW_ERRORS and loop.time() < lease_deadline:
715
+ continue
716
+ # This heartbeat lost local confidence (two consecutive
717
+ # transient renewal errors, or a failure at/after its
718
+ # local deadline). Before clearing the SHARED leadership
719
+ # state, consult the authoritative shared lease: because
720
+ # ``run_if_leader`` is a shared singleton across every
721
+ # scheduler, a concurrent heartbeat on the SAME instance
722
+ # may have renewed the row and advanced
723
+ # ``self._lease_deadline`` past this heartbeat's stale
724
+ # local deadline. Clearing ``self._is_leader`` /
725
+ # ``self._lease_deadline`` here would cancel that
726
+ # sibling's otherwise-valid leader work and make its next
727
+ # ``renew()`` return ``False`` without touching the
728
+ # database, even though the DB lease is freshly held.
729
+ # Demotion of the shared flag is authoritative only for a
730
+ # rowcount-0 loss (already cleared by ``renew`` and
731
+ # handled in the ``else`` branch) or a genuinely expired
732
+ # shared lease. When a sibling has advanced the shared
733
+ # deadline beyond this heartbeat's local view, adopt it
734
+ # and keep renewing rather than tearing down shared
735
+ # leadership; this heartbeat's own transient errors are
736
+ # not evidence the DB lease is gone.
737
+ if (
738
+ self._is_leader
739
+ and self._lease_deadline is not None
740
+ and self._lease_deadline > lease_deadline
741
+ ):
742
+ lease_deadline = self._lease_deadline
743
+ consecutive_errors = 0
744
+ continue
745
+ self._is_leader = False
746
+ self._lease_deadline = None
747
+ renewed = False
748
+ else:
749
+ consecutive_errors = 0
750
+ if renewed and self._lease_deadline is not None:
751
+ # ``renew`` anchored ``_lease_deadline`` to the lease
752
+ # the database RETURNed as remaining (its ``expires_at``
753
+ # minus the DB's own clock) from a monotonic instant
754
+ # captured AFTER its statement, so the working deadline
755
+ # tracks the DB's authoritative expiry — neither
756
+ # backdated by a lock wait nor outrunning the row.
757
+ # Adopt it rather than recomputing a pre-dispatch
758
+ # ``loop.time() + ttl`` here.
759
+ lease_deadline = self._lease_deadline
760
+ if not renewed:
761
+ lease_lost = True
762
+ return
763
+ finally:
764
+ # If the heartbeat is itself cancelled (e.g. shutdown) while a
765
+ # renewal is in flight, abandon it so it does not leak.
766
+ if inflight is not None and not inflight.done():
767
+ self._abandon_renewal(inflight)
768
+
769
+ heartbeat_task = asyncio.create_task(_heartbeat())
770
+ # Whether the lease-loss branch already cancelled (and possibly
771
+ # detached) the body. The finally block must not cancel a second time:
772
+ # a body draining shielded work sits at a plain ``await inner`` in its
773
+ # CancelledError handler, and a second ``Task.cancel()`` would cancel
774
+ # that shielded inner task through the await.
775
+ body_cancel_handled = False
776
+ try:
777
+ done, _ = await asyncio.wait({body_task, heartbeat_task}, return_when=asyncio.FIRST_COMPLETED)
778
+ if heartbeat_task in done and not lease_lost and not body_task.done():
779
+ # The heartbeat can only exit without flagging lease loss by
780
+ # crashing; without renewals leadership cannot be trusted.
781
+ logger.error(
782
+ "Leader heartbeat failed unexpectedly; demoting leader_id=%s",
783
+ self._leader_id,
784
+ exc_info=heartbeat_task.exception(),
785
+ )
786
+ self._is_leader = False
787
+ self._lease_deadline = None
788
+ lease_lost = True
789
+ if lease_lost:
790
+ logger.warning(
791
+ "Leader-gated task cancelled after lease loss leader_id=%s",
792
+ self._leader_id,
793
+ )
794
+ body_cancel_handled = True
795
+ await self._cancel_within_grace(body_task)
796
+ return None
797
+ return await body_task
798
+ finally:
799
+ # Shutdown-cancel path: ``run_if_leader`` was cancelled externally
800
+ # (e.g. a scheduler's ``stop()``) while the lease is still HELD —
801
+ # unlike the lease-loss branch above, which has already cancelled the
802
+ # body BECAUSE the lease was lost. Here the gated body may still be
803
+ # running as the rightful leader, e.g. draining a shielded token/
804
+ # usage refresh. Cancel and drain it FIRST, while the heartbeat keeps
805
+ # renewing the lease, and only stop the heartbeat once the body has
806
+ # exited (bounded by the cancel grace). Stopping renewals first — the
807
+ # old ordering — could let a body that honours cancellation slower
808
+ # than the remaining lease TTL (e.g. TTL=5s with the 5s cancel grace)
809
+ # outlive the DB lease, so a follower could acquire it and run the
810
+ # same singleton work concurrently. On the lease-loss / heartbeat-
811
+ # crash paths ``body_cancel_handled`` is already set, so the body is
812
+ # not re-cancelled here and the (already-returned) heartbeat is simply
813
+ # cancelled — the lease is genuinely gone there and MUST NOT be
814
+ # renewed.
815
+ if not body_cancel_handled and not body_task.done():
816
+ await self._cancel_within_grace(body_task)
817
+ heartbeat_task.cancel()
818
+ try:
819
+ await heartbeat_task
820
+ except asyncio.CancelledError:
821
+ pass
822
+ except Exception:
823
+ logger.exception("Leader heartbeat failed during cleanup")
824
+
825
+ async def _cancel_within_grace(self, task: asyncio.Task[Any]) -> None:
826
+ """Cancel ``task`` and await it for at most ``_CANCEL_GRACE_SECONDS``.
827
+
828
+ Gated bodies may shield in-flight singleton refreshes (e.g. the token
829
+ and usage refresh singleflights) and drain them after a cancellation
830
+ request, so this uses ``asyncio.wait`` (which does not re-cancel on
831
+ timeout) and detaches the task after the grace instead of blocking on
832
+ the shielded upstream call. A detached task keeps draining in the
833
+ background bounded by the underlying operation's own timeout; it is
834
+ tracked so ``release`` will not hand the lease over while it may still
835
+ run, and its outcome is logged from a done callback so failures are
836
+ still observed.
837
+ """
838
+ task.cancel()
839
+ _, pending = await asyncio.wait({task}, timeout=_CANCEL_GRACE_SECONDS)
840
+ if pending:
841
+ self._detached_bodies.add(task)
842
+ task.add_done_callback(self._on_detached_body_done)
843
+ logger.warning(
844
+ "Leader-gated task still draining shielded work %.1fs after cancellation; detaching",
845
+ _CANCEL_GRACE_SECONDS,
846
+ )
847
+ return
848
+ if task.cancelled():
849
+ return
850
+ exc = task.exception()
851
+ if exc is not None:
852
+ logger.warning("Leader-gated task failed while being cancelled", exc_info=exc)
853
+
854
+ def _abandon_renewal(self, task: asyncio.Task[bool]) -> None:
855
+ """Request cancellation of a stalled renewal without awaiting its unwind.
856
+
857
+ The heartbeat already observed the time-box elapse, so leadership can
858
+ be treated as at risk immediately. Cancellation cleanup of a hung
859
+ database call (a blocked rollback or driver call during session
860
+ teardown) may itself block, so the task is dropped here — tracked with
861
+ a strong reference and a done callback that consumes its result — and
862
+ left to unwind in the background rather than blocking the loop.
863
+ """
864
+ self._abandoned_renewals.add(task)
865
+ task.add_done_callback(self._on_renewal_done)
866
+ task.cancel()
867
+
868
+ def _on_renewal_done(self, task: asyncio.Task[bool]) -> None:
869
+ self._abandoned_renewals.discard(task)
870
+ if task.cancelled():
871
+ return
872
+ exc = task.exception()
873
+ if exc is not None:
874
+ logger.debug("Abandoned leader lease renewal finished with error", exc_info=exc)
875
+
876
+ def _on_detached_body_done(self, task: asyncio.Task[Any]) -> None:
877
+ self._detached_bodies.discard(task)
878
+ _log_detached_body_result(task)
879
+
880
+ async def _drain_detached_bodies(self) -> bool:
881
+ """Wait for detached gated bodies, renewing the lease while they run.
882
+
883
+ Returns ``True`` when no detached body remains running (the row is then
884
+ safe to delete). A body detached on the graceful-shutdown path is still
885
+ the rightful leader while it drains shielded singleton work, so the DB
886
+ lease MUST NOT expire under it: this renews the lease on the heartbeat
887
+ cadence (``max(1, ttl // 3)``) for as long as it waits. The wait is
888
+ bounded by ``_RELEASE_DRAIN_GRACE_SECONDS`` so shutdown always proceeds;
889
+ if a body is still draining when the grace elapses this returns
890
+ ``False`` and the caller leaves the row for the lease to expire by TTL
891
+ (the last renewal bought roughly one more TTL, after which the body is
892
+ treated as abandoned). A renewal that finds rowcount 0 — the lease was
893
+ already taken over on the lease-loss detach path — is a harmless no-op.
894
+ """
895
+ pending = {task for task in self._detached_bodies if not task.done()}
896
+ if not pending:
897
+ return True
898
+ settings = get_settings()
899
+ renew_interval = max(1, settings.leader_election_ttl_seconds // 3)
900
+ loop = asyncio.get_running_loop()
901
+ logger.info(
902
+ "Waiting up to %.1fs for %d detached leader-gated task(s), renewing the lease "
903
+ "meanwhile, before releasing it",
904
+ _RELEASE_DRAIN_GRACE_SECONDS,
905
+ len(pending),
906
+ )
907
+ deadline = loop.time() + _RELEASE_DRAIN_GRACE_SECONDS
908
+ while pending:
909
+ remaining = deadline - loop.time()
910
+ if remaining <= 0:
911
+ return False
912
+ # Renew BEFORE each wait slice so the database lease cannot expire
913
+ # while a detached body may still be acting as leader.
914
+ await self._renew_lease_row()
915
+ _, pending = await asyncio.wait(pending, timeout=min(float(renew_interval), remaining))
916
+ return True
917
+
918
+ async def _renew_lease_row(self) -> bool:
919
+ """Extend the lease row's expiry keyed on our ``leader_id``.
920
+
921
+ Unlike :meth:`renew` this consults neither the in-memory ``_is_leader``
922
+ flag nor the locally tracked deadline: it is used by ``release`` while a
923
+ body detached on the graceful-shutdown path may still be acting as
924
+ leader, at which point ``_is_leader`` has already been cleared but the
925
+ database row is still ours. Like :meth:`renew` the UPDATE is guarded on
926
+ the lease still being unexpired, so it can never resurrect a row whose
927
+ ``expires_at`` has already passed (a follower is then free to take it).
928
+ Errors are swallowed (shutdown must proceed); the caller treats a
929
+ failure — or an expired/taken-over no-match — as "lease not renewed".
930
+ """
931
+ settings = get_settings()
932
+ ttl = settings.leader_election_ttl_seconds
933
+ try:
934
+ async with get_background_session() as session:
935
+ # Guard the renewal on the lease still being unexpired at the
936
+ # database's execution-time clock so a shutdown/drain renewal can
937
+ # never resurrect a row whose ``expires_at`` has already passed.
938
+ renew_sql = _SQLITE_RENEW_SQL if _dialect_name(session) == "sqlite" else _POSTGRES_RENEW_SQL
939
+ result = await session.execute(
940
+ renew_sql,
941
+ {"leader_id": self._leader_id, "ttl": ttl},
942
+ )
943
+ # A RETURNING row means the guarded UPDATE extended the row; no
944
+ # row means it was expired/taken over (a harmless no-op here).
945
+ # The drain path holds no locally tracked deadline to re-anchor.
946
+ renewed = _returned_remaining(result) is not None
947
+ await session.commit()
948
+ return renewed
949
+ except Exception as exc:
950
+ # This renewal is best-effort: the caller (the release keeper and the
951
+ # drain loop) treats a ``False`` as "not renewed this cadence" and
952
+ # renews again on the next tick, so a transient SQLite write-lock
953
+ # failure must not raise out of the shutdown path nor spam warnings.
954
+ # Failing fast IS the retry here — an in-place retry would hold the
955
+ # cadence past its deadline — so log it at DEBUG with the driver's
956
+ # ``sqlite_errorname`` and let the next tick try; surface other
957
+ # errors as before.
958
+ if is_sqlite_lock_error(exc):
959
+ logger.debug(
960
+ "Leader lease renewal contended on a locked database; will retry next cadence "
961
+ "leader_id=%s sqlite_errorname=%s",
962
+ self._leader_id,
963
+ sqlite_error_name(exc),
964
+ exc_info=True,
965
+ )
966
+ return False
967
+ logger.warning(
968
+ "Failed to renew leader lease while draining detached bodies leader_id=%s",
969
+ self._leader_id,
970
+ exc_info=True,
971
+ )
972
+ return False
973
+
974
+
975
+ def _log_detached_body_result(task: asyncio.Task[Any]) -> None:
976
+ if task.cancelled():
977
+ logger.info("Detached leader-gated task finished cancelling after lease loss")
978
+ return
979
+ exc = task.exception()
980
+ if exc is not None:
981
+ logger.warning("Detached leader-gated task failed after lease loss", exc_info=exc)
982
+ else:
983
+ logger.info("Detached leader-gated task completed after lease loss")
984
+
985
+
986
+ _leader_election: LeaderElection | None = None
987
+
988
+
989
+ def get_leader_election() -> LeaderElection:
990
+ global _leader_election
991
+ if _leader_election is None:
992
+ _leader_election = LeaderElection()
993
+ return _leader_election