Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/jmcomic/jm_option.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ def decide_domain_list():
if clazz == AbstractJmClient or not issubclass(clazz, AbstractJmClient):
raise NotImplementedError(clazz)

client: AbstractJmClient = clazz(
client: JmcomicClient = clazz(
postman=postman,
domain_list=decide_domain_list(),
retry_times=retry_times,
Expand Down
75 changes: 69 additions & 6 deletions src/jmcomic/jm_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1221,23 +1221,86 @@ 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Validate required keys and normalize types in retry_config.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
🤖 Prompt for AI Agents
In src/jmcomic/jm_plugin.py around lines 1231-1233, after verifying retry_config
is a dict, validate that all required keys used elsewhere (define a local
required_keys list matching the keys the retry logic expects) are present and
non-empty by calling self.require_param for each missing key to fail fast; then
normalize numeric fields by coercing values to int (wrap int(...) in try/except
to raise a clear require_param error on invalid types) and replace
self.retry_config with the normalized dict so later code won’t encounter
unexpected types.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use per-attempt kwargs and reset failure count on success.

  • Each attempt should use its own kwargs copy; domain-specific header mutations shouldn’t leak across attempts.
  • Reset the domain’s failure counter on success to allow recovery.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
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(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(local_kwargs, None, is_image)
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
🧰 Tools
🪛 Ruff (0.12.2)

1266-1266: jm_log may be undefined, or defined from star imports

(F405)

🤖 Prompt for AI Agents
In src/jmcomic/jm_plugin.py around lines 1260 to 1274, the request loop mutates
and reuses the same kwargs across retry attempts and never clears a domain
failure count on success; change it so each attempt creates a fresh copy of
kwargs (e.g., per_attempt_kwargs = dict(kwargs)) and use that copy for
client.update_request_with_specify_domain and request so domain-specific header
changes do not leak between attempts, and after a successful response reset the
domain's failure counter (either via the existing client reset helper if
available or by setting the client's failure counter for that domain to zero) so
the domain can recover for future requests.

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)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
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)
Loading