Просмотр исходного кода

fix(realtime-control): rollback rejected day pauses

刘立冬 2 дней назад
Родитель
Сommit
b6f10499c0

+ 61 - 33
examples/tencent_realtime_control/tencent_client.py

@@ -426,21 +426,43 @@ class TencentClient:
             {"adgroup_id": adgroup_id, "begin_date": begin_date}
             {"adgroup_id": adgroup_id, "begin_date": begin_date}
             for adgroup_id in adgroup_ids
             for adgroup_id in adgroup_ids
         ]
         ]
-        params = {
-            **self._common_params(account_id),
-            "user_token": self._user_token(account_id),
-        }
-        response = self.session.post(
-            f"{self.base_url}/adgroups/update_datetime",
-            params=params,
-            json={
-                "account_id": account_id,
-                "update_datetime_spec": specs,
-            },
-            timeout=self.timeout,
-        )
-        response.raise_for_status()
-        data = self._check(response.json(), "update_ad_begin_dates")
+        try:
+            params = {
+                **self._common_params(account_id),
+                "user_token": self._user_token(account_id),
+            }
+        except Exception as exc:
+            raise TencentWriteNotSentError(str(exc)) from exc
+        try:
+            response = self.session.post(
+                f"{self.base_url}/adgroups/update_datetime",
+                params=params,
+                json={
+                    "account_id": account_id,
+                    "update_datetime_spec": specs,
+                },
+                timeout=self.timeout,
+            )
+        except requests.RequestException as exc:
+            raise TencentWriteOutcomeUnknownError(str(exc)) from exc
+        if response.status_code == 408 or response.status_code >= 500:
+            raise TencentWriteOutcomeUnknownError(
+                f"Tencent HTTP {response.status_code}: {response.text[:500]}"
+            )
+        try:
+            response.raise_for_status()
+        except requests.HTTPError as exc:
+            raise TencentWriteRejectedError(str(exc)) from exc
+        try:
+            payload = response.json()
+        except Exception as exc:
+            raise TencentWriteOutcomeUnknownError(
+                f"Tencent returned non-JSON success response: {response.text[:500]}"
+            ) from exc
+        try:
+            data = self._check(payload, "update_ad_begin_dates")
+        except Exception as exc:
+            raise TencentWriteRejectedError(str(exc)) from exc
         item_failures = [
         item_failures = [
             item
             item
             for item in data.get("list") or []
             for item in data.get("list") or []
@@ -448,33 +470,39 @@ class TencentClient:
         ]
         ]
         failed_ids = [int(value) for value in data.get("fail_id_list") or []]
         failed_ids = [int(value) for value in data.get("fail_id_list") or []]
         if item_failures or failed_ids:
         if item_failures or failed_ids:
-            raise RuntimeError(
+            raise TencentWriteRejectedError(
                 "update_ad_begin_dates partially failed: "
                 "update_ad_begin_dates partially failed: "
                 f"items={item_failures} fail_id_list={failed_ids}"
                 f"items={item_failures} fail_id_list={failed_ids}"
             )
             )
 
 
         target_ids = set(adgroup_ids)
         target_ids = set(adgroup_ids)
-        last_actual: dict[int, str] = {}
+        last_actual: dict[str, Any] = {}
         for attempt in range(1, self.verify_attempts + 1):
         for attempt in range(1, self.verify_attempts + 1):
-            ads = {
-                int(ad.get("adgroup_id") or 0): ad
-                for ad in self.get_ads(account_id)
-                if int(ad.get("adgroup_id") or 0) in target_ids
-            }
-            last_actual = {
-                adgroup_id: str(
-                    (ads.get(adgroup_id) or {}).get("begin_date") or ""
-                )
-                for adgroup_id in adgroup_ids
-            }
-            if all(value == begin_date for value in last_actual.values()):
-                return [ads[adgroup_id] for adgroup_id in adgroup_ids]
+            try:
+                ads = {
+                    int(ad.get("adgroup_id") or 0): ad
+                    for ad in self.get_ads(account_id)
+                    if int(ad.get("adgroup_id") or 0) in target_ids
+                }
+                begin_dates = {
+                    adgroup_id: str(
+                        (ads.get(adgroup_id) or {}).get("begin_date") or ""
+                    )
+                    for adgroup_id in adgroup_ids
+                }
+                last_actual = {"begin_dates": begin_dates}
+                if all(value == begin_date for value in begin_dates.values()):
+                    return [ads[adgroup_id] for adgroup_id in adgroup_ids]
+            except Exception as exc:
+                last_actual = {"verification_error": str(exc)}
             if attempt < self.verify_attempts:
             if attempt < self.verify_attempts:
                 time.sleep(self.verify_delay_seconds)
                 time.sleep(self.verify_delay_seconds)
 
 
-        raise RuntimeError(
-            "Tencent begin_date verification failed: "
-            f"account={account_id} expected={begin_date} actual={last_actual}"
+        raise PostWriteVerificationError(
+            account_id=account_id,
+            adgroup_id=adgroup_ids[0],
+            expected={"begin_date": begin_date},
+            actual=last_actual,
         )
         )
 
 
     def get_today_ad_metrics(
     def get_today_ad_metrics(

+ 53 - 1
examples/tencent_realtime_control/test_feishu_natural_commands.py

@@ -4,7 +4,7 @@ import sys
 import unittest
 import unittest
 from datetime import datetime
 from datetime import datetime
 from pathlib import Path
 from pathlib import Path
-from unittest.mock import patch
+from unittest.mock import Mock, patch
 from zoneinfo import ZoneInfo
 from zoneinfo import ZoneInfo
 
 
 
 
@@ -31,6 +31,7 @@ from operator_commands import (
 from operator_control import _execute_pause, pause_status_summary, preview_write_command
 from operator_control import _execute_pause, pause_status_summary, preview_write_command
 from realtime_config import RealtimeControlConfig
 from realtime_config import RealtimeControlConfig
 from run_scheduler import next_wake
 from run_scheduler import next_wake
+from tencent_client import TencentClient, TencentWriteRejectedError
 from today_spend_query import query_today_spend
 from today_spend_query import query_today_spend
 
 
 
 
@@ -320,6 +321,40 @@ class DayPauseExecutionTest(unittest.TestCase):
             record_item.call_args.kwargs["target_status"],
             record_item.call_args.kwargs["target_status"],
         )
         )
 
 
+    def test_rejected_day_pause_clears_pending_pause_state(self) -> None:
+        client = self.FakeTencent()
+        now = datetime(2026, 7, 30, 12, 0, tzinfo=SHANGHAI)
+        with (
+            patch.object(
+                client,
+                "update_ad_begin_dates",
+                side_effect=TencentWriteRejectedError("rejected"),
+            ),
+            patch(
+                "operator_control.load_ad_states",
+                return_value={101: {"operator_pause_mode": None}},
+            ),
+            patch("operator_control.set_operator_pause"),
+            patch("operator_control.clear_operator_pause") as clear_pause,
+            patch("operator_control._record_item"),
+        ):
+            result = _execute_pause(
+                {"command_id": "cmd_1", "action": ACTION_DAY_PAUSE},
+                {"account_id": 10000001, "audience_name": "auto"},
+                now=now,
+                start_hour=6,
+                client=client,
+                items=[{"id": 1, "adgroup_id": 101}],
+            )
+
+        self.assertEqual((0, 1, 0), result)
+        clear_pause.assert_called_once_with(
+            10000001,
+            101,
+            action="OPERATOR_PAUSE_FAILED",
+            action_at=now,
+        )
+
     def test_scheduler_waits_for_cpm_start_not_delivery_start(self) -> None:
     def test_scheduler_waits_for_cpm_start_not_delivery_start(self) -> None:
         config = RealtimeControlConfig(start_hour=12, next_delivery_hour=6)
         config = RealtimeControlConfig(start_hour=12, next_delivery_hour=6)
         now = datetime(2026, 7, 30, 1, 0, tzinfo=SHANGHAI)
         now = datetime(2026, 7, 30, 1, 0, tzinfo=SHANGHAI)
@@ -346,5 +381,22 @@ class DayPauseExecutionTest(unittest.TestCase):
         self.assertEqual(0, summary["total"])
         self.assertEqual(0, summary["total"])
 
 
 
 
+class TencentDateUpdateClassificationTest(unittest.TestCase):
+    def test_tencent_rejection_uses_rejected_error(self) -> None:
+        client = TencentClient()
+        response = Mock(status_code=200, text='{"code": 1900001}')
+        response.json.return_value = {
+            "code": 1900001,
+            "message": "rejected",
+        }
+        with (
+            patch.object(client, "_common_params", return_value={}),
+            patch.object(client, "_user_token", return_value="token"),
+            patch.object(client.session, "post", return_value=response),
+            self.assertRaises(TencentWriteRejectedError),
+        ):
+            client.update_ad_begin_dates(10000001, [101], "2026-07-31")
+
+
 if __name__ == "__main__":
 if __name__ == "__main__":
     unittest.main()
     unittest.main()