返回 MoneyPrinterTurbo
state.py
根目录 / app / services / state.py
1 import ast
2 import copy
3 import threading
4 from abc import ABC, abstractmethod
5
6 from app.config import config
7 from app.models import const
8
9
10 # Base class for state management
11 class BaseState(ABC):
12 @abstractmethod
13 def update_task(self, task_id: str, state: int, progress: int = 0, **kwargs):
14 pass
15
16 @abstractmethod
17 def get_task(self, task_id: str):
18 pass
19
20 @abstractmethod
21 def get_all_tasks(self, page: int, page_size: int):
22 pass
23
24
25 # Memory state management
26 class MemoryState(BaseState):
27 def __init__(self):
28 self._tasks = {}
29 self._lock = threading.RLock()
30
31 def get_all_tasks(self, page: int, page_size: int):
32 start = (page - 1) * page_size
33 end = start + page_size
34 with self._lock:
35 tasks = [copy.deepcopy(task) for task in self._tasks.values()]
36 total = len(tasks)
37 return tasks[start:end], total
38
39 def update_task(
40 self,
41 task_id: str,
42 state: int = const.TASK_STATE_PROCESSING,
43 progress: int = 0,
44 **kwargs,
45 ):
46 progress = int(progress)
47 if progress > 100:
48 progress = 100
49
50 with self._lock:
51 self._tasks[task_id] = {
52 "task_id": task_id,
53 "state": state,
54 "progress": progress,
55 **kwargs,
56 }
57
58 def get_task(self, task_id: str):
59 with self._lock:
60 task = self._tasks.get(task_id, None)
61 return copy.deepcopy(task) if task is not None else None
62
63 def delete_task(self, task_id: str):
64 with self._lock:
65 self._tasks.pop(task_id, None)
66
67
68 # Redis state management
69 class RedisState(BaseState):
70 """
71 Redis-backed task state.
72
73 Trust boundary: Redis is expected to be private to this application. Task
74 values are written by MoneyPrinterTurbo and converted back from strings for
75 compatibility with existing state records. Do not expose this Redis database
76 to untrusted writers without replacing deserialization with a stricter
77 schema-based format.
78 """
79
80 def __init__(self, host="localhost", port=6379, db=0, password=None):
81 import redis
82
83 self._redis = redis.StrictRedis(host=host, port=port, db=db, password=password)
84
85 def get_all_tasks(self, page: int, page_size: int):
86 start = (page - 1) * page_size
87 end = start + page_size
88 tasks = []
89 cursor = 0
90 total = 0
91 while True:
92 cursor, keys = self._redis.scan(cursor, count=page_size)
93 batch_start = total
94 batch_size = len(keys)
95 total += batch_size
96
97 # Redis SCAN 是分批返回 key。分页切片必须基于“当前批次起始索引”
98 # 计算,而不能用累积后的 total 反推,否则第一页会切到空数组,
99 # 第二页也可能只返回部分数据。
100 if batch_start < end and total > start:
101 slice_start = max(0, start - batch_start)
102 slice_end = min(batch_size, end - batch_start)
103 for key in keys[slice_start:slice_end]:
104 task_data = self._redis.hgetall(key)
105 task = {
106 k.decode("utf-8"): self._convert_to_original_type(v)
107 for k, v in task_data.items()
108 }
109 tasks.append(task)
110
111 # 即使当前页已经取满,也要继续 SCAN 到 cursor=0,
112 # 因为调用方需要准确 total 来渲染分页信息。
113 if cursor == 0:
114 break
115 return tasks, total
116
117 def update_task(
118 self,
119 task_id: str,
120 state: int = const.TASK_STATE_PROCESSING,
121 progress: int = 0,
122 **kwargs,
123 ):
124 progress = int(progress)
125 if progress > 100:
126 progress = 100
127
128 fields = {
129 "task_id": task_id,
130 "state": state,
131 "progress": progress,
132 **kwargs,
133 }
134
135 for field, value in fields.items():
136 self._redis.hset(task_id, field, str(value))
137
138 def get_task(self, task_id: str):
139 task_data = self._redis.hgetall(task_id)
140 if not task_data:
141 return None
142
143 task = {
144 key.decode("utf-8"): self._convert_to_original_type(value)
145 for key, value in task_data.items()
146 }
147 return task
148
149 def delete_task(self, task_id: str):
150 self._redis.delete(task_id)
151
152 @staticmethod
153 def _convert_to_original_type(value):
154 """
155 Convert values written by this application back to common Python types.
156
157 This compatibility parser assumes Redis is inside the application's
158 trust boundary. If Redis can be written by untrusted clients, task state
159 should move to a strict JSON/schema parser instead of open-ended literal
160 conversion.
161 """
162 value_str = value.decode("utf-8")
163
164 try:
165 # try to convert byte string array to list
166 return ast.literal_eval(value_str)
167 except (ValueError, SyntaxError):
168 pass
169
170 if value_str.isdigit():
171 return int(value_str)
172 # Add more conversions here if needed
173 return value_str
174
175
176 # Global state
177 _enable_redis = config.app.get("enable_redis", False)
178 _redis_host = config.app.get("redis_host", "localhost")
179 _redis_port = config.app.get("redis_port", 6379)
180 _redis_db = config.app.get("redis_db", 0)
181 _redis_password = config.app.get("redis_password", None)
182
183 state = (
184 RedisState(
185 host=_redis_host, port=_redis_port, db=_redis_db, password=_redis_password
186 )
187 if _enable_redis
188 else MemoryState()
189 )
190
190 lines PYTHON