mirror of
https://github.com/discourse/discourse.git
synced 2026-08-06 13:08:40 +08:00
Adds automatic retries to the completions endpoint base so transient failures no longer surface immediately to callers. Rate limits (429) get their own backoff budget honoring Retry-After (capped at 60s), while 408/409/5xx and network errors (timeouts, connection resets, SSL errors) share a separate transient budget. Retry waits are bounded, jittered, and interruptible via the cancel manager so both synchronous and background completions stay responsive. Each retried attempt's HTTP status is recorded in the new AiApiAuditLog#retry_attempt_statuses column (network errors use 0, which is outside the HTTP range) and exposed via the serializer along with response_status for visibility into what was retried. Bedrock Converse is intentionally left on SDK-managed retries to avoid double-retrying request streams, and now records the failing status code on the audit log. Also adds a configurable service_tier param for Gemini (standard, flex, priority) surfaced through the LLM model config. --------- Co-authored-by: discourse-patch-triage[bot] <272280883+discourse-patch-triage[bot]@users.noreply.github.com>
111 lines
3 KiB
Ruby
Vendored
111 lines
3 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
# special object that can be used to cancel completions and http requests
|
|
module DiscourseAi
|
|
module Completions
|
|
class CancelManager
|
|
attr_reader :cancelled
|
|
attr_reader :callbacks
|
|
|
|
def initialize
|
|
@cancelled = false
|
|
@cancel_event = Concurrent::Event.new
|
|
@callbacks = Concurrent::Array.new
|
|
@mutex = Mutex.new
|
|
@monitor_thread = nil
|
|
end
|
|
|
|
def monitor_thread
|
|
@mutex.synchronize { @monitor_thread }
|
|
end
|
|
|
|
def start_monitor(delay: 0.5, &block)
|
|
@mutex.synchronize do
|
|
raise "Already monitoring" if @monitor_thread
|
|
raise "Expected a block" if !block
|
|
|
|
db = RailsMultisite::ConnectionManagement.current_db
|
|
@stop_monitor = false
|
|
|
|
@monitor_thread =
|
|
Thread.new do
|
|
loop do
|
|
done = false
|
|
@mutex.synchronize { done = true if @stop_monitor }
|
|
break if done
|
|
sleep delay
|
|
@mutex.synchronize { done = true if @stop_monitor }
|
|
@mutex.synchronize { done = true if cancelled? }
|
|
break if done
|
|
|
|
should_cancel = false
|
|
RailsMultisite::ConnectionManagement.with_connection(db) do
|
|
should_cancel = block.call
|
|
end
|
|
|
|
@mutex.synchronize { cancel! if should_cancel }
|
|
|
|
break if cancelled?
|
|
end
|
|
ensure
|
|
@mutex.synchronize { @monitor_thread = nil }
|
|
end
|
|
end
|
|
end
|
|
|
|
def stop_monitor
|
|
monitor_thread = nil
|
|
|
|
@mutex.synchronize { monitor_thread = @monitor_thread }
|
|
|
|
if monitor_thread
|
|
@mutex.synchronize { @stop_monitor = true }
|
|
# so we do not deadlock
|
|
monitor_thread.wakeup
|
|
monitor_thread.join(2)
|
|
# should not happen
|
|
if monitor_thread.alive?
|
|
Rails.logger.warn("DiscourseAI: CancelManager monitor thread did not stop in time")
|
|
monitor_thread.kill if monitor_thread.alive?
|
|
end
|
|
@monitor_thread = nil
|
|
end
|
|
end
|
|
|
|
def cancelled?
|
|
@cancelled
|
|
end
|
|
|
|
def add_callback(cb)
|
|
@callbacks << cb
|
|
end
|
|
|
|
def wait_for_cancel(timeout)
|
|
@cancel_event.wait(timeout)
|
|
end
|
|
|
|
def remove_callback(cb)
|
|
@callbacks.delete(cb)
|
|
end
|
|
|
|
def cancel!
|
|
@cancelled = true
|
|
@cancel_event.set
|
|
monitor_thread = @monitor_thread
|
|
if monitor_thread && monitor_thread != Thread.current
|
|
monitor_thread.wakeup
|
|
monitor_thread.join(2)
|
|
if monitor_thread.alive?
|
|
Rails.logger.warn("DiscourseAI: CancelManager monitor thread did not stop in time")
|
|
monitor_thread.kill if monitor_thread.alive?
|
|
end
|
|
end
|
|
@callbacks.each do |cb|
|
|
cb.call
|
|
rescue StandardError
|
|
# ignore cause this may have already been cancelled
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|