From e52b600aa043695cd3420a66b0f808a12051bc6f Mon Sep 17 00:00:00 2001 From: Airwave1981 <32637309+Airwave1981@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:41:24 +0100 Subject: [PATCH] fix: Add retry logic for ad publishing (3 attempts before skipping) (#774) --- src/kleinanzeigen_bot/__init__.py | 39 ++++++++++++++++--- .../resources/translations.de.yaml | 4 ++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/kleinanzeigen_bot/__init__.py b/src/kleinanzeigen_bot/__init__.py index 6a99d9f..077b2b3 100644 --- a/src/kleinanzeigen_bot/__init__.py +++ b/src/kleinanzeigen_bot/__init__.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: © Sebastian Thomschke and contributors # SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/ -import atexit, enum, json, os, re, signal, sys, textwrap # isort: skip +import atexit, asyncio, enum, json, os, re, signal, sys, textwrap # isort: skip import getopt # pylint: disable=deprecated-module import urllib.parse as urllib_parse from datetime import datetime @@ -10,6 +10,7 @@ from pathlib import Path from typing import Any, Final import certifi, colorama, nodriver # isort: skip +from nodriver.core.connection import ProtocolException from ruamel.yaml import YAML from wcmatch import glob @@ -1021,6 +1022,8 @@ class KleinanzeigenBot(WebScrapingMixin): async def publish_ads(self, ad_cfgs:list[tuple[str, Ad, dict[str, Any]]]) -> None: count = 0 + failed_count = 0 + max_retries = 3 published_ads = json.loads( (await self.web_request(f"{self.root_url}/m-meine-anzeigen-verwalten.json?sort=DEFAULT"))["content"])["ads"] @@ -1033,16 +1036,40 @@ class KleinanzeigenBot(WebScrapingMixin): continue count += 1 + success = False - await self.publish_ad(ad_file, ad_cfg, ad_cfg_orig, published_ads, AdUpdateStrategy.REPLACE) - publish_timeout = self._timeout("publishing_result") - await self.web_await(self.__check_publishing_result, timeout = publish_timeout) + # Retry loop only for publish_ad (before submission completes) + for attempt in range(1, max_retries + 1): + try: + await self.publish_ad(ad_file, ad_cfg, ad_cfg_orig, published_ads, AdUpdateStrategy.REPLACE) + success = True + break # Publish succeeded, exit retry loop + except asyncio.CancelledError: + raise # Respect task cancellation + except (TimeoutError, ProtocolException) as ex: + if attempt < max_retries: + LOG.warning(_("Attempt %s/%s failed for '%s': %s. Retrying..."), attempt, max_retries, ad_cfg.title, ex) + await self.web_sleep(2) # Wait before retry + else: + LOG.error(_("All %s attempts failed for '%s': %s. Skipping ad."), max_retries, ad_cfg.title, ex) + failed_count += 1 - if self.config.publishing.delete_old_ads == "AFTER_PUBLISH" and not self.keep_old_ads: + # Check publishing result separately (no retry - ad is already submitted) + if success: + try: + publish_timeout = self._timeout("publishing_result") + await self.web_await(self.__check_publishing_result, timeout = publish_timeout) + except TimeoutError: + LOG.warning(_(" -> Could not confirm publishing for '%s', but ad may be online"), ad_cfg.title) + + if success and self.config.publishing.delete_old_ads == "AFTER_PUBLISH" and not self.keep_old_ads: await self.delete_ad(ad_cfg, published_ads, delete_old_ads_by_title = False) LOG.info("############################################") - LOG.info("DONE: (Re-)published %s", pluralize("ad", count)) + if failed_count > 0: + LOG.info(_("DONE: (Re-)published %s (%s failed after retries)"), pluralize("ad", count - failed_count), failed_count) + else: + LOG.info(_("DONE: (Re-)published %s"), pluralize("ad", count)) LOG.info("############################################") async def publish_ad(self, ad_file:str, ad_cfg:Ad, ad_cfg_orig:dict[str, Any], published_ads:list[dict[str, Any]], diff --git a/src/kleinanzeigen_bot/resources/translations.de.yaml b/src/kleinanzeigen_bot/resources/translations.de.yaml index 3b200f0..1b1230b 100644 --- a/src/kleinanzeigen_bot/resources/translations.de.yaml +++ b/src/kleinanzeigen_bot/resources/translations.de.yaml @@ -115,6 +115,10 @@ kleinanzeigen_bot/__init__.py: publish_ads: "Processing %s/%s: '%s' from [%s]...": "Verarbeite %s/%s: '%s' von [%s]..." "Skipping because ad is reserved": "Überspringen, da Anzeige reserviert ist" + " -> Could not confirm publishing for '%s', but ad may be online": " -> Veröffentlichung für '%s' konnte nicht bestätigt werden, aber Anzeige ist möglicherweise online" + "Attempt %s/%s failed for '%s': %s. Retrying...": "Versuch %s/%s fehlgeschlagen für '%s': %s. Erneuter Versuch..." + "All %s attempts failed for '%s': %s. Skipping ad.": "Alle %s Versuche fehlgeschlagen für '%s': %s. Überspringe Anzeige." + "DONE: (Re-)published %s (%s failed after retries)": "FERTIG: %s (erneut) veröffentlicht (%s fehlgeschlagen nach Wiederholungen)" "DONE: (Re-)published %s": "FERTIG: %s (erneut) veröffentlicht" "ad": "Anzeige" apply_auto_price_reduction: