🐛 Bilibili调度新增回避策略 (#573)

* 🐛 将Bilibili的调度速度降低到60s

*  增加回避策略

*  降低轮询间隔,增加回避次数,抛出阶段随机刷新

* ♻️ 更清晰的调度逻辑实现

* 🐛 兼容3.10的NamedTuple多继承

* ♻️ 合并重复逻辑

* ♻️ ctx放入fsm

* 🐛 测试并调整逻辑

* 🐛 补全类型标注

* ♻️ 添加Condition和State.on_exit/on_enter,以实现自动状态切换

*  调整测试

* 🐛 私有化命名方法

* 🔊 调整补充日志

* 🐛 添加测试后清理

* ✏️ fix typing typo
This commit is contained in:
Azide
2024-08-04 18:40:01 +08:00
committed by GitHub
parent 9e6a35585a
commit 38f0edcd25
7 changed files with 562 additions and 238 deletions
+83 -14
View File
@@ -1,11 +1,15 @@
import random
from time import time
from datetime import datetime
from typing import TYPE_CHECKING, Any
import respx
import pytest
from loguru import logger
from nonebug.app import App
from httpx import URL, Response
from freezegun import freeze_time
from pytest_mock import MockerFixture
from nonebot.compat import model_dump, type_validate_python
from .utils import get_json
@@ -55,12 +59,15 @@ def without_dynamic(app: App):
@pytest.mark.asyncio
async def test_retry_for_352(app: App):
async def test_retry_for_352(app: App, mocker: MockerFixture):
from nonebot_bison.post import Post
from nonebot_bison.types import Target, RawPost
from nonebot_bison.platform.platform import NewMessage
from nonebot_bison.types import Target, RawPost, ApiError
from nonebot_bison.platform.bilibili.platforms import ApiCode352Error
from nonebot_bison.utils import ClientManager, ProcessContext, http_client
from nonebot_bison.platform.bilibili.platforms import MAX_352_RETRY_COUNT, ApiCode352Error, retry_for_352
from nonebot_bison.platform.bilibili.retry import RetryAddon, RetryState, _retry_fsm, retry_for_352
mocker.patch.object(random, "random", return_value=0.0) # 稳定触发RAISE阶段的随缘刷新
now = time()
raw_post_1 = {"id": 1, "text": "p1", "date": now, "tags": ["tag1"], "category": 1}
@@ -118,18 +125,26 @@ async def test_retry_for_352(app: App):
refresh_client_call_count = 0
async def get_client(self, target: Target | None):
logger.debug(f"call get_client: {target}, {datetime.now()}")
logger.debug(f"times: {self.get_client_call_count} + 1")
self.get_client_call_count += 1
return http_client()
async def get_client_for_static(self):
logger.debug(f"call get_client_for_static: {datetime.now()}")
logger.debug(f"times: {self.get_client_for_static_call_count} + 1")
self.get_client_for_static_call_count += 1
return http_client()
async def get_query_name_client(self):
logger.debug(f"call get_query_name_client: {datetime.now()}")
logger.debug(f"times: {self.get_query_name_client_call_count} + 1")
self.get_query_name_client_call_count += 1
return http_client()
async def refresh_client(self):
logger.debug(f"call refresh_client: {datetime.now()}")
logger.debug(f"times: {self.refresh_client_call_count} + 1")
self.refresh_client_call_count += 1
fakebili = MockPlatform(ProcessContext(MockClientManager()))
@@ -141,29 +156,83 @@ async def test_retry_for_352(app: App):
assert client_mgr.refresh_client_call_count == 0
# 无异常
res: list[dict[str, Any]] = await fakebili.get_sub_list(Target("1")) # type: ignore
res: list[dict[str, Any]] = await fakebili.get_sub_list(Target("t1")) # type: ignore
assert len(res) == 1
assert res[0]["id"] == 1
assert client_mgr.get_client_call_count == 1
assert client_mgr.refresh_client_call_count == 0
res = await fakebili.get_sub_list(Target("1")) # type: ignore
res = await fakebili.get_sub_list(Target("t1")) # type: ignore
assert len(res) == 2
assert res[0]["id"] == 1
assert res[1]["id"] == 2
assert client_mgr.get_client_call_count == 2
assert client_mgr.refresh_client_call_count == 0
# 有异常
addon = RetryAddon()
# 异常直到最终报错
test_state_list: list[RetryState] = [RetryState.NROMAL] + [RetryState.REFRESH] * addon.max_refresh_count
for _ in range(addon.max_backoff_count):
test_state_list += [RetryState.BACKOFF] * 2
test_state_list += [RetryState.REFRESH] * addon.max_refresh_count
test_state_list += [RetryState.RAISE] * 2
freeze_start = datetime(2024, 6, 19, 0, 0, 0, 0)
timedelta_length = addon.backoff_timedelta
fakebili.set_raise352(True)
for i in range(MAX_352_RETRY_COUNT):
res1: list[dict[str, Any]] = await fakebili.get_sub_list(Target("1")) # type: ignore
assert len(res1) == 0
assert client_mgr.get_client_call_count == 3 + i
assert client_mgr.refresh_client_call_count == i + 1
# 超过最大重试次数,抛出异常
with pytest.raises(ApiError):
await fakebili.get_sub_list(Target("1"))
for state in test_state_list:
logger.info(f"\n\nnow state should be {state}")
assert _retry_fsm.current_state == state
with freeze_time(freeze_start):
res = await fakebili.get_sub_list(Target("t1")) # type: ignore
assert not res
if state == RetryState.BACKOFF:
freeze_start += timedelta_length * (_retry_fsm.addon.backoff_count + 1) ** 2
assert client_mgr.refresh_client_call_count == 4 * 3 + 3 # refresh + raise
assert client_mgr.get_client_call_count == 2 + 4 * 3 + 3 # previous + refresh + raise
# 重置回正常状态
fakebili.set_raise352(False)
res = await fakebili.get_sub_list(Target("t1")) # type: ignore
assert res
# REFRESH阶段中途正常返回
test_state_list2 = [RetryState.NROMAL, RetryState.REFRESH, RetryState.NROMAL]
for idx, _ in enumerate(test_state_list2):
if idx == len(test_state_list2) - 1:
fakebili.set_raise352(False)
res = await fakebili.get_sub_list(Target("t1")) # type: ignore
assert res
else:
fakebili.set_raise352(True)
res = await fakebili.get_sub_list(Target("t1")) # type: ignore
assert not res
fakebili.set_raise352(False)
# BACKOFF阶段在回避时间中
test_state_list3 = [RetryState.NROMAL] + [RetryState.REFRESH] * addon.max_refresh_count + [RetryState.BACKOFF]
for idx, _ in enumerate(test_state_list3):
if idx == len(test_state_list3) - 1:
fakebili.set_raise352(False)
res = await fakebili.get_sub_list(Target("t1")) # type: ignore
assert not res
else:
fakebili.set_raise352(True)
res = await fakebili.get_sub_list(Target("t1")) # type: ignore
assert not res
# 测试重置
await _retry_fsm.reset()
await fakebili.get_sub_list(Target("t1")) # type: ignore
await _retry_fsm.reset()
@pytest.mark.asyncio