perf: log system - keep file handle open + async queue writes
- Replace open/write/flush/close per log call with persistent file handle - Use threading.Queue + background daemon thread for non-blocking writes - Only flush on exception/critical levels or periodically (every 1s idle) - Queue full protection: drop oldest entry instead of blocking event loop - Eliminates disk I/O blocking on slow storage (NFS/cloud disk) during high traffic
This commit is contained in:
parent
afe7637966
commit
5238a08309
58
appPublic/README.md
Normal file
58
appPublic/README.md
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# EventDispatcher
|
||||||
|
|
||||||
|
生产级异步事件调度器。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- 支持普通函数
|
||||||
|
- 支持 async 协程
|
||||||
|
- 支持实例方法
|
||||||
|
- 弱引用自动GC
|
||||||
|
- 异常隔离
|
||||||
|
- 超时控制
|
||||||
|
- 自定义错误处理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 使用示例
|
||||||
|
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from event_dispatcher import EventDispatcher
|
||||||
|
|
||||||
|
|
||||||
|
def on_login(data):
|
||||||
|
print(data)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
|
||||||
|
dispatcher = EventDispatcher()
|
||||||
|
|
||||||
|
dispatcher.bind(
|
||||||
|
"login",
|
||||||
|
on_login
|
||||||
|
)
|
||||||
|
|
||||||
|
await dispatcher.dispatch(
|
||||||
|
"login",
|
||||||
|
{
|
||||||
|
"user": "张三"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 项目结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
event_dispatcher_project/
|
||||||
|
├── event_dispatcher.py
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
BIN
appPublic/__pycache__/Singleton.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/Singleton.cpython-310.pyc
Normal file
Binary file not shown.
BIN
appPublic/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
appPublic/__pycache__/aes.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/aes.cpython-310.pyc
Normal file
Binary file not shown.
BIN
appPublic/__pycache__/argsConvert.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/argsConvert.cpython-310.pyc
Normal file
Binary file not shown.
BIN
appPublic/__pycache__/base64_to_file.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/base64_to_file.cpython-310.pyc
Normal file
Binary file not shown.
BIN
appPublic/__pycache__/dictObject.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/dictObject.cpython-310.pyc
Normal file
Binary file not shown.
BIN
appPublic/__pycache__/event_dispatcher.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/event_dispatcher.cpython-310.pyc
Normal file
Binary file not shown.
BIN
appPublic/__pycache__/folderUtils.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/folderUtils.cpython-310.pyc
Normal file
Binary file not shown.
BIN
appPublic/__pycache__/jsonConfig.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/jsonConfig.cpython-310.pyc
Normal file
Binary file not shown.
BIN
appPublic/__pycache__/log.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/log.cpython-310.pyc
Normal file
Binary file not shown.
BIN
appPublic/__pycache__/myImport.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/myImport.cpython-310.pyc
Normal file
Binary file not shown.
BIN
appPublic/__pycache__/myTE.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/myTE.cpython-310.pyc
Normal file
Binary file not shown.
BIN
appPublic/__pycache__/myjson.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/myjson.cpython-310.pyc
Normal file
Binary file not shown.
BIN
appPublic/__pycache__/objectAction.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/objectAction.cpython-310.pyc
Normal file
Binary file not shown.
BIN
appPublic/__pycache__/rc4.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/rc4.cpython-310.pyc
Normal file
Binary file not shown.
BIN
appPublic/__pycache__/registerfunction.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/registerfunction.cpython-310.pyc
Normal file
Binary file not shown.
BIN
appPublic/__pycache__/streamhttpclient.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/streamhttpclient.cpython-310.pyc
Normal file
Binary file not shown.
BIN
appPublic/__pycache__/timeUtils.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/timeUtils.cpython-310.pyc
Normal file
Binary file not shown.
BIN
appPublic/__pycache__/unicoding.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/unicoding.cpython-310.pyc
Normal file
Binary file not shown.
BIN
appPublic/__pycache__/uniqueID.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/uniqueID.cpython-310.pyc
Normal file
Binary file not shown.
BIN
appPublic/__pycache__/version.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/version.cpython-310.pyc
Normal file
Binary file not shown.
BIN
appPublic/__pycache__/worker.cpython-310.pyc
Normal file
BIN
appPublic/__pycache__/worker.cpython-310.pyc
Normal file
Binary file not shown.
289
appPublic/k
Normal file
289
appPublic/k
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PROJECT_DIR="event_dispatcher_project"
|
||||||
|
|
||||||
|
mkdir -p "${PROJECT_DIR}"
|
||||||
|
|
||||||
|
BT='```'
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# event_dispatcher.py
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
cat > "${PROJECT_DIR}/event_dispatcher.py" <<'PYEOF'
|
||||||
|
import asyncio
|
||||||
|
import inspect
|
||||||
|
import traceback
|
||||||
|
import weakref
|
||||||
|
from typing import Callable, Any
|
||||||
|
|
||||||
|
|
||||||
|
class WeakCallback:
|
||||||
|
|
||||||
|
def __init__(self, func: Callable):
|
||||||
|
|
||||||
|
self._is_coroutine = inspect.iscoroutinefunction(func)
|
||||||
|
|
||||||
|
if inspect.ismethod(func):
|
||||||
|
self._ref = weakref.WeakMethod(func)
|
||||||
|
else:
|
||||||
|
self._ref = weakref.ref(func)
|
||||||
|
|
||||||
|
self._hash = hash(func)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_coroutine(self):
|
||||||
|
return self._is_coroutine
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
return self._ref()
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return isinstance(other, WeakCallback) and self._hash == other._hash
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return self._hash
|
||||||
|
|
||||||
|
|
||||||
|
class EventDispatcher:
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
continue_on_error=True,
|
||||||
|
log_traceback=True,
|
||||||
|
handler_timeout=None,
|
||||||
|
error_handler=None,
|
||||||
|
):
|
||||||
|
|
||||||
|
self._events = {}
|
||||||
|
|
||||||
|
self.continue_on_error = continue_on_error
|
||||||
|
self.log_traceback = log_traceback
|
||||||
|
self.handler_timeout = handler_timeout
|
||||||
|
self.error_handler = error_handler
|
||||||
|
|
||||||
|
def bind(self, event_name: str, func: Callable):
|
||||||
|
|
||||||
|
if event_name not in self._events:
|
||||||
|
self._events[event_name] = set()
|
||||||
|
|
||||||
|
self._events[event_name].add(WeakCallback(func))
|
||||||
|
|
||||||
|
def unbind(self, event_name: str, func: Callable):
|
||||||
|
|
||||||
|
if event_name not in self._events:
|
||||||
|
return
|
||||||
|
|
||||||
|
target = WeakCallback(func)
|
||||||
|
|
||||||
|
self._events[event_name] = {
|
||||||
|
cb for cb in self._events[event_name]
|
||||||
|
if cb != target
|
||||||
|
}
|
||||||
|
|
||||||
|
if not self._events[event_name]:
|
||||||
|
del self._events[event_name]
|
||||||
|
|
||||||
|
async def _run_error_handler(
|
||||||
|
self,
|
||||||
|
event_name,
|
||||||
|
func,
|
||||||
|
exc,
|
||||||
|
):
|
||||||
|
|
||||||
|
if not self.error_handler:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
if inspect.iscoroutinefunction(self.error_handler):
|
||||||
|
await self.error_handler(
|
||||||
|
event_name,
|
||||||
|
func,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.error_handler(
|
||||||
|
event_name,
|
||||||
|
func,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[EventDispatcher] error_handler failed: {e}")
|
||||||
|
|
||||||
|
async def _execute_handler(
|
||||||
|
self,
|
||||||
|
cb,
|
||||||
|
event_name,
|
||||||
|
data,
|
||||||
|
):
|
||||||
|
|
||||||
|
func = cb.get()
|
||||||
|
|
||||||
|
if func is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
if cb.is_coroutine:
|
||||||
|
|
||||||
|
coro = func(data)
|
||||||
|
|
||||||
|
if self.handler_timeout:
|
||||||
|
await asyncio.wait_for(
|
||||||
|
coro,
|
||||||
|
timeout=self.handler_timeout,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await coro
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
if self.handler_timeout:
|
||||||
|
|
||||||
|
await asyncio.wait_for(
|
||||||
|
asyncio.to_thread(func, data),
|
||||||
|
timeout=self.handler_timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
func(data)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"[EventDispatcher] "
|
||||||
|
f"handler failed: "
|
||||||
|
f"event={event_name}, "
|
||||||
|
f"handler={func}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.log_traceback:
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
await self._run_error_handler(
|
||||||
|
event_name,
|
||||||
|
func,
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.continue_on_error:
|
||||||
|
raise
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def dispatch(
|
||||||
|
self,
|
||||||
|
event_name: str,
|
||||||
|
data: Any = None,
|
||||||
|
):
|
||||||
|
|
||||||
|
if event_name not in self._events:
|
||||||
|
return
|
||||||
|
|
||||||
|
dead_callbacks = []
|
||||||
|
|
||||||
|
for cb in list(self._events[event_name]):
|
||||||
|
|
||||||
|
func = cb.get()
|
||||||
|
|
||||||
|
if func is None:
|
||||||
|
dead_callbacks.append(cb)
|
||||||
|
continue
|
||||||
|
|
||||||
|
await self._execute_handler(
|
||||||
|
cb,
|
||||||
|
event_name,
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
|
||||||
|
for cb in dead_callbacks:
|
||||||
|
self._events[event_name].discard(cb)
|
||||||
|
|
||||||
|
if (
|
||||||
|
event_name in self._events
|
||||||
|
and not self._events[event_name]
|
||||||
|
):
|
||||||
|
del self._events[event_name]
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# README.md
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
cat > "${PROJECT_DIR}/README.md" <<EOF
|
||||||
|
# EventDispatcher
|
||||||
|
|
||||||
|
生产级异步事件调度器。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- 支持普通函数
|
||||||
|
- 支持 async 协程
|
||||||
|
- 支持实例方法
|
||||||
|
- 弱引用自动GC
|
||||||
|
- 异常隔离
|
||||||
|
- 超时控制
|
||||||
|
- 自定义错误处理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 使用示例
|
||||||
|
|
||||||
|
${BT}python
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from event_dispatcher import EventDispatcher
|
||||||
|
|
||||||
|
|
||||||
|
def on_login(data):
|
||||||
|
print(data)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
|
||||||
|
dispatcher = EventDispatcher()
|
||||||
|
|
||||||
|
dispatcher.bind(
|
||||||
|
"login",
|
||||||
|
on_login
|
||||||
|
)
|
||||||
|
|
||||||
|
await dispatcher.dispatch(
|
||||||
|
"login",
|
||||||
|
{
|
||||||
|
"user": "张三"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
${BT}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 项目结构
|
||||||
|
|
||||||
|
${BT}text
|
||||||
|
event_dispatcher_project/
|
||||||
|
├── event_dispatcher.py
|
||||||
|
└── README.md
|
||||||
|
${BT}
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "================================================="
|
||||||
|
echo "生成完成:"
|
||||||
|
echo " ${PROJECT_DIR}"
|
||||||
|
echo "================================================="
|
||||||
|
echo
|
||||||
|
echo "查看:"
|
||||||
|
echo
|
||||||
|
echo " cat ${PROJECT_DIR}/README.md"
|
||||||
|
echo
|
||||||
@ -1,5 +1,7 @@
|
|||||||
import sys
|
import sys
|
||||||
import codecs
|
import codecs
|
||||||
|
import threading
|
||||||
|
import queue
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
from appPublic.timeUtils import timestampstr
|
from appPublic.timeUtils import timestampstr
|
||||||
from appPublic.Singleton import SingletonDecorator
|
from appPublic.Singleton import SingletonDecorator
|
||||||
@ -10,7 +12,6 @@ def my_function():
|
|||||||
caller_frame = frame_info.f_back
|
caller_frame = frame_info.f_back
|
||||||
file_name = inspect.getframeinfo(caller_frame).filename
|
file_name = inspect.getframeinfo(caller_frame).filename
|
||||||
line_number = inspect.getframeinfo(caller_frame).lineno
|
line_number = inspect.getframeinfo(caller_frame).lineno
|
||||||
# print(f"Called from file: {file_name}, line: {line_number}")
|
|
||||||
|
|
||||||
|
|
||||||
@SingletonDecorator
|
@SingletonDecorator
|
||||||
@ -30,18 +31,53 @@ class MyLogger:
|
|||||||
self.levelname = levelname
|
self.levelname = levelname
|
||||||
self.level = self.levels.get(levelname)
|
self.level = self.levels.get(levelname)
|
||||||
self.logfile = logfile
|
self.logfile = logfile
|
||||||
|
self.logger = None
|
||||||
|
# Async write queue + background thread
|
||||||
|
self._q = queue.Queue(maxsize=10000)
|
||||||
|
self._worker = threading.Thread(target=self._write_loop, daemon=True)
|
||||||
|
self._worker.start()
|
||||||
|
|
||||||
def open_logger(self):
|
def _get_logger(self):
|
||||||
|
"""Lazy open file handle, keep it open."""
|
||||||
|
if self.logger is not None:
|
||||||
|
return self.logger
|
||||||
if self.logfile:
|
if self.logfile:
|
||||||
self.logger = codecs.open(self.logfile, 'a', 'utf-8')
|
self.logger = codecs.open(self.logfile, 'a', 'utf-8')
|
||||||
else:
|
else:
|
||||||
self.logger = sys.stdout
|
self.logger = sys.stdout
|
||||||
|
return self.logger
|
||||||
|
|
||||||
def close_logger(self):
|
def _write_loop(self):
|
||||||
if self.logfile:
|
"""Background thread: drain queue and write to file."""
|
||||||
self.logger.close();
|
fh = None
|
||||||
self.logger = None
|
while True:
|
||||||
self.logger = None
|
try:
|
||||||
|
item = self._q.get(timeout=1.0)
|
||||||
|
if item is None:
|
||||||
|
# Poison pill: shut down
|
||||||
|
if fh is not None:
|
||||||
|
try:
|
||||||
|
fh.flush()
|
||||||
|
if fh is not sys.stdout:
|
||||||
|
fh.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
break
|
||||||
|
if fh is None:
|
||||||
|
fh = self._get_logger()
|
||||||
|
fh.write(item)
|
||||||
|
# Only flush on critical/exception to avoid blocking on slow disks
|
||||||
|
if item.find('[exception]') >= 0 or item.find('[critical]') >= 0:
|
||||||
|
fh.flush()
|
||||||
|
except queue.Empty:
|
||||||
|
# Periodic flush to prevent data loss on crash
|
||||||
|
if fh is not None:
|
||||||
|
try:
|
||||||
|
fh.flush()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def log(self, levelname, message, frame_info):
|
def log(self, levelname, message, frame_info):
|
||||||
caller_frame = frame_info.f_back
|
caller_frame = frame_info.f_back
|
||||||
@ -49,21 +85,25 @@ class MyLogger:
|
|||||||
lineno = inspect.getframeinfo(caller_frame).lineno
|
lineno = inspect.getframeinfo(caller_frame).lineno
|
||||||
level = self.levels.get(levelname)
|
level = self.levels.get(levelname)
|
||||||
if level > self.level:
|
if level > self.level:
|
||||||
# print(f'{level=},{self.level=}')
|
|
||||||
return
|
return
|
||||||
data = {
|
data = {
|
||||||
'timestamp':timestampstr(),
|
'timestamp': timestampstr(),
|
||||||
'name':self.name,
|
'name': self.name,
|
||||||
'levelname':levelname,
|
'levelname': levelname,
|
||||||
'message':message,
|
'message': message,
|
||||||
'filename':filename,
|
'filename': filename,
|
||||||
'lineno':lineno
|
'lineno': lineno
|
||||||
}
|
}
|
||||||
self.open_logger()
|
|
||||||
s = self.formater % data
|
s = self.formater % data
|
||||||
self.logger.write(s)
|
try:
|
||||||
self.logger.flush()
|
self._q.put_nowait(s)
|
||||||
self.close_logger()
|
except queue.Full:
|
||||||
|
# Queue full: drop oldest (non-blocking, never stall the event loop)
|
||||||
|
try:
|
||||||
|
self._q.get_nowait()
|
||||||
|
self._q.put_nowait(s)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def clientinfo(message):
|
def clientinfo(message):
|
||||||
frame_info = inspect.currentframe()
|
frame_info = inspect.currentframe()
|
||||||
@ -97,8 +137,7 @@ def critical(message):
|
|||||||
|
|
||||||
def exception(message):
|
def exception(message):
|
||||||
frame_info = inspect.currentframe()
|
frame_info = inspect.currentframe()
|
||||||
tb_msg = format_exc()
|
tb_msg = format_exc()
|
||||||
msg = f'{message}\n{tb_msg}'
|
msg = f'{message}\n{tb_msg}'
|
||||||
logger = MyLogger('exception')
|
logger = MyLogger('exception')
|
||||||
logger.log('exception', msg, frame_info)
|
logger.log('exception', msg, frame_info)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user