sage/bin/backend_accounting.py
yumoqing 3de5a1ce91 feat: multi-process architecture with independent backend processes
- Extract backend_accounting from llmage cleanupctx to independent process
- Add bin/backend_accounting.py for standalone LLM billing loop
- Rewrite start.sh with two-phase startup:
  1. Independent backend programs (run once)
  2. Sage Web workers (SO_REUSEPORT on same port)
- Rewrite stop.sh to handle both workers and backend processes
- Add .gitignore for build artifacts and runtime files

Architecture:
- CPU core detection for worker count
- All workers share port 9180 via SO_REUSEPORT
- Backend processes tracked in sage_backend.pid
- Workers tracked in sage.pid
2026-05-17 00:11:53 +08:00

80 lines
2.4 KiB
Python

#!/usr/bin/env python
"""
独立运行的LLM后台计费程序。
从 sage.py 的 llmage 模块中提取,避免多进程模式下重复运行。
"""
import os
import sys
import asyncio
import signal
# 切换到 Sage 工作目录
os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'py3', 'lib', 'python3.10', 'site-packages'))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'pkgs'))
from appPublic.folderUtils import ProgramPath
from appPublic.jsonConfig import getConfig
from sqlor.dbpools import DBPools
from appPublic.log import MyLogger, debug, exception, info
# 初始化配置
p = ProgramPath()
config = getConfig(NS={'workdir': os.getcwd(), 'ProgramPath': p})
DBPools(config.databases)
# 导入 llmage 的计费函数
from llmage.accounting import (
get_accounting_llmusages,
llm_accounting,
llm_accoung_failed
)
async def backend_accounting():
"""LLM 使用计费循环"""
info('backend accounting started ...')
while True:
try:
lus = await get_accounting_llmusages()
except Exception as e:
exception(f'{e}')
lus = []
debug(f'{len(lus)=} need to accounting........')
for lu in lus:
try:
debug(f'backend_accounting(): {lu.id=} handleing...')
await llm_accounting(lu)
except Exception as e:
exception(f'{e}, {lu.id=}')
await llm_accoung_failed(lu.id)
await asyncio.sleep(10)
def main():
logger = MyLogger('backend_accounting', levelname='info',
logfile=os.path.join(os.getcwd(), 'logs', 'backend_accounting.log'))
info(f'Backend accounting process started (PID: {os.getpid()})')
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
def handle_signal(signum, frame):
info(f'Received signal {signum}, shutting down...')
for task in asyncio.all_tasks(loop):
task.cancel()
loop.stop()
signal.signal(signal.SIGTERM, handle_signal)
signal.signal(signal.SIGINT, handle_signal)
try:
loop.run_until_complete(backend_accounting())
except asyncio.CancelledError:
pass
finally:
loop.close()
info('Backend accounting process stopped.')
if __name__ == '__main__':
main()