ldpy.base 0.0.1__py3-none-any.whl
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.
- ldpy/__init__.py +8 -0
- ldpy/base/__init__.py +7 -0
- ldpy/base/ldenv.py +62 -0
- ldpy/base/ldlog.py +50 -0
- ldpy/base/ldpath.py +29 -0
- ldpy/base/ldsync.py +147 -0
- ldpy/base/ldworker/__init__.py +61 -0
- ldpy/base/ldworker/call_client.py +242 -0
- ldpy/base/ldworker/call_master.py +236 -0
- ldpy/base/ldworker/call_service.py +165 -0
- ldpy/base/ldworker/call_test.py +58 -0
- ldpy/base/ldworker/call_worker.py +126 -0
- ldpy/base/ldworker/conn.py +49 -0
- ldpy_base-0.0.1.dist-info/METADATA +8 -0
- ldpy_base-0.0.1.dist-info/RECORD +17 -0
- ldpy_base-0.0.1.dist-info/WHEEL +5 -0
- ldpy_base-0.0.1.dist-info/top_level.txt +1 -0
ldpy/__init__.py
ADDED
ldpy/base/__init__.py
ADDED
ldpy/base/ldenv.py
ADDED
|
@@ -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
|
ldpy/base/ldlog.py
ADDED
|
@@ -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
|
ldpy/base/ldpath.py
ADDED
|
@@ -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}')
|
ldpy/base/ldsync.py
ADDED
|
@@ -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,17 @@
|
|
|
1
|
+
ldpy/__init__.py,sha256=Y-njSLV1W3Mbdh21wNcFytFIGkQzD98XBEYsSChBkXs,143
|
|
2
|
+
ldpy/base/__init__.py,sha256=dVaLmNrWWMZTf84uo3ZkD0dVXevQhZK-aMHX8mpHddM,78
|
|
3
|
+
ldpy/base/ldenv.py,sha256=MVc8llqrePByFy2jEopCQaC122weVj7INrLUinA8LUk,1125
|
|
4
|
+
ldpy/base/ldlog.py,sha256=y-pT2AHH8QvZ2s-Xi-xSwAQoybKZzmz-z1ZtGhrrITc,1503
|
|
5
|
+
ldpy/base/ldpath.py,sha256=NAuW0ZZh5rrEGLOOZZcyZvJ419-1yFrzCdiTQBrtffA,698
|
|
6
|
+
ldpy/base/ldsync.py,sha256=T30t4pFB7V9VQNmXa58PKaahCMFM-LOMeXwRXy2ID40,3234
|
|
7
|
+
ldpy/base/ldworker/__init__.py,sha256=k3R3uQTUMRjPVraHFLC-zgBb0xYCwrhz027K9ARvSCQ,1479
|
|
8
|
+
ldpy/base/ldworker/call_client.py,sha256=C0INbRiOCCzcFTbKve_Ar1_hfAZYByH0ey-rU-Y-YHg,7722
|
|
9
|
+
ldpy/base/ldworker/call_master.py,sha256=hit9t5L28dm1RNHHElwXdmqe2Dotes22iIvzZOVYKOE,6443
|
|
10
|
+
ldpy/base/ldworker/call_service.py,sha256=JWAlqqxDIIAPBvgn3kFg42AaruZrw1-sX5xDBcap5Po,4856
|
|
11
|
+
ldpy/base/ldworker/call_test.py,sha256=XxsyGGK9lM7YLZEOyt6OVDz8c5UJQOCdwxo86FLVhjE,1522
|
|
12
|
+
ldpy/base/ldworker/call_worker.py,sha256=iA_gHeZUr2v3WRcdF0jNadu7-unV6QlNfbNaj8zefGg,3506
|
|
13
|
+
ldpy/base/ldworker/conn.py,sha256=RhwzlQy7_RHLovNk4Y1C7koJWqBgNuJkf4HJvDUOJfM,1235
|
|
14
|
+
ldpy_base-0.0.1.dist-info/METADATA,sha256=yBk2Fs_J5iqb8brTdGxDxTSKP6kASOs02ig6ovYBB0E,238
|
|
15
|
+
ldpy_base-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
16
|
+
ldpy_base-0.0.1.dist-info/top_level.txt,sha256=dkG_F4jYkh6CS6gdpQeV_gKW3yaJBvl1ukUu0zo2KA8,5
|
|
17
|
+
ldpy_base-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ldpy
|