Make question loading resilient to network failures
This commit is contained in:
@@ -7,6 +7,7 @@ const DEFAULT_QUESTION_API_BASE_URL := "https://s3q.ranaze.com/api/questions"
|
||||
const DEFAULT_RATINGS_API_URL := "https://s3q.ranaze.com/api/ratings/me"
|
||||
const DEFAULT_QUESTION_REPORTS_API_URL := "https://s3q.ranaze.com/api/question-reports"
|
||||
const RETRY_DELAYS := [2, 5, 10, 20, 30]
|
||||
const REQUEST_TIMEOUT_SECONDS := 30.0
|
||||
const IMAGE_QUESTION_SEARCH_LIMIT := 200
|
||||
const ATTEMPT_QUEUE_PATH := "user://pending_question_attempts.json"
|
||||
|
||||
@@ -65,18 +66,22 @@ func _ready() -> void:
|
||||
str(ProjectSettings.get_setting("step3/question_reports_api_url", DEFAULT_QUESTION_REPORTS_API_URL)),
|
||||
"/api/question-reports")
|
||||
request = HTTPRequest.new()
|
||||
request.timeout = REQUEST_TIMEOUT_SECONDS
|
||||
request.request_completed.connect(_on_api_questions_loaded)
|
||||
add_child(request)
|
||||
|
||||
attempt_request = HTTPRequest.new()
|
||||
attempt_request.timeout = REQUEST_TIMEOUT_SECONDS
|
||||
attempt_request.request_completed.connect(_on_first_attempt_recorded)
|
||||
add_child(attempt_request)
|
||||
|
||||
rating_request = HTTPRequest.new()
|
||||
rating_request.timeout = REQUEST_TIMEOUT_SECONDS
|
||||
rating_request.request_completed.connect(_on_ratings_loaded)
|
||||
add_child(rating_request)
|
||||
|
||||
report_request = HTTPRequest.new()
|
||||
report_request.timeout = REQUEST_TIMEOUT_SECONDS
|
||||
report_request.request_completed.connect(_on_question_report_submitted)
|
||||
add_child(report_request)
|
||||
|
||||
@@ -108,6 +113,15 @@ func load_questions_from_api(reset_retry: bool = true, excluded_question_ids: Ar
|
||||
retry_attempt = 0
|
||||
next_retry_seconds = 0
|
||||
retry_timer.stop()
|
||||
if is_loading or request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
|
||||
request.cancel_request()
|
||||
is_loading = false
|
||||
call_deferred("load_questions_from_api", false, pending_excluded_question_ids, pending_require_media)
|
||||
return
|
||||
elif is_loading or request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
|
||||
# A late timer or repeated click must not turn an active request into an
|
||||
# immediate ERR_BUSY failure.
|
||||
return
|
||||
|
||||
var should_load_ratings := load_state == "idle"
|
||||
is_loading = true
|
||||
@@ -130,18 +144,21 @@ func load_questions_from_api(reset_retry: bool = true, excluded_question_ids: Ar
|
||||
}
|
||||
var err := request.request(questions_api_url, headers, HTTPClient.METHOD_POST, JSON.stringify(payload))
|
||||
if err != OK:
|
||||
_set_load_error("Failed to request questions API.")
|
||||
_set_load_error("Unable to connect to the question service.")
|
||||
|
||||
|
||||
func _on_api_questions_loaded(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
is_loading = false
|
||||
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||
_set_load_error("Questions API request failed with status %d." % response_code)
|
||||
var message := "The connection to the question service was interrupted."
|
||||
if response_code > 0:
|
||||
message = "Questions API request failed with status %d." % response_code
|
||||
_set_load_error(message, _is_transient_question_failure(result, response_code))
|
||||
return
|
||||
|
||||
var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
|
||||
if typeof(parsed) != TYPE_ARRAY:
|
||||
_set_load_error("Questions API response was not an array.")
|
||||
_set_load_error("Questions API response was not an array.", false)
|
||||
return
|
||||
|
||||
var api_questions: Array = []
|
||||
@@ -151,7 +168,7 @@ func _on_api_questions_loaded(result: int, response_code: int, _headers: PackedS
|
||||
api_questions.append(normalized)
|
||||
|
||||
if api_questions.is_empty():
|
||||
_set_load_error("Questions API returned no usable questions.")
|
||||
_set_load_error("Questions API returned no usable questions.", false)
|
||||
return
|
||||
if pending_require_media and not _contains_question_with_media(api_questions):
|
||||
for question in api_questions:
|
||||
@@ -178,7 +195,10 @@ func retry_now() -> void:
|
||||
retry_attempt = 0
|
||||
next_retry_seconds = 0
|
||||
retry_timer.stop()
|
||||
load_questions_from_api(false, pending_excluded_question_ids)
|
||||
if is_loading or request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
|
||||
request.cancel_request()
|
||||
is_loading = false
|
||||
call_deferred("load_questions_from_api", false, pending_excluded_question_ids, pending_require_media)
|
||||
|
||||
|
||||
func load_next_random_question(excluded_question_ids: Array) -> void:
|
||||
@@ -323,15 +343,22 @@ func _set_load_error(message: String, schedule_retry: bool = true) -> void:
|
||||
is_loading = false
|
||||
last_error = message
|
||||
push_warning(message)
|
||||
questions_load_failed.emit(message)
|
||||
if schedule_retry and not AuthState.access_token.is_empty():
|
||||
_schedule_retry()
|
||||
else:
|
||||
load_state = "failed"
|
||||
next_retry_seconds = 0
|
||||
questions_load_failed.emit(message)
|
||||
questions_changed.emit()
|
||||
|
||||
|
||||
func _is_transient_question_failure(result: int, response_code: int) -> bool:
|
||||
if result != HTTPRequest.RESULT_SUCCESS:
|
||||
return true
|
||||
return response_code == 0 or response_code == 408 or response_code == 425 \
|
||||
or response_code == 429 or response_code >= 500
|
||||
|
||||
|
||||
func _schedule_retry() -> void:
|
||||
var delay_index := retry_attempt
|
||||
if delay_index >= RETRY_DELAYS.size():
|
||||
@@ -344,6 +371,9 @@ func _schedule_retry() -> void:
|
||||
|
||||
func _on_retry_timer_timeout() -> void:
|
||||
next_retry_seconds = 0
|
||||
if is_loading or request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
|
||||
retry_timer.start(1.0)
|
||||
return
|
||||
load_questions_from_api(false)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user