ldpy.base 0.0.1__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.
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: ldpy.base
3
+ Version: 0.0.1
4
+ Summary: No description
5
+ Author-email: distroy <distroy@live.com>
6
+ Requires-Dist: requests>=2.25.0
7
+ Requires-Dist: setproctitle>=1.3.7
8
+ Requires-Dist: pywin32>=300; sys_platform == "win32"
@@ -0,0 +1,35 @@
1
+ # # === setuptools ===
2
+ [build-system]
3
+ # 声明构建所需的工具
4
+ requires = ["setuptools>=61.0", "wheel"]
5
+ # 指定构建后端
6
+ build-backend = "setuptools.build_meta"
7
+
8
+ [project]
9
+ # 【必填】PyPI上的包名,全网唯一
10
+ name = "ldpy.base"
11
+ # 【必填】版本号,遵循语义化版本,例如0.1.0
12
+ version = "0.0.1"
13
+ # 包的作者信息
14
+ authors = [
15
+ { name="distroy", email="distroy@live.com" },
16
+ ]
17
+ # 包的描述文字
18
+ description = "No description"
19
+ # 指定README文件,内容会显示在PyPI项目主页上
20
+ # readme = "README.md"
21
+ # 指定开源许可证
22
+ # license = { text = "MIT" }
23
+ # 声明项目依赖的三方库
24
+ dependencies = [
25
+ "requests>=2.25.0",
26
+ "setproctitle>=1.3.7",
27
+ 'pywin32>=300; sys_platform == "win32"', # 条件依赖示例
28
+ ]
29
+ # 定义命令行入口,实现pip安装后即可使用自定义命令
30
+ # [project.scripts]
31
+ # your-command = "your_package.main:cli"
32
+
33
+ # 配置需要打包的Python代码路径
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"] # 若代码在src目录下
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright (C) distroy
5
+ #
6
+
7
+
8
+ __path__ = __import__("pkgutil").extend_path(__path__, __name__)
@@ -0,0 +1,7 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright (C) distroy
5
+ #
6
+
7
+
@@ -0,0 +1,62 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright (C) distroy
5
+ #
6
+
7
+
8
+ import os
9
+ from typing import TypeVar, Union
10
+
11
+ T = TypeVar('T')
12
+
13
+
14
+ def get(key: 'str', _def: 'T' = None) -> 'Union[str, T]':
15
+ return os.getenv(key, _def)
16
+
17
+
18
+ def get_as_str(key: 'str', _def: 'str' = '') -> 'str':
19
+ return get(key, _def)
20
+
21
+
22
+ def get_as_int(key: 'str', _def: 'int' = 0) -> 'int':
23
+ v = get(key)
24
+ if v is None:
25
+ return _def
26
+ try:
27
+ return int(v)
28
+ except:
29
+ pass
30
+ try:
31
+ return int(float(v))
32
+ except:
33
+ pass
34
+ try:
35
+ return 1 if bool(v) else 0
36
+ except:
37
+ pass
38
+ return _def
39
+
40
+
41
+ def get_as_bool(key: 'str', _def: 'bool' = False) -> 'bool':
42
+ v = get(key)
43
+ if not v:
44
+ return _def
45
+ try:
46
+ return True if int(v) else False
47
+ except:
48
+ pass
49
+ try:
50
+ return bool(v)
51
+ except:
52
+ pass
53
+ try:
54
+ return True if float(v) else False
55
+ except:
56
+ pass
57
+ v = v.lower()
58
+ # if v in ('on', 'enable', 'enabled'):
59
+ # return True
60
+ if v in ('off', 'disable', 'disabled', 'null'):
61
+ return False
62
+ return True
@@ -0,0 +1,50 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright (C) distroy
5
+ #
6
+
7
+
8
+ import logging
9
+ import time
10
+ import traceback
11
+ from typing import Optional
12
+
13
+
14
+ class WithLog(object):
15
+ def __init__(self, name: str, logging=True, level=logging.INFO, ignore_exc: bool = False):
16
+ self._name = name
17
+ self._begin = 0
18
+ self._ignore_exc = ignore_exc
19
+ self._logging = logging
20
+ self._level = level
21
+
22
+ def _log_func(self, msg: 'str', stacklevel: 'int', exc_info: 'Optional[Exception]' = None):
23
+ if self._logging:
24
+ logging.log(self._level, msg, stacklevel=stacklevel + 1,
25
+ exc_info=exc_info)
26
+ elif exc_info:
27
+ print(f'{msg}\n{"".join(traceback.format_exception(exc_info))}')
28
+
29
+ else:
30
+ print(msg)
31
+
32
+ def __enter__(self):
33
+ self._begin = time.time()
34
+ self._log_func(f' === do {self._name} begin', stacklevel=3)
35
+
36
+ def __exit__(self, exc_type, exc_value, exc_traceback):
37
+ cost = time.time() - self._begin
38
+ self._log_func(f' === do {self._name} end. cost:{cost:.6f}s', stacklevel=3,
39
+ exc_info=exc_value)
40
+ if self._ignore_exc:
41
+ return True
42
+
43
+
44
+ if __name__ == "__main__":
45
+ logging.basicConfig(level=logging.INFO,
46
+ format='%(asctime)s - %(filename)s:%(lineno)d - %(levelname)s - %(message)s')
47
+ with WithLog('abc', logging=False, ignore_exc=True):
48
+ 1 / 0
49
+ with WithLog('abc', ignore_exc=True):
50
+ 1 / 0
@@ -0,0 +1,29 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright (C) distroy
5
+ #
6
+
7
+
8
+ import logging
9
+ import os
10
+ import sys
11
+ import tempfile
12
+
13
+
14
+ class TempDir(object):
15
+ def __init__(self, dir='', prefix=''):
16
+ if not os.path.exists(dir):
17
+ os.makedirs(dir, exist_ok=True)
18
+
19
+ self._root_dir = dir
20
+ self._tdir = tempfile.TemporaryDirectory(dir=dir, prefix=prefix)
21
+ self.path = self._tdir.name
22
+ logging.info(f' === open temporary directory. dir:{self.path}')
23
+
24
+ def __enter__(self):
25
+ return self
26
+
27
+ def __exit__(self, exc_type, exc_value, exc_traceback):
28
+ self._tdir.cleanup()
29
+ logging.info(f' === clean temporary directory. dir:{self.path}')
@@ -0,0 +1,147 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright (C) distroy
5
+ #
6
+
7
+
8
+ import threading
9
+ from typing import Callable, Generic, Optional, TypeVar
10
+
11
+
12
+ T = TypeVar('T')
13
+
14
+
15
+ class _once(Generic[T]):
16
+ def __init__(self, func: 'Callable[[], T]'):
17
+ if not callable(func):
18
+ raise TypeError("the first argument must be callable")
19
+
20
+ self.__func = func
21
+
22
+ self.__done = False
23
+ self.__res = None
24
+
25
+ def reset(self):
26
+ self.__done = False
27
+
28
+ def done(self):
29
+ return self.__done
30
+
31
+ def __call__(self) -> T:
32
+ return self.call()
33
+
34
+ def call(self) -> T:
35
+ if self.__done:
36
+ return self.__res
37
+
38
+ self.__res = self.__func()
39
+ self.__done = True
40
+
41
+ return self.__res
42
+
43
+ def _get_res(self) -> 'Optional[T]':
44
+ return self.__res
45
+
46
+
47
+ class Once(Generic[T]):
48
+ def __init__(self, func: 'Callable[[], T]'):
49
+ self.__once = _once(func)
50
+
51
+ self.__lock = threading.Lock()
52
+
53
+ def reset(self):
54
+ with self.__lock:
55
+ self.__once.reset()
56
+
57
+ def done(self):
58
+ return self.__once.done()
59
+
60
+ # @decorator.decorator
61
+ def __call__(self) -> T:
62
+ return self.call()
63
+
64
+ def call(self) -> T:
65
+ if self.done():
66
+ return self.__once._get_res()
67
+
68
+ with self.__lock:
69
+ return self.__once.call()
70
+
71
+
72
+ class ThreadOnce(Generic[T]):
73
+ def __init__(self, func: 'Callable[[], T]'):
74
+ if not callable(func):
75
+ raise TypeError("the first argument must be callable")
76
+
77
+ self.__func = func
78
+ self.__data = threading.local()
79
+
80
+ def _get(self, force: bool = True) -> 'Optional[_once[T]]':
81
+ attr = 'value'
82
+ if hasattr(self.__data, attr):
83
+ return getattr(self.__data, attr)
84
+
85
+ if not force:
86
+ return None
87
+
88
+ v = _once(self.__func)
89
+ setattr(self.__data, attr, v)
90
+ return v
91
+
92
+ def reset(self):
93
+ v = self._get(force=False)
94
+ if not v:
95
+ return
96
+ v.reset()
97
+
98
+ def done(self) -> bool:
99
+ v = self._get(force=False)
100
+ return bool(v) and v.done()
101
+
102
+ def __call__(self) -> T:
103
+ return self.call()
104
+
105
+ def call(self) -> T:
106
+ v = self._get(force=True)
107
+ return v.call()
108
+
109
+
110
+ def main():
111
+ __key = 'sequence'
112
+ __cache = {__key: 0}
113
+
114
+ def get_seq() -> int:
115
+ key = 'sequence'
116
+ __cache[key] += 1
117
+ return __cache[key]
118
+
119
+ def get_thread_run(fn: 'Callable[[], T]'):
120
+ def thread_run():
121
+ try:
122
+ import time
123
+ time.sleep(1)
124
+ print(fn(), fn(), fn())
125
+ except Exception as exc:
126
+ import traceback
127
+ print(traceback.format_exception(exc))
128
+ return thread_run
129
+
130
+ from concurrent.futures import ThreadPoolExecutor
131
+ num = 10
132
+
133
+ fn0 = Once(get_seq)
134
+ with ThreadPoolExecutor(max_workers=num) as executor:
135
+ for _ in range(num):
136
+ executor.submit(get_thread_run(fn0))
137
+ print(' === ')
138
+
139
+ fn1 = ThreadOnce(get_seq)
140
+ with ThreadPoolExecutor(max_workers=num) as executor:
141
+ for _ in range(num):
142
+ executor.submit(get_thread_run(fn1))
143
+ print(' === ')
144
+
145
+
146
+ if __name__ == "__main__":
147
+ main()
@@ -0,0 +1,61 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright (C) tanrenchong
5
+ #
6
+
7
+
8
+ import logging
9
+ from .. import ldenv
10
+ from .call_client import new_call
11
+
12
+ DEFAULT_START_TIMEOUT = 600
13
+
14
+
15
+ class CallWorkerBase(object):
16
+ def __init__(self):
17
+ pass
18
+
19
+ @classmethod
20
+ def name(cls):
21
+ n = cls.__name__
22
+ n = n.removesuffix('Worker')
23
+ n = n.lower()
24
+ n = n.removesuffix('_worker')
25
+ return n
26
+
27
+ @classmethod
28
+ def backlog(cls) -> int:
29
+ n = cls.name()
30
+ key0 = f'{n.upper()}_BACKLOG'
31
+ key1 = 'BACKLOG'
32
+ backlog = ldenv.get_as_int(key0) or ldenv.get_as_int(key1) or 0
33
+ logging.info(f'get call worker listen backlog succ. worker:{n}, '
34
+ f'backlog:{backlog}')
35
+ return backlog
36
+
37
+ @classmethod
38
+ def worker_num(cls):
39
+ n = cls.name()
40
+ key = f'{n.upper()}_WORKER_NUM'
41
+ num = ldenv.get_as_int(key, 1)
42
+ logging.info(f'get call worker num succ. worker:{n}, num:{num}')
43
+ return num
44
+
45
+ @classmethod
46
+ def start_timeout(cls):
47
+ n = cls.name()
48
+ key0 = f'{n.upper()}_START_TIMEOUT'
49
+ key1 = 'START_TIMEOUT'
50
+ timeout = ldenv.get_as_int(key0) or ldenv.get_as_int(
51
+ key1) or DEFAULT_START_TIMEOUT
52
+ logging.info(f'get call worker start timeout succ. worker:{n}, '
53
+ f'timeout:{timeout}')
54
+ return timeout
55
+
56
+
57
+ __all__ = [
58
+ 'DEFAULT_START_TIMEOUT',
59
+ 'new_call',
60
+ 'CallWorkerBase',
61
+ ]
@@ -0,0 +1,242 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright (C) distroy
5
+ #
6
+
7
+
8
+ from collections.abc import Iterable, Mapping
9
+ import logging
10
+ import os
11
+ import socket
12
+ import select
13
+ import time
14
+ from typing import Type, TypeVar
15
+
16
+ # import bytedance.context
17
+ # from euler.errors import EulerError
18
+
19
+ from . import conn, call_worker, call_master
20
+
21
+ RES = TypeVar('RES')
22
+
23
+
24
+ class CallClientImpl(call_worker.CallBase[RES]):
25
+ def __init__(self, worker_cls: 'Type[call_worker.CallWorker[RES]]'):
26
+ super().__init__(worker_cls)
27
+
28
+ pid = os.getpid()
29
+ self._logid = f'[client-{self._name}:{pid}]'
30
+
31
+ self._process_func = self._process
32
+
33
+ def start(self):
34
+ call_master.start(self)
35
+
36
+ def connect(self):
37
+ logid = self._logid
38
+ logging.info(f'{logid} connect to call worker begin')
39
+
40
+ self._process_func = self._build_process_func(self._process, self._client_midwares)
41
+
42
+ try:
43
+ now = time.time()
44
+ timeout = self._worker_cls.start_timeout()
45
+ ddl = now + timeout
46
+ while True:
47
+ try:
48
+ c = self._connect(False)
49
+ # logging.info(f'{logid} connect succ')
50
+
51
+ req = call_worker.CallRequest(cmd=call_worker.CMD_INIT)
52
+ req_raw = call_worker.CallRequest.encode(req)
53
+ c.send(req_raw)
54
+ # logging.info(f'{logid} send succ')
55
+
56
+ rsp_raw = c.recv()
57
+ rsp = call_worker.CallResponse[RES].decode(rsp_raw)
58
+ logging.info(f'{logid} connect to call worker succ')
59
+ return
60
+
61
+ except Exception as exc:
62
+ if time.time() < ddl:
63
+ continue
64
+
65
+ logging.error(f'{logid} connect to call worker panic.',
66
+ exc_info=exc)
67
+ raise exc
68
+ finally:
69
+ logging.info(f'{logid} connect to call worker end')
70
+
71
+ def _connect(self, timeout=1, need_log: 'bool' = True):
72
+ '''
73
+ panic if not log
74
+ '''
75
+
76
+ logid = self._logid
77
+
78
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
79
+ sock.setblocking(False)
80
+ try:
81
+ sock.connect(self._worker_sock_path)
82
+ except BlockingIOError:
83
+ pass
84
+ except Exception as exc:
85
+ sock.close()
86
+
87
+ if not need_log:
88
+ logging.error(f'{logid} connect to call worker panic.',
89
+ exc_info=exc)
90
+ raise exc
91
+
92
+ _, writeable, _ = select.select([], [sock], [], timeout)
93
+ if writeable:
94
+ # 再次检查连接状态
95
+ ret = sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
96
+ if ret == 0:
97
+ sock.setblocking(True) # 恢复为阻塞模式以便后续操作
98
+ return conn.Conn(sock)
99
+
100
+ sock.close()
101
+ if need_log:
102
+ logging.error(
103
+ f'{logid} connect to call worker fail. ret:{ret}')
104
+ raise ConnectionError(f'connect to call worker fail. ret:{ret}')
105
+
106
+ sock.close()
107
+ if need_log:
108
+ logging.error(f'{logid} connect to call worker timeout:{timeout}s')
109
+ raise TimeoutError(
110
+ f'connect to call worker timeout. timeout:{timeout}s')
111
+
112
+ def process(self, *args, **kwargs) -> 'RES':
113
+ return self._process_func(*args, **kwargs)
114
+
115
+ def _process(self, *args, **kwargs) -> 'RES':
116
+ # logid = bytedance.context.get('logid')
117
+ req = call_worker.CallRequest(args=list(args), kwargs=kwargs)
118
+ req_raw = call_worker.CallRequest.encode(req)
119
+
120
+ i = 0
121
+ max_retry = 3
122
+ while True:
123
+ i += 1
124
+ try:
125
+ rsp_raw = self._send_and_recv(req_raw)
126
+ break
127
+
128
+ except OSError as exc:
129
+ if exc.errno == 107 and i <= max_retry:
130
+ continue
131
+
132
+ logging.error(f'{self._logid} send to call worker fail',
133
+ exc_info=exc)
134
+ raise exc
135
+
136
+ rsp = call_worker.CallResponse[RES].decode(rsp_raw)
137
+ if rsp.exc:
138
+ raise rsp.exc
139
+
140
+ res = rsp.res
141
+ logging.info(f'call worker process succ. worker:{self._name}, '
142
+ f'args:{_to_print(args)}, kwargs:{_to_print(kwargs)}, '
143
+ f'res:{_to_print(res)}')
144
+ return res
145
+
146
+ def _send_and_recv(self, req0_raw: 'bytes') -> 'bytes':
147
+ c = self._connect()
148
+
149
+ try:
150
+ c.send(req0_raw)
151
+ return c.recv()
152
+
153
+ finally:
154
+ c.close()
155
+
156
+ try:
157
+ from torch import Tensor
158
+ except:
159
+ class Tensor(object):
160
+ dtype = 0
161
+ device = 0
162
+ shape = 0
163
+
164
+ def detach(self, *args, **kwargs): return self
165
+ def reshape(self, *args, **kwargs): return self
166
+ def numpy(self, *args, **kwargs): return self
167
+ def __getitem__(self, key): return self
168
+
169
+ def _to_print(obj, seen=None):
170
+ """
171
+ 将任意对象递归转换为 dict / list 结构,基础类型原样返回。
172
+
173
+ 参数:
174
+ obj: 任意 Python 对象
175
+ seen: 用于检测循环引用的集合(内部使用,一般不需要手动提供)
176
+
177
+ 返回:
178
+ 转换后的 dict / list / 基础类型
179
+ """
180
+ if seen is None:
181
+ seen = set()
182
+
183
+ # 避免循环引用导致的无限递归
184
+ obj_id = id(obj)
185
+ if obj_id in seen:
186
+ # 循环引用时返回字符串标识(也可返回 None 或原对象,视需求而定)
187
+ return f"<recursion: {repr(obj)}>"
188
+ seen.add(obj_id)
189
+
190
+ try:
191
+ try:
192
+ if isinstance(obj, Tensor):
193
+ return f'<torch.Tensor(dtype={obj.dtype}, device={obj.device}, shape={obj.shape}, ' \
194
+ f'sample={obj.detach()[:10].reshape(-1)[:10].numpy()})>'
195
+ except:
196
+ pass
197
+
198
+ # 1. 基础类型(不可变且无需转换)
199
+ if isinstance(obj, (int, float, str, bool, type(None))):
200
+ return obj
201
+
202
+ if isinstance(obj, bytes):
203
+ return f'<bytes(size={len(obj)}, sample={obj[:10]})>'
204
+
205
+ # 2. 字典或映射类型
206
+ if isinstance(obj, Mapping):
207
+ return {_to_print(k, seen): _to_print(v, seen) for k, v in obj.items()}
208
+
209
+ # 3. 可迭代对象(list, tuple, set, frozenset 等)
210
+ if isinstance(obj, Iterable) and not isinstance(obj, (str, bytes, dict)):
211
+ # 注意:str/bytes 已在上面的基础类型中处理,此处不会重复
212
+ return [_to_print(item, seen) for item in obj]
213
+
214
+ # 4. 自定义类实例(包括 namedtuple、dataclass、普通类)
215
+ # 优先使用 __dict__,若没有则尝试 __slots__
216
+ if hasattr(obj, '__dict__'):
217
+ # 直接使用实例字典
218
+ return _to_print(vars(obj), seen)
219
+
220
+ if hasattr(obj, '__slots__'):
221
+ # __slots__ 类没有 __dict__,手动构建字典
222
+ slots_dict = {
223
+ slot: getattr(obj, slot)
224
+ for slot in obj.__slots__ if hasattr(obj, slot)
225
+ }
226
+ return _to_print(slots_dict, seen)
227
+ # 5. 其他无法转换的对象(函数、模块、类型、None 等)
228
+ # 可根据需要改为 str(obj) 或抛出异常,这里保留原样
229
+ return obj
230
+
231
+ finally:
232
+ # 递归返回后移除 id,允许其他分支复用 seen
233
+ seen.remove(obj_id)
234
+
235
+
236
+ def new_call(worker_cls: 'Type[call_worker.CallWorker[RES]]', start=False, connect=False) -> 'call_worker.CallClient[RES]':
237
+ cli = CallClientImpl[RES](worker_cls)
238
+ if start or connect:
239
+ cli.start()
240
+ if connect:
241
+ cli.connect()
242
+ return cli
@@ -0,0 +1,236 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright (C) distroy
5
+ #
6
+
7
+
8
+ import time
9
+ import backgrounds
10
+ import fcntl
11
+ import logging
12
+ import os
13
+ import setproctitle
14
+ import signal
15
+ import socket
16
+ import stat
17
+ import sys
18
+ from typing import Generic, List, TypeVar
19
+
20
+ from .. import ldlog
21
+ from . import call_service, call_worker
22
+
23
+ RES = TypeVar('RES')
24
+
25
+
26
+ class Lock(object):
27
+ def __init__(self, file_path: str) -> None:
28
+ self.lock_file_path = file_path
29
+ self.dir_path = os.path.dirname(file_path)
30
+ self.lock_fd = None
31
+
32
+ def try_lock(self):
33
+ lock_path = self.lock_file_path
34
+ lock_dir = os.path.dirname(lock_path)
35
+ try:
36
+ os.makedirs(lock_dir, 0o755, True)
37
+ except Exception as exc:
38
+ logging.error(f'os.makedirs fail. dir:{lock_dir}', exc_info=exc)
39
+
40
+ lock_fd = open(lock_path, 'w')
41
+ try:
42
+ fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
43
+ self.lock_fd = lock_fd
44
+ return True
45
+
46
+ except BlockingIOError:
47
+ lock_fd.close()
48
+ return False
49
+
50
+ def unlock(self):
51
+ lock_fd = self.lock_fd
52
+ self.lock_fd = None
53
+ if lock_fd:
54
+ fcntl.flock(lock_fd, fcntl.LOCK_UN)
55
+ lock_fd.close()
56
+
57
+
58
+ class CallMaster(Generic[RES]):
59
+ def __init__(self, base: 'call_worker.CallBase[RES]'):
60
+ super().__init__()
61
+
62
+ self._base = base
63
+ worker_cls = base._worker_cls
64
+
65
+ pid = os.getpid()
66
+ self._logid = f'[master-{self._base._name}:{pid}]'
67
+
68
+ self._running = False
69
+ self._worker_num = max(worker_cls.worker_num(), 1)
70
+ self._children: 'List[int]' = []
71
+
72
+ def name(self): return self._base.name()
73
+
74
+ def _stop(self):
75
+ self._running = False
76
+
77
+ def run(self):
78
+ backgrounds.start()
79
+ self._running = True
80
+
81
+ def graceful_exit(sig, frame):
82
+ self._stop()
83
+
84
+ signal.signal(signal.SIGTERM, graceful_exit)
85
+ signal.signal(signal.SIGINT, graceful_exit)
86
+
87
+ lock = Lock(self._base._master_lock_path)
88
+ if not lock.try_lock():
89
+ logging.warning(f'{self._logid} exit because locked by another')
90
+ sys.exit(0)
91
+ logging.info(f'{self._logid} continue start after locked')
92
+
93
+ logging.info(f'{self._logid} run begin')
94
+ try:
95
+ self._run()
96
+ finally:
97
+ lock.unlock()
98
+ logging.info(f'{self._logid} run end')
99
+
100
+ def _run(self):
101
+ sock = self._listen()
102
+
103
+ try:
104
+ while self._running:
105
+ self._start_children(sock)
106
+ self._check_children_exit()
107
+ time.sleep(0.2)
108
+
109
+ finally:
110
+ sock.close()
111
+ self._wait_children()
112
+
113
+ def _wait_children(self):
114
+ logid = self._logid
115
+
116
+ try:
117
+ logging.info(f'{logid} kill all children at exit')
118
+ for pid in self._children:
119
+ os.kill(pid, signal.SIGTERM)
120
+
121
+ logging.info(f'{logid} wait all children at exit')
122
+
123
+ ddl = time.time() + 60
124
+ while len(self._children) > 0:
125
+ self._check_children_exit()
126
+ now = time.time()
127
+ if now > ddl:
128
+ break
129
+ time.sleep(0.2)
130
+
131
+ for pid in self._children:
132
+ os.kill(pid, signal.SIGKILL)
133
+
134
+ except Exception as exc:
135
+ logging.error(f'{logid} wait all children panic at exit',
136
+ exc_info=exc)
137
+ finally:
138
+ logging.info(f'{logid} wait all children end at exit')
139
+
140
+ def _check_children_exit(self):
141
+ logid = self._logid
142
+
143
+ for pid in self._children:
144
+ try:
145
+ ret_pid, status = os.waitpid(pid, os.WNOHANG)
146
+ if ret_pid != pid: # 子进程已结束
147
+ continue
148
+ code = os.WEXITSTATUS(status)
149
+ logging.info(f'{logid} wait child exit. pid:{pid}, code:{code}')
150
+ self._children.remove(pid)
151
+ except ChildProcessError:
152
+ # 指定的子进程不存在
153
+ code = 'unknow'
154
+ logging.info(f'{logid} wait child exit. pid:{pid}, code:{code}')
155
+ self._children.remove(pid)
156
+
157
+ def _start_children(self, sock: socket.socket):
158
+ num = self._worker_num
159
+ while len(self._children) < num:
160
+ pid = call_service.start(num, sock, self._base)
161
+ self._children.append(pid)
162
+ logging.info(f'{self._logid} start call worker succ. pid:{pid}')
163
+
164
+ def _listen(self):
165
+ sock_path = self._base._worker_sock_path
166
+ if os.path.exists(sock_path):
167
+ os.remove(sock_path)
168
+
169
+ backlog = self._base._worker_cls.backlog()
170
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
171
+ sock.bind(sock_path)
172
+ sock.listen(backlog)
173
+
174
+ logging.info(f'{self._logid} call worker listen succ. sock:{sock_path}')
175
+ return sock
176
+
177
+
178
+ def _is_socket(fd: int):
179
+ try:
180
+ # 1. 检查是否是 socket
181
+ mode = os.fstat(fd).st_mode
182
+ if stat.S_IFMT(mode) != stat.S_IFSOCK:
183
+ return False
184
+
185
+ # 2. 创建临时 socket 对象(不拥有 fd 的所有权)
186
+ s = socket.socket(fileno=fd)
187
+ # 3. 获取 SO_ACCEPTCONN 选项
188
+ opt = s.getsockopt(socket.SOL_SOCKET, socket.SO_ACCEPTCONN)
189
+ # 4. 将 fd 从 socket 对象中分离,防止对象销毁时关闭 fd
190
+ s.detach()
191
+ return opt == 1
192
+ except OSError:
193
+ # fd 无效或不存在,忽略
194
+ return False
195
+
196
+ return True
197
+
198
+
199
+ def close_all_socket():
200
+ try:
201
+ max_fd = os.sysconf('SC_OPEN_MAX')
202
+ except (ValueError, OSError):
203
+ max_fd = 1024 # 常见系统的安全上限
204
+
205
+ # skip stdin(0), stdout(1), stderr(2)
206
+ for fd in range(3, max_fd):
207
+ try:
208
+ if _is_socket(fd):
209
+ os.close(fd)
210
+ except:
211
+ pass
212
+
213
+
214
+ def start(base: 'call_worker.CallBase[RES]'):
215
+ # wait + fork 2次,避免僵尸进程
216
+ pid = os.fork()
217
+ if pid > 0:
218
+ # in parent
219
+ os.waitpid(pid, 0)
220
+ return pid
221
+
222
+ pid = os.fork()
223
+ if pid > 0:
224
+ # in parent
225
+ sys.exit(0)
226
+
227
+ # in child
228
+ proc_title = f'call: master [{base.name()}]'
229
+ setproctitle.setproctitle(proc_title)
230
+
231
+ with ldlog.WithLog('close_all_socket'):
232
+ close_all_socket()
233
+
234
+ mgr = CallMaster(base)
235
+ mgr.run()
236
+ sys.exit(0)
@@ -0,0 +1,165 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright (C) distroy
5
+ #
6
+
7
+
8
+ import threading
9
+ import time
10
+ import backgrounds
11
+ import logging
12
+ import os
13
+ import sys
14
+ import signal
15
+ import socket
16
+ from typing import Generic, TypeVar
17
+ import psutil
18
+ import setproctitle
19
+
20
+ from .. import ldlog
21
+ from . import conn, call_worker
22
+
23
+
24
+ RES = TypeVar('RES')
25
+
26
+
27
+ class Service(Generic[RES]):
28
+ def __init__(self, worker_num: 'int', sock: 'socket.socket', base: 'call_worker.CallBase[RES]'):
29
+ super().__init__()
30
+
31
+ self._base = base
32
+ worker_cls = base._worker_cls
33
+
34
+ ppid = os.getppid()
35
+ pid = os.getpid()
36
+
37
+ self._logid = f'[worker-{worker_cls.name()}:{pid}]'
38
+
39
+ with ldlog.WithLog(f'init_worker_object {worker_cls.name()}'):
40
+ w = worker_cls()
41
+
42
+ self._ppid = ppid
43
+ self._name = worker_cls.name()
44
+ self._worker_cls = worker_cls
45
+ self._worker = w
46
+ self._worker_num = worker_num
47
+
48
+ self._process_func = base._build_process_func(w.process, base._server_midwares)
49
+
50
+ self._sock = sock
51
+ self._running = False
52
+
53
+ def _stop(self):
54
+ self._running = False
55
+
56
+ def run(self):
57
+ backgrounds.start()
58
+ self._running = True
59
+
60
+ def graceful_exit(sig, frame):
61
+ self._stop()
62
+
63
+ signal.signal(signal.SIGTERM, graceful_exit)
64
+ signal.signal(signal.SIGINT, graceful_exit)
65
+
66
+ th = threading.Thread(target=self._thread_check_parent)
67
+ th.daemon = True
68
+ th.start()
69
+
70
+ for func in self._base._post_fork_funcs:
71
+ func()
72
+
73
+ self._sock.settimeout(1.0)
74
+
75
+ logid = self._logid
76
+ logging.info(f'{logid} call worker run begin')
77
+ try:
78
+ while self._running:
79
+ try:
80
+ sock, addr = self._sock.accept()
81
+ except socket.timeout:
82
+ # 超时后继续循环,检查 should_exit
83
+ continue
84
+
85
+ # logging.info(f'{logid} accept conn succ. addr:{addr}')
86
+ c = conn.Conn(sock)
87
+
88
+ try:
89
+ self._handle_conn(c)
90
+ except Exception as exc:
91
+ logging.info(f'{logid} call worker handle conn panic', exc_info=exc)
92
+ finally:
93
+ c.close()
94
+ finally:
95
+ logging.info(f'{logid} call worker run end')
96
+
97
+ def _thread_check_parent(self):
98
+ ppid = self._ppid
99
+ logid = self._logid
100
+
101
+ logging.info(f'{logid} thread check parent begin. ppid:{ppid}')
102
+
103
+ pps = psutil.Process(ppid)
104
+ while self._running:
105
+ try:
106
+ status = pps.status()
107
+ if status in (psutil.STATUS_DEAD, psutil.STATUS_STOPPED):
108
+ logging.warning(f'{logid} parent is not running now. '
109
+ f'ppid:{ppid}')
110
+ self._stop()
111
+ except psutil.NoSuchProcess:
112
+ logging.warning(f'{logid} parent is not running now. '
113
+ f'ppid:{ppid}')
114
+ self._stop()
115
+ finally:
116
+ time.sleep(1)
117
+ logging.info(f'{logid} thread check parent end. ppid:{ppid}')
118
+
119
+ def _handle_conn(self, c: 'conn.Conn'):
120
+ req_raw = c.recv()
121
+ req = call_worker.CallRequest.decode(req_raw)
122
+
123
+ if req.command == call_worker.CMD_INIT:
124
+ rsp = call_worker.CallResponse[RES]()
125
+ elif req.command == call_worker.CMD_CALL:
126
+ rsp = self._process_request(req)
127
+ else:
128
+ msg = f'invalid command. cmd:{req.command}, worker:{self._name}'
129
+ logging.error(msg)
130
+ exc = Exception(msg)
131
+ rsp = call_worker.CallResponse[RES](exc=exc)
132
+
133
+ rsp_raw = call_worker.CallResponse.encode(rsp)
134
+ c.send(rsp_raw)
135
+
136
+ def _process_request(self, req: 'call_worker.CallRequest') -> 'call_worker.CallResponse[RES]':
137
+ logid = self._logid
138
+
139
+ with ldlog.WithLog(f'call worker process [{self._name}]'):
140
+ try:
141
+ # res = self._worker.process(*req.args, **req.kwargs)
142
+ res = self._process_func(*req.args, **req.kwargs)
143
+ logging.info(f'{logid} process request succ')
144
+ return call_worker.CallResponse[RES](res=res)
145
+
146
+ except Exception as exc:
147
+ logging.error(f'{logid} process request panic', exc_info=exc)
148
+ return call_worker.CallResponse[RES](exc=exc)
149
+
150
+
151
+ def start(worker_num: 'int', sock: 'socket.socket', base: 'call_worker.CallBase[RES]') -> 'int':
152
+ pid = os.fork()
153
+ if pid > 0:
154
+ # in parent
155
+ return pid
156
+
157
+ # in child
158
+ s = Service(worker_num, sock, base)
159
+
160
+ worker_cls = base._worker_cls
161
+ proc_title = f'call: worker [{worker_cls.name()}]'
162
+ setproctitle.setproctitle(proc_title)
163
+
164
+ s.run()
165
+ sys.exit(0)
@@ -0,0 +1,58 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright (C) distroy
5
+ #
6
+
7
+
8
+ import logging
9
+ import os
10
+ import sys
11
+ from typing import Any, Callable, Dict, Optional, Tuple
12
+
13
+ from .. import ldlog
14
+
15
+ from . import CallWorkerBase, new_call
16
+
17
+
18
+ def log(source: 'str', globals: 'Optional[Dict[str, Any]]' = None, locals: 'Optional[Dict[str, Any]]' = None):
19
+ logging.info(f'{source} = {eval(source, globals, locals)}')
20
+
21
+
22
+ class TestWorker(CallWorkerBase):
23
+ def process(self, a: 'int', b: 'int') -> 'Tuple[float, int]':
24
+ return a / b, a % b
25
+
26
+
27
+ def server_midware(*args, next: 'Callable', **kwargs):
28
+ with ldlog.WithLog('server_midware'):
29
+ return next(*args, **kwargs)
30
+
31
+ def client_midware(*args, next: 'Callable', **kwargs):
32
+ with ldlog.WithLog('client_midware'):
33
+ return next(*args, **kwargs)
34
+
35
+
36
+ def main():
37
+ cli = new_call(TestWorker)
38
+
39
+ cli.add_client_midware(client_midware)
40
+ cli.add_server_midware(server_midware)
41
+ cli.add_post_fork_func(lambda: logging.info(f'worker has started. pid:{os.getpid()}'))
42
+
43
+ cli.start()
44
+ cli.connect()
45
+
46
+ locals = {'cli': cli}
47
+ log('cli.process(1, 2)', locals=locals)
48
+ log('cli.process(a=1, b=0)', locals=locals)
49
+
50
+
51
+ if __name__ == "__main__":
52
+ logging.basicConfig(level=logging.INFO,
53
+ format='%(asctime)s - %(filename)s:%(lineno)d - %(levelname)s - %(message)s')
54
+ try:
55
+ sys.exit(main())
56
+ except KeyboardInterrupt:
57
+ sys.stderr.write("\033[1;31moperation cancelled by user\033[0m\n")
58
+ sys.exit(-1)
@@ -0,0 +1,126 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright (C) distroy
5
+ #
6
+ # pyright: reportInvalidTypeVarUse=false
7
+
8
+
9
+ import multiprocessing.reduction
10
+ import os
11
+ from typing import Callable, Dict, Generic, List, Optional, Protocol, Type, TypeVar
12
+
13
+ PICKLER = multiprocessing.reduction.ForkingPickler
14
+
15
+ RES = TypeVar('RES')
16
+
17
+
18
+ class CallWorker(Protocol[RES]):
19
+ @classmethod
20
+ def name(cls) -> str: ...
21
+ @classmethod
22
+ def backlog(cls) -> int: ...
23
+ @classmethod
24
+ def worker_num(cls) -> int: ...
25
+ @classmethod
26
+ def start_timeout(cls) -> int: ...
27
+
28
+ def process(self, *args, **kwargs) -> 'RES': ...
29
+
30
+
31
+ class CallMidware(Protocol[RES]):
32
+ def __call__(self, *args, next: 'Callable[..., RES]', **kwargs) -> 'RES':
33
+ ...
34
+
35
+
36
+ class CallClient(Protocol[RES]):
37
+ def name(self) -> str: ...
38
+ def add_post_fork_func(self, func: 'Callable[[], None]'): ...
39
+ def add_server_midware(self, mw: 'CallMidware[RES]'): ...
40
+ def add_client_midware(self, mw: 'CallMidware[RES]'): ...
41
+ def start(self): ...
42
+ def connect(self): ...
43
+ def process(self, *args, **kwargs) -> 'RES': ...
44
+
45
+
46
+ class CallBase(Generic[RES]):
47
+ def __init__(self, worker_cls: 'Type[CallWorker[RES]]'):
48
+ super().__init__()
49
+
50
+ name = worker_cls.name()
51
+ name = name.removesuffix('_worker')
52
+ self._worker_cls = worker_cls
53
+ self._name = name
54
+
55
+ cwd = os.path.abspath(os.getcwd())
56
+ cache_dir = os.path.join(cwd, f'.cache/ld-call-worker')
57
+
58
+ self._cache_dir = cache_dir
59
+ self._master_lock_path = os.path.join(cache_dir, f'{name}-master.lock')
60
+ self._worker_sock_path = os.path.join(cache_dir, f'{name}-worker.sock')
61
+
62
+ self._post_fork_funcs: 'List[Callable[[], None]]' = []
63
+ self._server_midwares: 'List[CallMidware[RES]]' = []
64
+ self._client_midwares: 'List[CallMidware[RES]]' = []
65
+
66
+ def name(self) -> str:
67
+ return self._name
68
+
69
+ def add_post_fork_func(self, func: 'Callable[[], None]'):
70
+ self._post_fork_funcs.append(func)
71
+
72
+ def add_server_midware(self, mw: 'CallMidware[RES]'):
73
+ self._server_midwares.append(mw)
74
+
75
+ def add_client_midware(self, mw: 'CallMidware[RES]'):
76
+ self._client_midwares.append(mw)
77
+
78
+ def _build_process_func(self, func: 'Callable[..., RES]', mws: 'List[CallMidware[RES]]'):
79
+ i = len(mws)
80
+ def get_next_func(next, mw):
81
+ return lambda *args, **kwargs: mw(*args, next=next, **kwargs)
82
+ while i > 0:
83
+ i -= 1
84
+ mw = mws[i]
85
+ func = get_next_func(func, mw)
86
+ return func
87
+
88
+
89
+
90
+ CMD_INIT = 0
91
+ CMD_CALL = 1
92
+
93
+
94
+ class CallRequest(object):
95
+ def __init__(self, cmd=CMD_CALL, args: 'List' = [], kwargs: 'Dict' = {}) -> None:
96
+ super().__init__()
97
+
98
+ self.command = cmd
99
+ self.args = args
100
+ self.kwargs = kwargs
101
+
102
+ @classmethod
103
+ def decode(cls, raw: 'bytes') -> 'CallRequest':
104
+ return PICKLER.loads(raw)
105
+
106
+ @classmethod
107
+ def encode(cls, obj: 'CallRequest') -> 'bytes':
108
+ raw = PICKLER.dumps(obj)
109
+ return bytes(raw)
110
+
111
+
112
+ class CallResponse(Generic[RES]):
113
+ def __init__(self, exc: 'Optional[Exception]' = None, res: 'Optional[RES]' = None) -> None:
114
+ super().__init__()
115
+
116
+ self.exc = exc
117
+ self.res = res
118
+
119
+ @classmethod
120
+ def decode(cls, raw: 'bytes') -> 'CallResponse[RES]':
121
+ return PICKLER.loads(raw)
122
+
123
+ @classmethod
124
+ def encode(cls, obj: 'CallResponse[RES]') -> 'bytes':
125
+ raw = PICKLER.dumps(obj)
126
+ return bytes(raw)
@@ -0,0 +1,49 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright (C) tanrenchong
5
+ #
6
+
7
+
8
+ import socket
9
+
10
+
11
+ PADDING = b'x'[0]
12
+
13
+ class Conn(object):
14
+ def __init__(self, sock: 'socket.socket') -> None:
15
+ self.sock = sock
16
+
17
+ def recv_exact(self, n: 'int'):
18
+ sock = self.sock
19
+ data = b''
20
+ while len(data) < n:
21
+ chunk = sock.recv(n - len(data))
22
+ if not chunk:
23
+ raise ConnectionError("connection has been closed")
24
+ data += chunk
25
+ return data
26
+
27
+ def recv(self):
28
+ """full message: 1 byte padding + 4 bytes size + raw"""
29
+ raw0 = self.recv_exact(1)
30
+ pad = raw0[0]
31
+ if pad != PADDING:
32
+ raise Exception(f'recv invalid padding. want:{PADDING}, got:{pad}')
33
+ raw1 = self.recv_exact(4)
34
+ size = int.from_bytes(raw1, 'big', signed=True)
35
+
36
+ raw = self.recv_exact(size)
37
+ return raw
38
+
39
+ def send(self, raw: bytes):
40
+ """full message: 1 byte padding + 4 bytes size + raw"""
41
+ raw0 = bytes([PADDING])
42
+ size = len(raw)
43
+ raw1 = size.to_bytes(4, 'big', signed=True)
44
+
45
+ buff = b''.join([raw0, raw1, raw])
46
+ self.sock.sendall(buff)
47
+
48
+ def close(self):
49
+ self.sock.close()
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: ldpy.base
3
+ Version: 0.0.1
4
+ Summary: No description
5
+ Author-email: distroy <distroy@live.com>
6
+ Requires-Dist: requests>=2.25.0
7
+ Requires-Dist: setproctitle>=1.3.7
8
+ Requires-Dist: pywin32>=300; sys_platform == "win32"
@@ -0,0 +1,19 @@
1
+ pyproject.toml
2
+ src/ldpy/__init__.py
3
+ src/ldpy.base.egg-info/PKG-INFO
4
+ src/ldpy.base.egg-info/SOURCES.txt
5
+ src/ldpy.base.egg-info/dependency_links.txt
6
+ src/ldpy.base.egg-info/requires.txt
7
+ src/ldpy.base.egg-info/top_level.txt
8
+ src/ldpy/base/__init__.py
9
+ src/ldpy/base/ldenv.py
10
+ src/ldpy/base/ldlog.py
11
+ src/ldpy/base/ldpath.py
12
+ src/ldpy/base/ldsync.py
13
+ src/ldpy/base/ldworker/__init__.py
14
+ src/ldpy/base/ldworker/call_client.py
15
+ src/ldpy/base/ldworker/call_master.py
16
+ src/ldpy/base/ldworker/call_service.py
17
+ src/ldpy/base/ldworker/call_test.py
18
+ src/ldpy/base/ldworker/call_worker.py
19
+ src/ldpy/base/ldworker/conn.py
@@ -0,0 +1,5 @@
1
+ requests>=2.25.0
2
+ setproctitle>=1.3.7
3
+
4
+ [:sys_platform == "win32"]
5
+ pywin32>=300