-
-
Notifications
You must be signed in to change notification settings - Fork 11.3k
增强域名重试机制 (#472) #474
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
增强域名重试机制 (#472) #474
Changes from 6 commits
6cf0d74
32059dd
a1b9597
60929f9
3355c33
d39cc8b
e695d72
650d271
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # .github/release.yml | ||
|
|
||
| changelog: | ||
| exclude: | ||
| labels: | ||
| - ignore-for-release | ||
| authors: | ||
| - octocat | ||
| categories: | ||
| - title: 🏕 Features | ||
| labels: | ||
| - '*' | ||
| exclude: | ||
| labels: | ||
| - dependencies | ||
| - title: 👒 Dependencies | ||
| labels: | ||
| - dependencies | ||
| - title: Other Changes | ||
| labels: | ||
| - "*" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -101,6 +101,17 @@ def __init__(self, resp, ts: str): | |
| super().__init__(resp) | ||
| self.ts = ts | ||
|
|
||
| # 重写json()方法,专门处理API响应的清理 | ||
| @field_cache() | ||
| def json(self) -> Dict: | ||
| from .jm_toolkit import safe_parse_json # 导入清理函数 | ||
| try: | ||
| # 仅对API响应进行清理(保留原始JmJsonResp的逻辑不变) | ||
| clean_text = safe_parse_json(self.resp.text) | ||
| return clean_text | ||
| except Exception as e: | ||
| ExceptionTool.raises_resp(f'API json解析失败: {e}', self, JsonResolveFailException) | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix ImportError and make JSON parsing robust (pipeline red).
Apply this diff: - # 重写json()方法,专门处理API响应的清理
+ # 重写json()方法, 专门处理API响应的清理
@field_cache()
def json(self) -> Dict:
- from .jm_toolkit import safe_parse_json # 导入清理函数
- try:
- # 仅对API响应进行清理(保留原始JmJsonResp的逻辑不变)
- clean_text = safe_parse_json(self.resp.text)
- return clean_text
- except Exception as e:
- ExceptionTool.raises_resp(f'API json解析失败: {e}', self, JsonResolveFailException)
+ # 延迟导入, 若不存在则优雅降级到 resp.json()
+ try:
+ from .jm_toolkit import safe_parse_json # type: ignore
+ except Exception:
+ safe_parse_json = None # type: ignore[assignment]
+
+ try:
+ # 仅对API响应进行清理 (保留原始 JmJsonResp 的逻辑不变)
+ text = self.resp.text
+ data = safe_parse_json(text) if callable(safe_parse_json) else self.resp.json()
+ if not isinstance(data, dict):
+ raise TypeError(f"Expected dict, got {type(data).__name__}")
+ return data
+ except (ValueError, TypeError) as e:
+ ExceptionTool.raises_resp(f'API json解析失败: {e}', self, JsonResolveFailException)Add this import at the top of the file to silence F405 on Dict: from typing import Dict🧰 Tools🪛 Ruff (0.12.2)104-104: Comment contains ambiguous (RUF003) 105-105: (F405) 106-106: (F405) 109-109: Comment contains ambiguous (RUF003) 109-109: Comment contains ambiguous (RUF003) 111-111: Consider moving this statement to an (TRY300) 112-112: Do not catch blind exception: (BLE001) 113-113: (F405) 113-113: (F405) 🪛 GitHub Actions: Run Test (API)[error] 107-107: Command 'python -m unittest' failed due to ImportError: cannot import name 'safe_parse_json' from 'jmcomic.jm_toolkit' (/home/runner/work/JMComic-Crawler-Python/JMComic-Crawler-Python/src/jmcomic/jm_toolkit.py). |
||
| @property | ||
| def is_success(self) -> bool: | ||
| return super().is_success and self.json()['code'] == 200 | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1221,23 +1221,88 @@ def new_decide_dir(photo, ensure_exists=True) -> str: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class AdvancedRetryPlugin(JmOptionPlugin): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| plugin_key = 'advanced-retry' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def __init__(self, option: JmOption): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| super().__init__(option) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.retry_config = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def invoke(self, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| retry_config, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| **kwargs): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.require_param(isinstance(retry_config, dict), '必须配置retry_config为dict') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.retry_config = retry_config | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1231
to
+1233
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Validate required keys and normalize types in Fail fast if keys are missing; coerce to int to avoid surprises later. Apply this diff: - self.require_param(isinstance(retry_config, dict), '必须配置retry_config为dict')
- self.retry_config = retry_config
+ self.require_param(isinstance(retry_config, dict), '必须配置retry_config为dict')
+ required = ('retry_rounds', 'retry_domain_max_times')
+ missing = [k for k in required if k not in retry_config]
+ self.require_param(not missing, f'缺少配置项: {missing}')
+ retry_config['retry_rounds'] = int(retry_config['retry_rounds'])
+ retry_config['retry_domain_max_times'] = int(retry_config['retry_domain_max_times'])
+ self.retry_config = retry_config📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| new_jm_client: Callable = self.option.new_jm_client | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def hook_new_jm_client(*args, **kwargs): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| client: AbstractJmClient = new_jm_client(*args, **kwargs) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| client: JmcomicClient = new_jm_client(*args, **kwargs) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| client.domain_retry_strategy = self.request_with_retry | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| client.domain_req_failed_counter = {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from threading import Lock | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| client.domain_counter_lock = Lock() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return client | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.option.new_jm_client = hook_new_jm_client | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def request_with_retry(self, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| client, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| is_image, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| client: AbstractJmClient, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request: Callable, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url: str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| is_image: bool, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| **kwargs, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 实现如下域名重试机制: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - 对域名列表轮询请求,配置:retry_rounds | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - 限制单个域名最大失败次数,配置:retry_domain_max_times | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - 轮询域名列表前,根据历史失败次数对域名列表排序,失败多的后置 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def do_request(domain): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url_to_use = url | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if url_to_use.startswith('/'): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # path → url | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url_to_use = client.of_api_url(url, domain) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| client.update_request_with_specify_domain(kwargs, domain, is_image) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jm_log(client.log_topic(), client.decode(url_to_use)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif is_image: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 图片url | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| client.update_request_with_specify_domain(kwargs, None, is_image) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resp = request(url_to_use, **kwargs) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resp = client.raise_if_resp_should_retry(resp, is_image) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return resp | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1260
to
+1274
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Use per-attempt kwargs and reset failure count on success.
Apply this diff: - def do_request(domain):
- url_to_use = url
+ def do_request(domain):
+ # Use a fresh kwargs for each attempt
+ local_kwargs = dict(kwargs)
+ url_to_use = url
if url_to_use.startswith('/'):
# path → url
url_to_use = client.of_api_url(url, domain)
- client.update_request_with_specify_domain(kwargs, domain, is_image)
+ client.update_request_with_specify_domain(local_kwargs, domain, is_image)
jm_log(client.log_topic(), client.decode(url_to_use))
elif is_image:
# 图片url
- client.update_request_with_specify_domain(kwargs, None, is_image)
+ client.update_request_with_specify_domain(local_kwargs, None, is_image)
- resp = request(url_to_use, **kwargs)
+ resp = request(url_to_use, **local_kwargs)
resp = client.raise_if_resp_should_retry(resp, is_image)
+ # Success → clear failed counter for this domain
+ with client.domain_counter_lock:
+ client.domain_req_failed_counter[domain] = 0
return resp📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.12.2)1266-1266: (F405) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| retry_domain_max_times: int = self.retry_config['retry_domain_max_times'] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| retry_rounds: int = self.retry_config['retry_rounds'] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for rindex in range(retry_rounds): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| domain_list = self.get_sorted_domain(client, retry_domain_max_times) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for i, domain in enumerate(domain_list): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self.failed_count(client, domain) >= retry_domain_max_times: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return do_request(domain) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from common import traceback_print_exec | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| traceback_print_exec() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jm_log('req.error', str(e)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.update_failed_count(client, domain) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return client.fallback(request, url, 0, 0, is_image, **kwargs) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def get_sorted_domain(self, client: JmcomicClient, times): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| domain_list = client.get_domain_list() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return sorted( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filter(lambda d: self.failed_count(client, d) < times, domain_list), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key=lambda d: self.failed_count(client, d) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # noinspection PyUnresolvedReferences | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def update_failed_count(self, client: AbstractJmClient, domain: str): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with client.domain_counter_lock: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| client.domain_req_failed_counter[domain] = self.failed_count(client, domain) + 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @staticmethod | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def failed_count(client: JmcomicClient, domain: str) -> int: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # noinspection PyUnresolvedReferences | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return client.domain_req_failed_counter.get(domain, 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Add newline at EOF; confirm retry interplay.
Add final newline. Also verify that client.retry_times is intentionally unused when advanced-retry is active to avoid confusion.
🏁 Script executed:
Length of output: 3936
🏁 Script executed:
Length of output: 842
🏁 Script executed:
Length of output: 29
Add final newline to assets/option/option_test_api.yml. Confirmed that when advanced-retry (domain_retry_strategy) is set, JmcomicClient always uses request_with_retry and ignores client.retry_times, so retry_times is intentionally unused.
🧰 Tools
🪛 YAMLlint (1.37.1)
[error] 34-34: no new line character at the end of file
(new-line-at-end-of-file)
🤖 Prompt for AI Agents