jiayi.xie Claude Opus 4.8 (1M context) commited on
Commit
a33013a
·
1 Parent(s): 29b5866

Make blocking threadpool size configurable to lift image concurrency

Browse files

All blocking endpoints (image gen, text, content filter) run via
run_in_threadpool on anyio's default thread limiter, capped at 40. That
40 was the hard ceiling on simultaneous in-flight image generations
regardless of how many accounts (each accounts x image_account_concurrency
slot) the pool had.

Add config.blocking_threadpool_size (env BLOCKING_THREADPOOL_SIZE or
config.json, floor 40 / ceil 2048) and raise the limiter in the app
lifespan so it takes effect inside the event loop. Image gen is
I/O-bound, so a few hundred threads cost little on modest hardware; the
real ceiling then becomes accounts x concurrency and upstream rate
limits rather than the runtime.

Bake BLOCKING_THREADPOOL_SIZE=128 into the Space image as the default;
override via a Space Variable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files changed (3) hide show
  1. Dockerfile +2 -1
  2. api/app.py +16 -0
  3. services/config.py +15 -0
Dockerfile CHANGED
@@ -22,7 +22,8 @@ ARG TARGETARCH
22
  ENV PYTHONDONTWRITEBYTECODE=1 \
23
  PYTHONUNBUFFERED=1 \
24
  UV_LINK_MODE=copy \
25
- HOME=/app
 
26
 
27
  WORKDIR /app
28
 
 
22
  ENV PYTHONDONTWRITEBYTECODE=1 \
23
  PYTHONUNBUFFERED=1 \
24
  UV_LINK_MODE=copy \
25
+ HOME=/app \
26
+ BLOCKING_THREADPOOL_SIZE=128
27
 
28
  WORKDIR /app
29
 
api/app.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
  from contextlib import asynccontextmanager
4
  from threading import Event
5
 
 
6
  from fastapi import FastAPI, HTTPException
7
  from fastapi.middleware.cors import CORSMiddleware
8
  from fastapi.responses import FileResponse
@@ -12,6 +13,20 @@ from api import accounts, ai, image_tasks, openai_keys, register, system
12
  from api.support import resolve_web_asset, start_limited_account_watcher
13
  from services.backup_service import backup_service
14
  from services.config import config
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
 
17
  def create_app() -> FastAPI:
@@ -19,6 +34,7 @@ def create_app() -> FastAPI:
19
 
20
  @asynccontextmanager
21
  async def lifespan(_: FastAPI):
 
22
  stop_event = Event()
23
  thread = start_limited_account_watcher(stop_event)
24
  backup_service.start()
 
3
  from contextlib import asynccontextmanager
4
  from threading import Event
5
 
6
+ import anyio
7
  from fastapi import FastAPI, HTTPException
8
  from fastapi.middleware.cors import CORSMiddleware
9
  from fastapi.responses import FileResponse
 
13
  from api.support import resolve_web_asset, start_limited_account_watcher
14
  from services.backup_service import backup_service
15
  from services.config import config
16
+ from utils.log import logger
17
+
18
+
19
+ def _apply_blocking_threadpool_size() -> None:
20
+ # 抬高 anyio run_in_threadpool 线程池上限(默认 40),解除生图/文本接口的同时并发硬顶。
21
+ # 必须在事件循环内调用(limiter 绑定当前 async backend),故放在 lifespan 启动里。
22
+ size = config.blocking_threadpool_size
23
+ try:
24
+ limiter = anyio.to_thread.current_default_thread_limiter()
25
+ if limiter.total_tokens != size:
26
+ limiter.total_tokens = size
27
+ logger.info({"event": "blocking_threadpool_configured", "total_tokens": limiter.total_tokens})
28
+ except Exception as exc:
29
+ logger.warning({"event": "blocking_threadpool_config_failed", "error": str(exc)})
30
 
31
 
32
  def create_app() -> FastAPI:
 
34
 
35
  @asynccontextmanager
36
  async def lifespan(_: FastAPI):
37
+ _apply_blocking_threadpool_size()
38
  stop_event = Event()
39
  thread = start_limited_account_watcher(stop_event)
40
  backup_service.start()
services/config.py CHANGED
@@ -224,6 +224,20 @@ class ConfigStore:
224
  except (TypeError, ValueError):
225
  return 3
226
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  @property
228
  def auto_remove_invalid_accounts(self) -> bool:
229
  value = self.data.get("auto_remove_invalid_accounts", False)
@@ -310,6 +324,7 @@ class ConfigStore:
310
  data["image_retention_days"] = self.image_retention_days
311
  data["image_poll_timeout_secs"] = self.image_poll_timeout_secs
312
  data["image_account_concurrency"] = self.image_account_concurrency
 
313
  data["auto_remove_invalid_accounts"] = self.auto_remove_invalid_accounts
314
  data["auto_remove_rate_limited_accounts"] = self.auto_remove_rate_limited_accounts
315
  data["log_levels"] = self.log_levels
 
224
  except (TypeError, ValueError):
225
  return 3
226
 
227
+ @property
228
+ def blocking_threadpool_size(self) -> int:
229
+ # 所有阻塞型接口(生图/文本/内容过滤)都跑在 anyio 的 run_in_threadpool 线程池里,
230
+ # 默认只有 40,是生图同时并发的硬顶(见 conversation.stream_image_outputs_with_pool)。
231
+ # 这里允许通过 env BLOCKING_THREADPOOL_SIZE 或 config.json 抬高;I/O 密集,2C16G 抬到
232
+ # 一两百只占很少内存,真正天花板会让位给 账号数×image_account_concurrency 与上游限流。
233
+ raw = os.getenv("BLOCKING_THREADPOOL_SIZE") or self.data.get("blocking_threadpool_size") or 40
234
+ try:
235
+ value = int(raw)
236
+ except (TypeError, ValueError):
237
+ value = 40
238
+ # 不低于 anyio 默认 40(避免误降并发),上限防手滑打爆内存。
239
+ return max(40, min(2048, value))
240
+
241
  @property
242
  def auto_remove_invalid_accounts(self) -> bool:
243
  value = self.data.get("auto_remove_invalid_accounts", False)
 
324
  data["image_retention_days"] = self.image_retention_days
325
  data["image_poll_timeout_secs"] = self.image_poll_timeout_secs
326
  data["image_account_concurrency"] = self.image_account_concurrency
327
+ data["blocking_threadpool_size"] = self.blocking_threadpool_size
328
  data["auto_remove_invalid_accounts"] = self.auto_remove_invalid_accounts
329
  data["auto_remove_rate_limited_accounts"] = self.auto_remove_rate_limited_accounts
330
  data["log_levels"] = self.log_levels