terryutils 1.0.6__tar.gz → 1.0.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {terryutils-1.0.6 → terryutils-1.0.7}/PKG-INFO +1 -1
- {terryutils-1.0.6 → terryutils-1.0.7}/pyproject.toml +1 -1
- terryutils-1.0.7/src/terryutils/mysql_pool_util.py +63 -0
- terryutils-1.0.7/src/terryutils/task_framework.py +222 -0
- {terryutils-1.0.6 → terryutils-1.0.7}/src/terryutils.egg-info/PKG-INFO +1 -1
- {terryutils-1.0.6 → terryutils-1.0.7}/src/terryutils.egg-info/SOURCES.txt +2 -0
- {terryutils-1.0.6 → terryutils-1.0.7}/README.md +0 -0
- {terryutils-1.0.6 → terryutils-1.0.7}/setup.cfg +0 -0
- {terryutils-1.0.6 → terryutils-1.0.7}/src/terryutils/__init__.py +0 -0
- {terryutils-1.0.6 → terryutils-1.0.7}/src/terryutils/date_util.py +0 -0
- {terryutils-1.0.6 → terryutils-1.0.7}/src/terryutils/log.py +0 -0
- {terryutils-1.0.6 → terryutils-1.0.7}/src/terryutils/mysql_util.py +0 -0
- {terryutils-1.0.6 → terryutils-1.0.7}/src/terryutils.egg-info/dependency_links.txt +0 -0
- {terryutils-1.0.6 → terryutils-1.0.7}/src/terryutils.egg-info/requires.txt +0 -0
- {terryutils-1.0.6 → terryutils-1.0.7}/src/terryutils.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from dbutils.pooled_db import PooledDB
|
|
2
|
+
import pymysql
|
|
3
|
+
from pymysql.cursors import DictCursor
|
|
4
|
+
|
|
5
|
+
MYSQL_POOL = PooledDB(
|
|
6
|
+
creator=pymysql,
|
|
7
|
+
maxconnections=20, # 最大连接数(重点)
|
|
8
|
+
mincached=2, # 启动时创建
|
|
9
|
+
maxcached=10, # 池中最大空闲
|
|
10
|
+
blocking=True, # 连接耗尽时等待
|
|
11
|
+
host="localhost",
|
|
12
|
+
user="user",
|
|
13
|
+
password="password",
|
|
14
|
+
database="db",
|
|
15
|
+
port=3306,
|
|
16
|
+
charset="utf8mb4",
|
|
17
|
+
cursorclass=DictCursor,
|
|
18
|
+
autocommit=False
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MysqlClient:
|
|
23
|
+
|
|
24
|
+
def __init__(self, pool):
|
|
25
|
+
self.pool = pool
|
|
26
|
+
|
|
27
|
+
def _execute(self, fn):
|
|
28
|
+
conn = self.pool.connection()
|
|
29
|
+
try:
|
|
30
|
+
result = fn(conn)
|
|
31
|
+
conn.commit()
|
|
32
|
+
return result
|
|
33
|
+
except Exception:
|
|
34
|
+
conn.rollback()
|
|
35
|
+
raise
|
|
36
|
+
finally:
|
|
37
|
+
conn.close() # ❗归还给 pool,不是真 close
|
|
38
|
+
|
|
39
|
+
def execute(self, sql, params=None):
|
|
40
|
+
def run(conn):
|
|
41
|
+
with conn.cursor() as cursor:
|
|
42
|
+
return cursor.execute(sql, params or ())
|
|
43
|
+
return self._execute(run)
|
|
44
|
+
|
|
45
|
+
def executemany(self, sql, params_list):
|
|
46
|
+
def run(conn):
|
|
47
|
+
with conn.cursor() as cursor:
|
|
48
|
+
return cursor.executemany(sql, params_list)
|
|
49
|
+
return self._execute(run)
|
|
50
|
+
|
|
51
|
+
def fetchone(self, sql, params=None):
|
|
52
|
+
def run(conn):
|
|
53
|
+
with conn.cursor() as cursor:
|
|
54
|
+
cursor.execute(sql, params or ())
|
|
55
|
+
return cursor.fetchone()
|
|
56
|
+
return self._execute(run)
|
|
57
|
+
|
|
58
|
+
def fetchall(self, sql, params=None):
|
|
59
|
+
def run(conn):
|
|
60
|
+
with conn.cursor() as cursor:
|
|
61
|
+
cursor.execute(sql, params or ())
|
|
62
|
+
return cursor.fetchall()
|
|
63
|
+
return self._execute(run)
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
import asyncio
|
|
5
|
+
import inspect
|
|
6
|
+
import logging
|
|
7
|
+
import traceback
|
|
8
|
+
import threading
|
|
9
|
+
from queue import Queue, Empty
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Callable, Optional
|
|
12
|
+
from logging.handlers import RotatingFileHandler
|
|
13
|
+
|
|
14
|
+
log_dir = "logs"
|
|
15
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
16
|
+
|
|
17
|
+
log_file = os.path.join(log_dir, "request_framework.log")
|
|
18
|
+
|
|
19
|
+
formatter = logging.Formatter(
|
|
20
|
+
'%(asctime)s | %(levelname)-5s | %(threadName)s | %(message)s',
|
|
21
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
22
|
+
)
|
|
23
|
+
# --- 文件 handler ---
|
|
24
|
+
file_handler = RotatingFileHandler(
|
|
25
|
+
log_file,
|
|
26
|
+
maxBytes=50 * 1024 * 1024,
|
|
27
|
+
backupCount=5,
|
|
28
|
+
encoding='utf-8'
|
|
29
|
+
)
|
|
30
|
+
file_handler.setFormatter(formatter)
|
|
31
|
+
file_handler.setLevel(logging.INFO)
|
|
32
|
+
|
|
33
|
+
# --- 控制台 handler ---
|
|
34
|
+
console_handler = logging.StreamHandler()
|
|
35
|
+
console_handler.setFormatter(formatter)
|
|
36
|
+
console_handler.setLevel(logging.INFO)
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger("RequestFramework")
|
|
39
|
+
logger.setLevel(logging.INFO)
|
|
40
|
+
|
|
41
|
+
# 防止重复 add
|
|
42
|
+
if not logger.handlers:
|
|
43
|
+
logger.addHandler(file_handler)
|
|
44
|
+
logger.addHandler(console_handler)
|
|
45
|
+
|
|
46
|
+
logger.propagate = False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# --- 1. 任务实体:统一封装请求 ---
|
|
52
|
+
@dataclass
|
|
53
|
+
class RequestTask:
|
|
54
|
+
name: str # 任务名称(日志用)
|
|
55
|
+
func: Callable # 执行函数 (requests.get 或 sdk.call)
|
|
56
|
+
args: tuple = field(default_factory=tuple)
|
|
57
|
+
kwargs: dict = field(default_factory=dict)
|
|
58
|
+
callback: Optional[Callable] = None # 成功回调
|
|
59
|
+
max_retries: int = 3 # 最大重试次数
|
|
60
|
+
retry_count: int = 0 # 当前重试计数
|
|
61
|
+
priority: int = 10 # 优先级(数字越小越高)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# --- 2. 频率控制器:令牌桶思想 ---
|
|
65
|
+
class RateLimiter:
|
|
66
|
+
def __init__(self, qps: int):
|
|
67
|
+
self.interval = 1.0 / qps
|
|
68
|
+
self.last_run = 0.0
|
|
69
|
+
self._lock = threading.Lock()
|
|
70
|
+
|
|
71
|
+
def wait(self):
|
|
72
|
+
with self._lock:
|
|
73
|
+
elapsed = time.time() - self.last_run
|
|
74
|
+
wait_time = self.interval - elapsed
|
|
75
|
+
if wait_time > 0:
|
|
76
|
+
time.sleep(wait_time)
|
|
77
|
+
self.last_run = time.time()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# --- 3. 调度引擎 ---
|
|
81
|
+
class RequestEngine:
|
|
82
|
+
def __init__(self, worker_num: int = 3, qps: int = 5):
|
|
83
|
+
self.task_queue = Queue() # 待处理队列
|
|
84
|
+
self.error_queue = Queue() # 错误队列
|
|
85
|
+
self.limiter = RateLimiter(qps) # 频率控制
|
|
86
|
+
self.worker_num = worker_num
|
|
87
|
+
self.running = False
|
|
88
|
+
|
|
89
|
+
def add_task(self, task: RequestTask):
|
|
90
|
+
"""外部提交任务接口"""
|
|
91
|
+
self.task_queue.put(task)
|
|
92
|
+
|
|
93
|
+
def start(self):
|
|
94
|
+
"""启动引擎"""
|
|
95
|
+
self.running = True
|
|
96
|
+
for i in range(self.worker_num):
|
|
97
|
+
t = threading.Thread(target=self._worker_loop, name=f"Worker-{i}")
|
|
98
|
+
t.daemon = True
|
|
99
|
+
t.start()
|
|
100
|
+
logger.info(f"引擎启动:工作线程={self.worker_num}, QPS限制={1 / self.limiter.interval}")
|
|
101
|
+
|
|
102
|
+
def _worker_loop(self):
|
|
103
|
+
"""工作线程主循环"""
|
|
104
|
+
while self.running:
|
|
105
|
+
try:
|
|
106
|
+
# 1. 获取任务 (阻塞1秒以便检查running状态)
|
|
107
|
+
task: RequestTask = self.task_queue.get(timeout=1)
|
|
108
|
+
|
|
109
|
+
# 2. 频率控制
|
|
110
|
+
self.limiter.wait()
|
|
111
|
+
|
|
112
|
+
# 3. 执行任务
|
|
113
|
+
self._execute(task)
|
|
114
|
+
|
|
115
|
+
self.task_queue.task_done()
|
|
116
|
+
except Empty:
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
def _execute(self, task: RequestTask):
|
|
120
|
+
"""执行逻辑:包含重试和异常处理"""
|
|
121
|
+
try:
|
|
122
|
+
logger.info(f"正在执行: {task.name}, 参数: {task.args}")
|
|
123
|
+
# 判断并处理异步函数
|
|
124
|
+
if inspect.iscoroutinefunction(task.func):
|
|
125
|
+
# 如果是 async def 定义的函数,启动一个临时的事件循环运行它
|
|
126
|
+
result = asyncio.run(task.func(*task.args, **task.kwargs))
|
|
127
|
+
else:
|
|
128
|
+
# 如果是普通的 def 定义的函数,直接调用
|
|
129
|
+
result = task.func(*task.args, **task.kwargs)
|
|
130
|
+
|
|
131
|
+
# 4. 执行回调
|
|
132
|
+
if task.callback:
|
|
133
|
+
task.callback(result)
|
|
134
|
+
|
|
135
|
+
logger.info(f"执行成功: {task.name}")
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
exc_type, exc_obj, tb = sys.exc_info()
|
|
139
|
+
filename = tb.tb_frame.f_code.co_filename
|
|
140
|
+
lineno = tb.tb_lineno
|
|
141
|
+
traceback.print_exc()
|
|
142
|
+
logger.error(
|
|
143
|
+
f"执行失败: {task.name}, "
|
|
144
|
+
f"{exc_type.__name__} at {filename}:{lineno}, "
|
|
145
|
+
f"错误信息: {e}"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# 5. 重试逻辑
|
|
149
|
+
if task.retry_count < task.max_retries:
|
|
150
|
+
task.retry_count += 1
|
|
151
|
+
wait_s = 2 ** task.retry_count # 指数退避
|
|
152
|
+
logger.warning(f"任务 {task.name} 将在 {wait_s}s 后进行第 {task.retry_count} 次重试")
|
|
153
|
+
|
|
154
|
+
# 异步延迟重试:不阻塞当前线程,放入定时器重新入队
|
|
155
|
+
threading.Timer(wait_s, self.add_task, args=[task]).start()
|
|
156
|
+
else:
|
|
157
|
+
# 6. 最终失败,放入错误队列
|
|
158
|
+
logger.critical(f"任务 {task.name} 达到最大重试次数,放弃处理")
|
|
159
|
+
self.error_queue.put({"task": task, "error": str(e)})
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
import requests
|
|
166
|
+
|
|
167
|
+
# 1. 初始化引擎(5个并发,每秒最多2个请求)
|
|
168
|
+
engine = RequestEngine(worker_num=3, qps=1)
|
|
169
|
+
# 2. 启动
|
|
170
|
+
engine.start()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# 示例 A: 封装 requests 请求
|
|
174
|
+
def get_weather(city):
|
|
175
|
+
resp = requests.get(f"https://httpbin.org/get?city={city}", timeout=5)
|
|
176
|
+
resp.raise_for_status()
|
|
177
|
+
return resp.json()
|
|
178
|
+
|
|
179
|
+
def callback_func(result):
|
|
180
|
+
print(result)
|
|
181
|
+
|
|
182
|
+
task1 = RequestTask(
|
|
183
|
+
name="天气查询-北京",
|
|
184
|
+
func=get_weather,
|
|
185
|
+
args=("Beijing",),
|
|
186
|
+
callback=callback_func # 成功后落库
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# 示例 B: 封装 SDK 请求
|
|
191
|
+
class MySDK:
|
|
192
|
+
def upload_file(self, file_id):
|
|
193
|
+
if file_id == "bad_id": raise ValueError("SDK内部错误")
|
|
194
|
+
return {"status": "uploaded", "id": file_id}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
sdk = MySDK()
|
|
198
|
+
task2 = RequestTask(
|
|
199
|
+
name="SDK上传-001",
|
|
200
|
+
func=sdk.upload_file,
|
|
201
|
+
kwargs={"file_id": "file_123"},
|
|
202
|
+
callback=callback_func
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# 示例 C: 故意失败的任务(测试重试)
|
|
206
|
+
task3 = RequestTask(
|
|
207
|
+
name="必失败任务",
|
|
208
|
+
func=sdk.upload_file,
|
|
209
|
+
kwargs={"file_id": "bad_id"},
|
|
210
|
+
max_retries=2
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# 3. 提交任务到队列
|
|
214
|
+
engine.add_task(task1)
|
|
215
|
+
engine.add_task(task2)
|
|
216
|
+
engine.add_task(task3)
|
|
217
|
+
|
|
218
|
+
# 保持主线程运行
|
|
219
|
+
try:
|
|
220
|
+
while True: time.sleep(1)
|
|
221
|
+
except KeyboardInterrupt:
|
|
222
|
+
print("退出中...")
|
|
@@ -3,7 +3,9 @@ pyproject.toml
|
|
|
3
3
|
src/terryutils/__init__.py
|
|
4
4
|
src/terryutils/date_util.py
|
|
5
5
|
src/terryutils/log.py
|
|
6
|
+
src/terryutils/mysql_pool_util.py
|
|
6
7
|
src/terryutils/mysql_util.py
|
|
8
|
+
src/terryutils/task_framework.py
|
|
7
9
|
src/terryutils.egg-info/PKG-INFO
|
|
8
10
|
src/terryutils.egg-info/SOURCES.txt
|
|
9
11
|
src/terryutils.egg-info/dependency_links.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|