feat: stabilize multi-region runtime sync and worker orchestration

This commit is contained in:
root
2026-04-27 15:48:12 +08:00
parent 7cbde2aa78
commit 215a364891
137 changed files with 31931 additions and 1943 deletions

View File

@@ -12,6 +12,7 @@ import re # 正则表达式模块
import os
import threading
import time
from collections import OrderedDict
import requests # HTTP请求库
from loguru import logger # 日志记录
from requests.adapters import HTTPAdapter
@@ -23,7 +24,12 @@ SO_SEARCH_URL = 'https://www.so.com/s' # 360搜索URL
SO_REFERER_TEMPLATE = 'https://www.so.com/s?ie=utf-8&q=site%3A{domain}' # Referer模板
SEARCH_PATTERN = r'target="_blank">([^<]+)</a></h3>' # 搜索结果匹配模式
BLOCKED_CODE = 3 # 拦截状态码
_SESSION_LOCAL = threading.local()
_DIRECT_HTTP = requests.Session()
_DIRECT_HTTP.trust_env = False
_DIRECT_HTTP.mount("http://", HTTPAdapter(pool_connections=128, pool_maxsize=256, max_retries=0))
_DIRECT_HTTP.mount("https://", HTTPAdapter(pool_connections=128, pool_maxsize=256, max_retries=0))
_PROXY_SESSIONS = OrderedDict()
_PROXY_SESSION_LOCK = threading.Lock()
SO_TIMEOUT_PROXY = max(
1.0,
float(os.getenv("DOMAINCHECK_360_TIMEOUT_PROXY", "1.4") or 1.4),
@@ -34,6 +40,20 @@ SO_TIMEOUT_DIRECT = max(
)
def _proxy_session_cache_limit() -> int:
try:
return max(1, int(os.getenv("DOMAINCHECK_360_PROXY_SESSION_CACHE_SIZE", "128") or 128))
except Exception:
return 128
def _close_session_quietly(session) -> None:
try:
session.close()
except Exception:
pass
def _resolve_360_timeout(proxies: Optional[Dict] = None, budget_seconds: Optional[float] = None) -> float:
timeout = float(SO_TIMEOUT_PROXY if proxies else SO_TIMEOUT_DIRECT)
if budget_seconds not in (None, "", 0, "0"):
@@ -41,15 +61,27 @@ def _resolve_360_timeout(proxies: Optional[Dict] = None, budget_seconds: Optiona
return max(0.6, timeout)
def _get_session():
session = getattr(_SESSION_LOCAL, "session", None)
if session is not None:
def _get_session(proxies: Optional[Dict] = None):
proxy_url = ""
if proxies:
proxy_url = str(proxies.get("https") or proxies.get("http") or "").strip()
if not proxy_url:
return _DIRECT_HTTP
with _PROXY_SESSION_LOCK:
session = _PROXY_SESSIONS.get(proxy_url)
if session is not None:
_PROXY_SESSIONS.move_to_end(proxy_url)
return session
session = requests.Session()
session.trust_env = False
session.mount("http://", HTTPAdapter(pool_connections=64, pool_maxsize=128, max_retries=0))
session.mount("https://", HTTPAdapter(pool_connections=64, pool_maxsize=128, max_retries=0))
_PROXY_SESSIONS[proxy_url] = session
while len(_PROXY_SESSIONS) > _proxy_session_cache_limit():
_, stale_session = _PROXY_SESSIONS.popitem(last=False)
_close_session_quietly(stale_session)
return session
session = requests.Session()
session.mount("http://", HTTPAdapter(pool_connections=64, pool_maxsize=128, max_retries=0))
session.mount("https://", HTTPAdapter(pool_connections=64, pool_maxsize=128, max_retries=0))
_SESSION_LOCAL.session = session
return session
def check_domain(
@@ -95,7 +127,7 @@ def check_domain(
try:
session_started_at = time.perf_counter()
logger.info(f'360阶段: domain={domain} | stage=session_init_start')
session = _get_session()
session = _get_session(proxies)
logger.info(
f'360阶段: domain={domain} | stage=session_init_done '
f'| elapsed_ms={int((time.perf_counter() - session_started_at) * 1000)}'
@@ -103,20 +135,28 @@ def check_domain(
# 发送GET请求获取搜索结果
request_started_at = time.perf_counter()
logger.info(f'360阶段: domain={domain} | stage=request_start | proxy={"yes" if proxies else "no"}')
response = session.get(
SO_SEARCH_URL,
params=params,
headers=headers,
proxies=proxies,
timeout=_resolve_360_timeout(proxies, budget_seconds=budget_seconds),
)
logger.info(
f'360阶段: domain={domain} | stage=request_done | status={response.status_code} '
f'| elapsed_ms={int((time.perf_counter() - request_started_at) * 1000)}'
)
response = None
try:
response = session.get(
SO_SEARCH_URL,
params=params,
headers=headers,
proxies=proxies,
timeout=_resolve_360_timeout(proxies, budget_seconds=budget_seconds),
)
logger.info(
f'360阶段: domain={domain} | stage=request_done | status={response.status_code} '
f'| elapsed_ms={int((time.perf_counter() - request_started_at) * 1000)}'
)
# 解析响应内容
response_html = response.content.decode('utf-8', errors='ignore')
# 解析响应内容
response_html = response.content.decode('utf-8', errors='ignore')
finally:
try:
if response is not None:
response.close()
except Exception:
pass
# 使用正则表达式提取搜索结果
search_results = re.findall(SEARCH_PATTERN, response_html)

View File

@@ -15,10 +15,23 @@ import subprocess
import execjs
def _resolve_geetest_asset_timeout():
connect_timeout = max(
0.3,
float(os.getenv("DOMAINCHECK_GEETEST_ASSET_CONNECT_TIMEOUT", "2.0") or 2.0),
)
read_timeout = max(
0.5,
float(os.getenv("DOMAINCHECK_GEETEST_ASSET_READ_TIMEOUT", "4.0") or 4.0),
)
return (connect_timeout, read_timeout)
class slide():
def __init__(self):
self.headers = randomUA()
self.t = round(time.time() * 1000)
self.asset_timeout = _resolve_geetest_asset_timeout()
def __ease_out_expo(self, sep):
if sep == 1:
@@ -83,7 +96,7 @@ class slide():
for idv, p_url in vdict.items():
p_url = 'http://static.geetest.com/' + p_url
# print(p_url)
vcode = requests.get(p_url, headers=self.headers)
vcode = requests.get(p_url, headers=self.headers, timeout=self.asset_timeout)
text = vcode.content
if idv == 'bg':
text = self.tp_huanyuan(text)
@@ -131,7 +144,7 @@ class Geetest2():
"callback": f"geetest_{self.t}"
}
try:
respe = http.get(url=url, params=params, proxies=proxies).text
respe = http.get(url=url, params=params, proxies=proxies, timeout=self.timeout).text
except Exception as e:
if proxy:
if pparmas:

View File

@@ -13,7 +13,6 @@ import base64 # Base64编码解码
import hashlib # 哈希加密
import json # JSON数据处理
import os # 操作系统接口
import pickle # 序列化反序列化
import random # 随机数生成
import shutil
import subprocess # 子进程管理
@@ -24,6 +23,7 @@ import requests # HTTP请求库
from loguru import logger # 日志记录
from requests.cookies import RequestsCookieJar # Cookie管理
from detect.locked_pickle import load_pickle_locked, save_pickle_atomic
# # 移除自定义子进程类的替换,避免影响其他模块
@@ -109,12 +109,19 @@ class JC(object): # 聚查网API封装类
def __init__(self, proxies: dict = None): # 初始化JC类
self.session = requests.Session() # 创建会话对象
self.session.timeout = 10 # 设置超时时间
self.session.proxies = proxies # 设置代理
self.request_timeout = max(
0.5,
float(os.getenv("DOMAINCHECK_JUCHA_TIMEOUT", "10") or 10),
)
self.base_url = "https://www.jucha.com" # 设置基础URL
self.cookie = requests.cookies.RequestsCookieJar() # 初始化CookieJar
self.juming_cookie = requests.cookies.RequestsCookieJar() # 初始化聚名网CookieJar
def _request(self, method: str, url: str, **kwargs):
kwargs.setdefault("timeout", self.request_timeout)
return getattr(self.session, method)(url, **kwargs)
def _get_headers(self, referer: str = None) -> dict: # 获取请求头可自定义referer
headers = self.headers.copy() # 复制默认请求头
if referer: # 如果指定了referer
@@ -139,7 +146,7 @@ class JC(object): # 聚查网API封装类
return False, f'滑块验证码失败,{retry_times}次内未成功' # 返回失败
def _check_request(self, url: str, data: dict, headers: dict, cookies=None) -> dict: # 检查请求并处理验证码
res = self.session.post(url=url, data=data, headers=headers, cookies=cookies) # 发送POST请求
res = self._request("post", url=url, data=data, headers=headers, cookies=cookies) # 发送POST请求
try:
response = res.json() # 尝试解析JSON
except Exception as e: # JSON解析失败
@@ -179,7 +186,7 @@ class JC(object): # 聚查网API封装类
"seed": calculate_seed(286) # 种子值
}
url = f"{self.base_url}/captcha/init" # 初始化URL
response = self.session.post(url, headers=self.headers, data=data, cookies=self.cookie).json() # 发送请求
response = self._request("post", url, headers=self.headers, data=data, cookies=self.cookie).json() # 发送请求
if response['code'] == 1: # 初始化成功
self.captchaId = response['data']['captchaId'] # 保存验证码ID
self.encryptionPublicKey = response['data']['encryptionPublicKey'] # 保存加密公钥
@@ -234,8 +241,13 @@ class JC(object): # 聚查网API封装类
url = f"{self.base_url}/captcha/verify" # 验证URL
verify_headers = self.headers.copy() # 复制请求头
verify_headers['Content-Type'] = 'application/json' # 设置内容类型(仅用于验证请求)
verify_response = self.session.post(url, headers=verify_headers, data=verify_data,
cookies=self.cookie) # 发送验证请求
verify_response = self._request(
"post",
url,
headers=verify_headers,
data=verify_data,
cookies=self.cookie,
) # 发送验证请求
if verify_response.cookies: # 如果响应中有新的Cookie
self.cookie.update(verify_response.cookies) # 合并Cookie而不是替换
response = verify_response.json() # 解析JSON响应
@@ -254,20 +266,12 @@ class JC(object): # 聚查网API封装类
return False, "Node.js脚本未输出任何内容" # 返回失败
def save_cookies(self, filepath="jucha_cookies.pkl"): # 保存Cookie到文件
with open(filepath, "wb") as f: # 以二进制写入模式打开文件
pickle.dump(self.cookie, f) # 序列化保存Cookie
save_pickle_atomic(filepath, self.cookie)
logger.info(f"已保存Cookie到文件: {filepath}")
# 保存到Redis
try:
import redis
from app.config import config
redis_client = redis.Redis(
host=config.REDIS_HOST,
port=config.REDIS_PORT,
password=config.REDIS_PASSWORD,
db=config.REDIS_DB,
decode_responses=True
)
from app.utils.redis_client import get_redis_client
redis_client = get_redis_client(role="standard")
# 将cookie转换为字典
cookie_dict = {}
# 检查self.cookie的类型
@@ -292,25 +296,29 @@ class JC(object): # 聚查网API封装类
def load_cookies(self, filepath="jucha_cookies.pkl"): # 从文件加载Cookie
try: # 尝试加载
with open(filepath, "rb") as f: # 以二进制读取模式打开文件
loaded_cookie = pickle.load(f) # 反序列化加载Cookie
# 检查加载的cookie类型
if isinstance(loaded_cookie, dict):
# 如果是字典转换为RequestsCookieJar
cookie_jar = requests.cookies.RequestsCookieJar()
for name, value in loaded_cookie.items():
cookie_jar.set(name, value)
self.cookie = cookie_jar
else:
self.cookie = loaded_cookie
loaded_cookie = load_pickle_locked(
filepath,
default_factory=requests.cookies.RequestsCookieJar,
)
# 检查加载的cookie类型
if isinstance(loaded_cookie, dict):
# 如果是字典转换为RequestsCookieJar
cookie_jar = requests.cookies.RequestsCookieJar()
for name, value in loaded_cookie.items():
cookie_jar.set(name, value)
self.cookie = cookie_jar
else:
self.cookie = loaded_cookie
except Exception as e: # 加载失败
logger.error(f"加载Cookie失败: {e}")
self.cookie = requests.cookies.RequestsCookieJar() # 创建空的CookieJar
def load_juming_cookies(self, filepath="juming_cookies.pkl"): # 从文件加载聚名网Cookie
try: # 尝试加载
with open(filepath, "rb") as f: # 以二进制读取模式打开文件
self.juming_cookie = pickle.load(f) # 反序列化加载Cookie
self.juming_cookie = load_pickle_locked(
filepath,
default_factory=requests.cookies.RequestsCookieJar,
)
except: # 加载失败
self.juming_cookie = requests.cookies.RequestsCookieJar() # 创建空的CookieJar
@@ -318,7 +326,8 @@ class JC(object): # 聚查网API封装类
params = { # 请求参数
'platform': 'juming', # 平台标识
}
res = self.session.get( # 发送GET请求
res = self._request( # 发送GET请求
"get",
url=f'{self.base_url}/home/login/get_auth_url', # 获取授权URL
params=params, # 请求参数
headers=self.headers, # 请求头
@@ -333,7 +342,8 @@ class JC(object): # 聚查网API封装类
combined_cookies = requests.cookies.RequestsCookieJar()
combined_cookies.update(self.juming_cookie)
combined_cookies.update(self.cookie)
response = self.session.get( # 发送GET请求
response = self._request( # 发送GET请求
"get",
url=url, # 跳转URL
headers=self.headers, # 请求头
cookies=combined_cookies, # 合并后的Cookie
@@ -345,7 +355,8 @@ class JC(object): # 聚查网API封装类
self.cookie[name] = value # 直接赋值覆盖同名Cookie
if response.status_code == 302: # 重定向状态码
url = response.headers['location'] # 获取重定向URL
response = self.session.get( # 发送GET请求
response = self._request( # 发送GET请求
"get",
url=url, # 重定向URL
headers=self.headers, # 请求头
allow_redirects=False # 不自动重定向

View File

@@ -12,7 +12,6 @@ import base64 # Base64编码模块
import hashlib # 哈希算法模块
import json # JSON处理模块
import os # 操作系统接口模块
import pickle # 序列化模块
import random # 随机数生成模块
import shutil
import subprocess # 子进程管理模块
@@ -23,6 +22,7 @@ import requests # HTTP请求库
from loguru import logger # 日志记录库
from requests.cookies import RequestsCookieJar # Cookie处理
from detect.locked_pickle import load_pickle_locked, save_pickle_atomic
# 常量定义
BASE_URL = "https://www.juming.com" # 聚名网基础URL
@@ -170,11 +170,18 @@ class JM(object): # 聚名网API封装类
def __init__(self, proxies: Optional[Dict] = None): # 初始化方法
self.session = requests.Session() # 创建会话对象
self.session.timeout = REQUEST_TIMEOUT # 设置超时时间
self.session.proxies = proxies # 设置代理
self.request_timeout = max(
0.5,
float(os.getenv("DOMAINCHECK_JUMING_TIMEOUT", str(REQUEST_TIMEOUT)) or REQUEST_TIMEOUT),
)
self.base_url = BASE_URL # 设置基础URL
logger.debug(proxies)
def _request(self, method: str, url: str, **kwargs):
kwargs.setdefault("timeout", self.request_timeout)
return getattr(self.session, method)(url, **kwargs)
def captcha_init(self) -> Tuple[bool, str]: # 初始化滑块验证码
"""
初始化滑块验证码
@@ -194,7 +201,7 @@ class JM(object): # 聚名网API封装类
# self.cookie.clear()
# self.cookie.update({'acw_sc__v2': '69d948ed49117d89433d4316a79ada6f00d68f26'})
# logger.info(self.cookie)
response = self.session.post(url, headers=self.headers, data=data, cookies=self.cookie).json() # 发送请求
response = self._request("post", url, headers=self.headers, data=data, cookies=self.cookie).json() # 发送请求
logger.info(response)
if response['code'] == 1: # 初始化成功
@@ -271,7 +278,7 @@ class JM(object): # 聚名网API封装类
url = f"{self.base_url}/captcha/verify" # 验证URL
self.headers['Content-Type'] = 'application/json' # 设置内容类型
response = self.session.post(url, headers=self.headers, data=verify_data, cookies=self.cookie).json() # 发送验证请求
response = self._request("post", url, headers=self.headers, data=verify_data, cookies=self.cookie).json() # 发送验证请求
if response['code'] == 1: # 验证成功
self.token = response['data']['token'] # 保存token
self.session_id = response['data']['session_id'] # 保存会话ID
@@ -294,7 +301,7 @@ class JM(object): # 聚名网API封装类
"""
url = f"{self.base_url}/user_zh/wxdl_ewm" # 二维码URL
try: # 异常处理
response = self.session.post(url, headers=self.headers).json() # 发送请求
response = self._request("post", url, headers=self.headers).json() # 发送请求
self.token = response['data']["token"] # 保存token
return response['code'] == 1, response['msg'], response['data']['url'] if response['code'] == 1 else None # 返回结果
except Exception as e: # 异常处理
@@ -338,7 +345,8 @@ class JM(object): # 聚名网API封装类
}
login_url = f"{self.base_url}/user_zh/p_login" # 登录URL
response = self.session.post( # 发送登录请求
response = self._request( # 发送登录请求
"post",
url=login_url,
headers=self.headers,
json=data,
@@ -384,7 +392,7 @@ class JM(object): # 聚名网API封装类
url = f"{self.base_url}/ykj/get_list" # 列表URL
try: # 异常处理
self.headers['Content-Type'] = 'application/x-www-form-urlencoded' # 设置内容类型
res = self.session.post(url, headers=self.headers, data=data, cookies=self.cookie) # 发送请求
res = self._request("post", url, headers=self.headers, data=data, cookies=self.cookie) # 发送请求
response = res.json() # 解析响应
if response['code'] == -401: # 需要验证码
@@ -427,7 +435,7 @@ class JM(object): # 聚名网API封装类
"""
url = f"{self.base_url}/newcha/del_down?scsj={date}" # 下载URL
try: # 异常处理
response = self.session.get(url, headers=self.headers, cookies=self.cookie, allow_redirects=False) # 发送请求
response = self._request("get", url, headers=self.headers, cookies=self.cookie, allow_redirects=False) # 发送请求
url = response.headers['Location'] # 获取重定向URL
response = self.session.get(url, timeout=DOWNLOAD_TIMEOUT) # 下载文件
response_text = response.content.decode('utf-8') # 解码内容
@@ -443,20 +451,12 @@ class JM(object): # 聚名网API封装类
Args:
filepath: 文件路径
"""
with open(filepath, "wb") as f: # 以二进制写入模式打开文件
pickle.dump(self.cookie, f) # 序列化保存
save_pickle_atomic(filepath, self.cookie)
# 保存到Redis
try:
import redis
from app.config import config
redis_client = redis.Redis(
host=config.REDIS_HOST,
port=config.REDIS_PORT,
password=config.REDIS_PASSWORD,
db=config.REDIS_DB,
decode_responses=True
)
from app.utils.redis_client import get_redis_client
redis_client = get_redis_client(role="standard")
# 将cookie转换为字典
cookie_dict = {}
for cookie in self.cookie:
@@ -473,8 +473,10 @@ class JM(object): # 聚名网API封装类
filepath: 文件路径
"""
try: # 异常处理
with open(filepath, "rb") as f: # 以二进制读取模式打开文件
self.cookie = pickle.load(f) # 反序列化加载
self.cookie = load_pickle_locked(
filepath,
default_factory=requests.cookies.RequestsCookieJar,
)
except: # 异常处理
self.cookie = requests.cookies.RequestsCookieJar() # 创建空Cookie

View File

@@ -10,9 +10,7 @@
# 导入标准库
import os # 操作系统接口
import pickle # 序列化反序列化
import re
import time # 时间处理
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import base64
@@ -22,6 +20,7 @@ from loguru import logger # 日志记录
from requests.cookies import RequestsCookieJar # Cookie管理
from detect.geetest2 import Geetest2
from detect.locked_pickle import load_pickle_locked, save_pickle_atomic
@@ -69,30 +68,29 @@ class Juziseo:
self.gt = None
self.session = requests.Session() # 创建会话对象
self.session.proxies = proxies # 设置代理
self.session.timeout = 10 # 设置超时时间10秒
self.request_timeout = max(
0.5,
float(os.getenv("DOMAINCHECK_JUZISEO_TIMEOUT", "10") or 10),
)
self.base_url = "https://seo.juziseo.com" # 设置基础URL
def _request(self, method: str, url: str, **kwargs):
kwargs.setdefault("timeout", self.request_timeout)
return getattr(self.session, method)(url, **kwargs)
def save_cookies(self, filepath="juziseo_cookies.pkl"):
"""
保存Cookie到文件和Redis
:param filepath: 文件路径
"""
with open(filepath, "wb") as f: # 以二进制写入模式打开文件
pickle.dump(self.cookie, f) # 序列化保存Cookie
save_pickle_atomic(filepath, self.cookie)
logger.info(f"已保存桔子SEO Cookie到 {filepath}")
# 保存到Redis
try:
import redis
from app.config import config
redis_client = redis.Redis(
host=config.REDIS_HOST,
port=config.REDIS_PORT,
password=config.REDIS_PASSWORD,
db=config.REDIS_DB,
decode_responses=True
)
from app.utils.redis_client import get_redis_client
redis_client = get_redis_client(role="standard")
# 将cookie转换为字典
cookie_dict = {}
for cookie in self.cookie:
@@ -108,8 +106,10 @@ class Juziseo:
:param filepath: 文件路径
"""
try: # 尝试加载
with open(filepath, "rb") as f: # 以二进制读取模式打开文件
self.cookie = pickle.load(f) # 反序列化加载Cookie
self.cookie = load_pickle_locked(
filepath,
default_factory=requests.cookies.RequestsCookieJar,
)
# logger.info(f"已从 {filepath} 加载桔子SEO Cookie")
except: # 加载失败
self.cookie = requests.cookies.RequestsCookieJar() # 创建空的CookieJar
@@ -117,7 +117,7 @@ class Juziseo:
def start_msg_captcha_servlet(self):
url = f"{self.base_url}/class/gtcode/msg/StartMsgCaptchaServlet.php"
response = self.session.get(url, headers=self.headers, cookies=self.cookie).json()
response = self._request("get", url, headers=self.headers, cookies=self.cookie).json()
logger.info(response)
self.gt = response['gt']
self.challenge = response['challenge']
@@ -161,7 +161,7 @@ class Juziseo:
logger.debug(data)
# 发送登录请求
url = f"{self.base_url}/account/ajax/login_process/"
response = self.session.post(url, headers=self.headers, data=data, cookies=self.cookie)
response = self._request("post", url, headers=self.headers, data=data, cookies=self.cookie)
logger.info(response.text)
# 解析响应
result = response.json()
@@ -190,11 +190,11 @@ class Juziseo:
'_post_type': 'ajax',
}
url = f"{self.base_url}/snapshot/save/"
response = self.session.post(url, headers=self.headers, data=data, cookies=self.cookie).json()
response = self._request("post", url, headers=self.headers, data=data, cookies=self.cookie).json()
# logger.info(response)
if response['errno'] == 1:
url = response['rsm']['url']
response_html = self.session.get(url, headers=self.headers, cookies=self.cookie).content.decode('utf-8')
response_html = self._request("get", url, headers=self.headers, cookies=self.cookie).content.decode('utf-8')
title_sensitive_words_match = re.search(r'标题敏感词\D*(\d+)', response_html, re.S)
title_suspected_sensitive_words_match = re.search(r'标题有疑似敏感词\D*(\d+)', response_html, re.S)
content_sensitive_words_match = re.search(r'内容敏感词\D*(\d+)', response_html, re.S)
@@ -221,11 +221,11 @@ class Juziseo:
'_post_type': 'ajax',
}
url = f"{self.base_url}/domain_rank/save_domain/"
response = self.session.post(url, headers=self.headers, data=data, cookies=self.cookie).json()
response = self._request("post", url, headers=self.headers, data=data, cookies=self.cookie).json()
logger.info(response)
if response['errno'] == 1:
url = response['rsm']['url']
response_html = self.session.get(url, headers=self.headers, cookies=self.cookie).content.decode('utf-8')
response_html = self._request("get", url, headers=self.headers, cookies=self.cookie).content.decode('utf-8')
subdomain_match = re.search(r'子域名:\D*(\d+)', response_html, re.S)
if subdomain_match:
return False, f"存在子域名: {subdomain_match.group(1)}"

View File

@@ -0,0 +1,91 @@
# -*- coding: UTF-8 -*-
"""Helpers for cross-process safe pickle persistence."""
import os
import pickle
import tempfile
import time
from contextlib import contextmanager
from typing import Callable, TypeVar
T = TypeVar("T")
try: # pragma: no cover - platform specific
import fcntl
except ImportError: # pragma: no cover - platform specific
fcntl = None
try: # pragma: no cover - platform specific
import msvcrt
except ImportError: # pragma: no cover - platform specific
msvcrt = None
def _acquire_platform_lock(lock_file, *, timeout_seconds: float) -> None:
deadline = time.monotonic() + max(0.1, float(timeout_seconds or 0.0))
while True:
try:
if fcntl is not None:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
elif msvcrt is not None:
msvcrt.locking(lock_file.fileno(), msvcrt.LK_NBLCK, 1)
return
except (BlockingIOError, OSError):
if time.monotonic() >= deadline:
raise TimeoutError(f"lock acquire timed out for {lock_file.name}")
time.sleep(0.05)
def _release_platform_lock(lock_file) -> None:
try:
if fcntl is not None:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
elif msvcrt is not None:
lock_file.seek(0)
msvcrt.locking(lock_file.fileno(), msvcrt.LK_UNLCK, 1)
except Exception:
pass
@contextmanager
def _locked_path(path: str, *, timeout_seconds: float = 10.0):
normalized_path = os.path.abspath(path)
directory = os.path.dirname(normalized_path) or "."
os.makedirs(directory, exist_ok=True)
lock_path = f"{normalized_path}.lock"
with open(lock_path, "a+b") as lock_file:
_acquire_platform_lock(lock_file, timeout_seconds=timeout_seconds)
try:
yield normalized_path
finally:
_release_platform_lock(lock_file)
def save_pickle_atomic(path: str, value, *, timeout_seconds: float = 10.0) -> None:
with _locked_path(path, timeout_seconds=timeout_seconds) as normalized_path:
directory = os.path.dirname(normalized_path) or "."
fd, temp_path = tempfile.mkstemp(
prefix=f".{os.path.basename(normalized_path)}.",
suffix=".tmp",
dir=directory,
)
try:
with os.fdopen(fd, "wb") as temp_file:
pickle.dump(value, temp_file)
temp_file.flush()
os.fsync(temp_file.fileno())
os.replace(temp_path, normalized_path)
finally:
if os.path.exists(temp_path):
try:
os.remove(temp_path)
except OSError:
pass
def load_pickle_locked(path: str, *, default_factory: Callable[[], T], timeout_seconds: float = 10.0) -> T:
with _locked_path(path, timeout_seconds=timeout_seconds) as normalized_path:
if not os.path.exists(normalized_path):
return default_factory()
with open(normalized_path, "rb") as source:
return pickle.load(source)

View File

@@ -11,6 +11,7 @@
import json
import os
import threading
from collections import OrderedDict
from datetime import datetime, timezone, timedelta # 日期时间处理,用于时区转换
from typing import Optional
@@ -22,10 +23,24 @@ _DIRECT_HTTP = requests.Session()
_DIRECT_HTTP.trust_env = False
_DIRECT_HTTP.mount("http://", requests.adapters.HTTPAdapter(pool_connections=256, pool_maxsize=512, max_retries=0))
_DIRECT_HTTP.mount("https://", requests.adapters.HTTPAdapter(pool_connections=256, pool_maxsize=512, max_retries=0))
_PROXY_MANAGERS = {}
_PROXY_MANAGERS = OrderedDict()
_PROXY_MANAGER_LOCK = threading.Lock()
def _proxy_manager_cache_limit() -> int:
try:
return max(1, int(os.getenv("DOMAINCHECK_REGISTER_PROXY_SESSION_CACHE_SIZE", "128") or 128))
except Exception:
return 128
def _close_session_quietly(session) -> None:
try:
session.close()
except Exception:
pass
def _get_http_manager(proxies: Optional[dict] = None):
proxy_url = ""
if proxies:
@@ -34,12 +49,17 @@ def _get_http_manager(proxies: Optional[dict] = None):
return _DIRECT_HTTP
with _PROXY_MANAGER_LOCK:
manager = _PROXY_MANAGERS.get(proxy_url)
if manager is None:
manager = requests.Session()
manager.trust_env = False
manager.mount("http://", requests.adapters.HTTPAdapter(pool_connections=128, pool_maxsize=256, max_retries=0))
manager.mount("https://", requests.adapters.HTTPAdapter(pool_connections=128, pool_maxsize=256, max_retries=0))
_PROXY_MANAGERS[proxy_url] = manager
if manager is not None:
_PROXY_MANAGERS.move_to_end(proxy_url)
return manager
manager = requests.Session()
manager.trust_env = False
manager.mount("http://", requests.adapters.HTTPAdapter(pool_connections=128, pool_maxsize=256, max_retries=0))
manager.mount("https://", requests.adapters.HTTPAdapter(pool_connections=128, pool_maxsize=256, max_retries=0))
_PROXY_MANAGERS[proxy_url] = manager
while len(_PROXY_MANAGERS) > _proxy_manager_cache_limit():
_, stale_manager = _PROXY_MANAGERS.popitem(last=False)
_close_session_quietly(stale_manager)
return manager