diff --git a/openedx/features/wikimedia_features/meta_translations/management/commands/sync_translated_strings_to_edx_from_meta.py b/openedx/features/wikimedia_features/meta_translations/management/commands/sync_translated_strings_to_edx_from_meta.py index c89d31795e8b..50f5d4481c92 100644 --- a/openedx/features/wikimedia_features/meta_translations/management/commands/sync_translated_strings_to_edx_from_meta.py +++ b/openedx/features/wikimedia_features/meta_translations/management/commands/sync_translated_strings_to_edx_from_meta.py @@ -1,5 +1,5 @@ """ -Django admin command to send untranslated data to Meta Wiki. +Django admin command to fetch translated data to edX from Meta. """ import os import asyncio @@ -27,7 +27,7 @@ class Command(BaseCommand): """ - This command will check and send updated block strings to meta server for translations. + This command will check and fetch updated block strings from meta server for translations. $ ./manage.py cms sync_translated_strings_to_edx_from_meta It will only show all blocks that are ready to fetched from meta. @@ -325,9 +325,11 @@ def _update_response_translations_in_db(self, data_dict, responses): ] """ self._UPDATED_TRANSLATIONS = [] + failed_count = 0 for response in responses: if not response: - continue; + failed_count += 1 + continue response_source_block = response.get('response_source_block') target_language_code = response.get('mclanguage') @@ -336,17 +338,19 @@ def _update_response_translations_in_db(self, data_dict, responses): target_language_code ) - if not response_source_block or not response_source_block or not response_source_block or not target_block_id: - log.error("Error in updating translations in db due to invalid response or data_dict.") + if not response_source_block or not target_language_code or not response_data or not target_block_id: log.error( - "Response details => response_source_block: {}, target_language_code: {}, response_data: {}".format( - response_source_block, response_source_block, response_source_block - ) + "Error in updating translations in db due to invalid response or data_dict. " + "response_source_block: %s, target_language_code: %s, target_block_id: %s, has_response_data: %s", + response_source_block, target_language_code, target_block_id, bool(response_data), ) - continue; + continue self._check_and_update_translations(response_data, target_block_id, target_language_code) + if failed_count: + log.warning("%d block(s) returned no response from Meta (rate limited or API error) and will be retried on next run.", failed_count) + def _get_tasks_to_fetch_data_from_wiki_meta(self, data_dict, meta_client, session): """ Returns list of tasks - required for Async API calls of Meta Wiki to fetch translations. diff --git a/openedx/features/wikimedia_features/meta_translations/meta_client.py b/openedx/features/wikimedia_features/meta_translations/meta_client.py index 502eaba13b09..8824d66511bb 100644 --- a/openedx/features/wikimedia_features/meta_translations/meta_client.py +++ b/openedx/features/wikimedia_features/meta_translations/meta_client.py @@ -1,6 +1,7 @@ """ Client to handle WikiMetaClient requests. """ +import asyncio import json import logging import requests @@ -9,6 +10,9 @@ from django.conf import settings from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers +_RATE_LIMIT_MAX_RETRIES = 3 +_RATE_LIMIT_BACKOFF_BASE = 5 # seconds; retry after 5s, 10s, 20s + logger = logging.getLogger(__name__) @@ -169,33 +173,52 @@ async def parse_response(self, request_params, request_data, response): except (aiohttp.ContentTypeError, ValueError, aiohttp.ClientError) as e: logger.error("Unable to extract json data from Meta response.") logger.error(f"Error type: {type(e).__name__}, Error: {e}") - error_text = await response.text() - logger.error(f"Response content: {error_text}") data = None - logger.info("For Meta request with data: {}, params: {}.".format(request_data, request_params)) + logger.info("For Meta request with params: %s, status: %s.", request_params, response.status) if data is not None and response.status in [200, 201]: if data.get('error'): logger.error("Meta API returned error code in response: %s.", json.dumps(data)) return False, data - logger.info("Meta API returned success response: %s.", json.dumps(data)) + mcgroup = request_params.get('mcgroup', '') if request_params else '' + mclanguage = request_params.get('mclanguage', '') if request_params else '' + msg_count = len(data.get('query', {}).get('messagecollection', [])) + logger.info( + "Meta API success: mcgroup=%s, language=%s, messages=%d.", + mcgroup, mclanguage, msg_count, + ) return True, data else: logger.error("Meta API return response with status code: %s.", response.status) - logger.error("Meta API return Error response: %s.", json.dumps(data)) return False, data async def handle_request(self, request_call, params=None, data=None): """ - Handles all Meta API calls. + Handles all Meta API calls. Retries on HTTP 429 with exponential backoff. """ headers = {'User-Agent': self.wikimedia_user_agent} - response = await request_call(url=self._BASE_API_END_POINT, params=params, data=data, headers=headers) - logger.info("Sending Meta request with data: {}, params: {}, headers: {}.".format(data, params, headers)) - return await self.parse_response(params, data, response) + logger.info("Sending Meta request with params: %s.", params) + for attempt in range(_RATE_LIMIT_MAX_RETRIES + 1): + response = await request_call(url=self._BASE_API_END_POINT, params=params, data=data, headers=headers) + if response.status == 429: + if attempt < _RATE_LIMIT_MAX_RETRIES: + wait = _RATE_LIMIT_BACKOFF_BASE * (2 ** attempt) + logger.warning( + "Meta API rate limited (429). Attempt %d/%d. Retrying in %ds. params=%s", + attempt + 1, _RATE_LIMIT_MAX_RETRIES, wait, params, + ) + await asyncio.sleep(wait) + continue + else: + logger.error( + "Meta API rate limited (429) after %d retries. Giving up. params=%s", + _RATE_LIMIT_MAX_RETRIES, params, + ) + return False, None + return await self.parse_response(params, data, response) async def fetch_login_token(self, session): @@ -307,3 +330,9 @@ async def sync_translations(self, mcgroup, mclanguage, session): 'mclanguage': mclanguage, 'response_data': response_data_dict } + else: + logger.error( + "Failed to fetch translations for mcgroup=%s, language=%s. Block will be retried on next run.", + mcgroup, mclanguage, + ) + return None