linglong-web 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.
- linglong_web/__init__.py +132 -0
- linglong_web/__version__.py +9 -0
- linglong_web/core/__init__.py +54 -0
- linglong_web/core/auth.py +31 -0
- linglong_web/core/cacher.py +92 -0
- linglong_web/core/cluster_lock.py +128 -0
- linglong_web/core/config.py +196 -0
- linglong_web/core/constants.py +38 -0
- linglong_web/core/cors.py +52 -0
- linglong_web/core/db.py +12 -0
- linglong_web/core/ddl_manager.py +457 -0
- linglong_web/core/errors.py +118 -0
- linglong_web/core/http.py +305 -0
- linglong_web/core/limiter.py +55 -0
- linglong_web/core/limiter_local.py +84 -0
- linglong_web/core/py.typed +0 -0
- linglong_web/core/resource.py +676 -0
- linglong_web/core/response.py +86 -0
- linglong_web/core/router.py +57 -0
- linglong_web/core/scheduler.py +78 -0
- linglong_web/core/schemas.py +74 -0
- linglong_web/core/server.py +364 -0
- linglong_web/core/server_extensions.py +23 -0
- linglong_web/core/types.py +17 -0
- linglong_web/py.typed +0 -0
- linglong_web/utils/__init__.py +8 -0
- linglong_web/utils/async_read_write_lock.py +113 -0
- linglong_web/utils/context.py +58 -0
- linglong_web/utils/ddl_manager.py +1110 -0
- linglong_web/utils/log.py +84 -0
- linglong_web/utils/pj_struct.py +12 -0
- linglong_web/utils/signal_handler.py +227 -0
- linglong_web/utils/time.py +16 -0
- linglong_web-0.0.1.dist-info/METADATA +249 -0
- linglong_web-0.0.1.dist-info/RECORD +38 -0
- linglong_web-0.0.1.dist-info/WHEEL +5 -0
- linglong_web-0.0.1.dist-info/licenses/LICENSE +21 -0
- linglong_web-0.0.1.dist-info/top_level.txt +1 -0
linglong_web/__init__.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Linglong Web – 异步 FastAPI 工具集 / Asynchronous FastAPI toolkit."""
|
|
2
|
+
from .__version__ import (
|
|
3
|
+
__author__,
|
|
4
|
+
__description__,
|
|
5
|
+
__license__,
|
|
6
|
+
__version__,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from .core.auth import login_required
|
|
10
|
+
from .core.cacher import cacher
|
|
11
|
+
from .core.cluster_lock import cluster_lock
|
|
12
|
+
from .core.config import (
|
|
13
|
+
LinglongConfigBase,
|
|
14
|
+
LinglongConfig,
|
|
15
|
+
init_config,
|
|
16
|
+
)
|
|
17
|
+
from .core.constants import LinglongConst
|
|
18
|
+
from .core.cors import allow_cors_specific
|
|
19
|
+
from .core.db import TableBase
|
|
20
|
+
from .core.ddl_manager import (
|
|
21
|
+
AutoDDLManager,
|
|
22
|
+
DDLManagerConfig,
|
|
23
|
+
)
|
|
24
|
+
from .core.http import (
|
|
25
|
+
HTTPClientConfig,
|
|
26
|
+
AsyncHTTPClient,
|
|
27
|
+
LinglongHTTPError,
|
|
28
|
+
http_client,
|
|
29
|
+
)
|
|
30
|
+
from .core.limiter import LimiterGuard, limiter
|
|
31
|
+
from .core.limiter_local import (
|
|
32
|
+
limiter_local,
|
|
33
|
+
reset_limiter,
|
|
34
|
+
get_limiter_stats,
|
|
35
|
+
)
|
|
36
|
+
from .core.resource import (
|
|
37
|
+
ResourceManager,
|
|
38
|
+
Rmanager,
|
|
39
|
+
DEFAULT_DB_ALIAS,
|
|
40
|
+
init_resources,
|
|
41
|
+
close_resources,
|
|
42
|
+
)
|
|
43
|
+
from .core.response import (
|
|
44
|
+
APIError,
|
|
45
|
+
APIResponse,
|
|
46
|
+
build_api_response,
|
|
47
|
+
build_success_response,
|
|
48
|
+
build_error_response,
|
|
49
|
+
)
|
|
50
|
+
from .core.router import (
|
|
51
|
+
BaseRoute,
|
|
52
|
+
ServerRouter,
|
|
53
|
+
)
|
|
54
|
+
from .core.scheduler import (
|
|
55
|
+
BaseScheduler,
|
|
56
|
+
SchedulerGroup,
|
|
57
|
+
to_group,
|
|
58
|
+
)
|
|
59
|
+
from .core.server import LinglongAppServer
|
|
60
|
+
from .core.server_extensions import BaseServerExtension
|
|
61
|
+
from .core.schemas import (
|
|
62
|
+
PgsqlConfig,
|
|
63
|
+
RedisConfig,
|
|
64
|
+
RabbitMQConfig,
|
|
65
|
+
MongoConfig,
|
|
66
|
+
CeleryConfig,
|
|
67
|
+
ResourceInitConfig,
|
|
68
|
+
)
|
|
69
|
+
from .core.errors import (
|
|
70
|
+
ErrorCode,
|
|
71
|
+
ErrorMsg,
|
|
72
|
+
LinglongHTTPException,
|
|
73
|
+
LoginRequiredError,
|
|
74
|
+
LimiterError,
|
|
75
|
+
ClusterLockError,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
__all__ = [
|
|
79
|
+
"login_required",
|
|
80
|
+
"cacher",
|
|
81
|
+
"cluster_lock",
|
|
82
|
+
"LinglongConfigBase",
|
|
83
|
+
"LinglongConfig",
|
|
84
|
+
"LinglongConst",
|
|
85
|
+
"init_config",
|
|
86
|
+
"allow_cors_specific",
|
|
87
|
+
"TableBase",
|
|
88
|
+
"AutoDDLManager",
|
|
89
|
+
"DDLManagerConfig",
|
|
90
|
+
"BaseRoute",
|
|
91
|
+
"ServerRouter",
|
|
92
|
+
"APIError",
|
|
93
|
+
"APIResponse",
|
|
94
|
+
"build_api_response",
|
|
95
|
+
"build_success_response",
|
|
96
|
+
"build_error_response",
|
|
97
|
+
"HTTPClientConfig",
|
|
98
|
+
"AsyncHTTPClient",
|
|
99
|
+
"LinglongHTTPError",
|
|
100
|
+
"http_client",
|
|
101
|
+
"LimiterGuard",
|
|
102
|
+
"limiter",
|
|
103
|
+
"limiter_local",
|
|
104
|
+
"reset_limiter",
|
|
105
|
+
"get_limiter_stats",
|
|
106
|
+
"ResourceManager",
|
|
107
|
+
"Rmanager",
|
|
108
|
+
"DEFAULT_DB_ALIAS",
|
|
109
|
+
"init_resources",
|
|
110
|
+
"close_resources",
|
|
111
|
+
"BaseScheduler",
|
|
112
|
+
"SchedulerGroup",
|
|
113
|
+
"to_group",
|
|
114
|
+
"LinglongAppServer",
|
|
115
|
+
"BaseServerExtension",
|
|
116
|
+
"PgsqlConfig",
|
|
117
|
+
"RedisConfig",
|
|
118
|
+
"RabbitMQConfig",
|
|
119
|
+
"MongoConfig",
|
|
120
|
+
"CeleryConfig",
|
|
121
|
+
"ResourceInitConfig",
|
|
122
|
+
"ErrorCode",
|
|
123
|
+
"ErrorMsg",
|
|
124
|
+
"LinglongHTTPException",
|
|
125
|
+
"LoginRequiredError",
|
|
126
|
+
"LimiterError",
|
|
127
|
+
"ClusterLockError",
|
|
128
|
+
"__version__",
|
|
129
|
+
"__author__",
|
|
130
|
+
"__license__",
|
|
131
|
+
"__description__",
|
|
132
|
+
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Linglong Web metadata / 版本信息模块."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.0.1"
|
|
4
|
+
__author__ = "Victor Lai"
|
|
5
|
+
__license__ = "MIT"
|
|
6
|
+
__description__ = (
|
|
7
|
+
"Asynchronous FastAPI toolkit providing service bootstrap, registry hooks, resource orchestration, "
|
|
8
|
+
"and observability helpers"
|
|
9
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Linglong Web – 异步 FastAPI 工具集 / Asynchronous FastAPI toolkit."""
|
|
2
|
+
from .auth import login_required
|
|
3
|
+
from .cacher import cacher
|
|
4
|
+
from .cluster_lock import cluster_lock
|
|
5
|
+
from .config import (
|
|
6
|
+
LinglongConfigBase,
|
|
7
|
+
LinglongConfig,
|
|
8
|
+
LinglongConfigProxy,
|
|
9
|
+
init_config,
|
|
10
|
+
)
|
|
11
|
+
from .constants import LinglongConst
|
|
12
|
+
from .cors import allow_cors_specific
|
|
13
|
+
from .db import TableBase
|
|
14
|
+
from .ddl_manager import (
|
|
15
|
+
AutoDDLManager,
|
|
16
|
+
DDLManagerConfig,
|
|
17
|
+
)
|
|
18
|
+
from .http import (
|
|
19
|
+
HTTPClientConfig,
|
|
20
|
+
AsyncHTTPClient,
|
|
21
|
+
LinglongHTTPError,
|
|
22
|
+
http_client,
|
|
23
|
+
)
|
|
24
|
+
from .limiter import (
|
|
25
|
+
LimiterGuard,
|
|
26
|
+
limiter,
|
|
27
|
+
)
|
|
28
|
+
from .limiter_local import (
|
|
29
|
+
limiter_local,
|
|
30
|
+
reset_limiter,
|
|
31
|
+
get_limiter_stats,
|
|
32
|
+
)
|
|
33
|
+
from .resource import (
|
|
34
|
+
ResourceManager,
|
|
35
|
+
Rmanager,
|
|
36
|
+
DEFAULT_DB_ALIAS,
|
|
37
|
+
init_resources,
|
|
38
|
+
close_resources,
|
|
39
|
+
)
|
|
40
|
+
from .response import (
|
|
41
|
+
APIError,
|
|
42
|
+
APIResponse,
|
|
43
|
+
build_api_response,
|
|
44
|
+
build_success_response,
|
|
45
|
+
build_error_response,
|
|
46
|
+
)
|
|
47
|
+
from .router import BaseRoute, ServerRouter
|
|
48
|
+
from .scheduler import (
|
|
49
|
+
BaseScheduler,
|
|
50
|
+
SchedulerGroup,
|
|
51
|
+
to_group,
|
|
52
|
+
)
|
|
53
|
+
from .server import LinglongAppServer
|
|
54
|
+
from .server_extensions import BaseServerExtension
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Authentication helpers shared across Linglong services.
|
|
2
|
+
|
|
3
|
+
提供 Linglong 服务统一的认证装饰器工具。
|
|
4
|
+
"""
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import (
|
|
7
|
+
Awaitable,
|
|
8
|
+
Callable,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from .errors import LoginRequiredError
|
|
12
|
+
from .types import P, R
|
|
13
|
+
from ..utils.context import get_context_user_id
|
|
14
|
+
from ..utils.log import logger
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def login_required(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
|
|
18
|
+
"""Ensure the current request has a valid login user id.
|
|
19
|
+
|
|
20
|
+
确保当前请求已经完成登录校验,如果缺少用户信息则抛出 ``LoginRequiredError``。
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@wraps(func)
|
|
24
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
25
|
+
user_id = get_context_user_id()
|
|
26
|
+
if user_id is not None and isinstance(user_id, int):
|
|
27
|
+
return await func(*args, **kwargs)
|
|
28
|
+
logger.warning("user is not login")
|
|
29
|
+
raise LoginRequiredError()
|
|
30
|
+
|
|
31
|
+
return wrapper
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Redis based caching decorator for Linglong services.
|
|
2
|
+
|
|
3
|
+
提供基于 Redis 的协程缓存装饰器,支持自定义键后缀与按用户隔离。
|
|
4
|
+
"""
|
|
5
|
+
import hashlib
|
|
6
|
+
import inspect
|
|
7
|
+
import os
|
|
8
|
+
from functools import wraps
|
|
9
|
+
from typing import (
|
|
10
|
+
Any,
|
|
11
|
+
Awaitable,
|
|
12
|
+
Callable,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
import orjson
|
|
16
|
+
from redis import asyncio as aioredis
|
|
17
|
+
|
|
18
|
+
from .resource import Rmanager
|
|
19
|
+
from .types import P, R
|
|
20
|
+
from ..utils.context import get_context_user_id
|
|
21
|
+
from ..utils.log import logger
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _build_cache_key(
|
|
25
|
+
func: Callable[..., Any],
|
|
26
|
+
*,
|
|
27
|
+
add_str: str,
|
|
28
|
+
cache_by_user: bool,
|
|
29
|
+
) -> str:
|
|
30
|
+
"""Generate a stable cache key based on function source metadata.
|
|
31
|
+
|
|
32
|
+
结合函数文件路径、行号、模块与名称生成稳定的缓存键,可选追加自定义后缀与用户 ID。
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
source_file = inspect.getsourcefile(func)
|
|
36
|
+
filepath = os.path.abspath(source_file) if source_file else "N/A"
|
|
37
|
+
hash_hex = hashlib.md5(filepath.encode("utf-8"), usedforsecurity=False).hexdigest()
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
_, line_no = inspect.getsourcelines(func)
|
|
41
|
+
except (TypeError, OSError): # pragma: no cover - fallback when inspect fails
|
|
42
|
+
line_no = "N/A"
|
|
43
|
+
|
|
44
|
+
key_parts = [hash_hex, str(line_no), func.__module__, func.__name__]
|
|
45
|
+
if add_str:
|
|
46
|
+
key_parts.append(add_str)
|
|
47
|
+
if cache_by_user:
|
|
48
|
+
user_id = get_context_user_id()
|
|
49
|
+
if user_id:
|
|
50
|
+
key_parts.append(str(user_id))
|
|
51
|
+
return ":".join(key_parts)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def cacher(
|
|
55
|
+
expire_time: int,
|
|
56
|
+
add_str: str = "",
|
|
57
|
+
cache_by_user: bool = False,
|
|
58
|
+
) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]:
|
|
59
|
+
"""Cache coroutine results in Redis for a configurable TTL.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
expire_time: 缓存秒数 / TTL in seconds.
|
|
63
|
+
add_str: 自定义键后缀 / Optional suffix for more granularity.
|
|
64
|
+
cache_by_user: 是否按用户区分缓存 / Split cache entries by user id.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def decorator(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
|
|
68
|
+
@wraps(func)
|
|
69
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
70
|
+
if Rmanager.RedisPool is None:
|
|
71
|
+
logger.warning("redis cache is not available")
|
|
72
|
+
return await func(*args, **kwargs)
|
|
73
|
+
|
|
74
|
+
redis_client = aioredis.Redis(connection_pool=Rmanager.RedisPool)
|
|
75
|
+
cache_key = _build_cache_key(func, add_str=add_str, cache_by_user=cache_by_user)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
cached = await redis_client.get(cache_key)
|
|
79
|
+
if cached:
|
|
80
|
+
logger.debug("redis cache hit: %s", cache_key)
|
|
81
|
+
return orjson.loads(cached)
|
|
82
|
+
|
|
83
|
+
result = await func(*args, **kwargs)
|
|
84
|
+
await redis_client.setex(cache_key, expire_time, orjson.dumps(result))
|
|
85
|
+
return result
|
|
86
|
+
except Exception as exc: # noqa: BLE001
|
|
87
|
+
logger.error("redis cache error: %s", exc, exc_info=True)
|
|
88
|
+
return await func(*args, **kwargs)
|
|
89
|
+
|
|
90
|
+
return wrapper
|
|
91
|
+
|
|
92
|
+
return decorator
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Redis backed cluster wide lock helpers.
|
|
2
|
+
|
|
3
|
+
提供依赖 Redis 的集群锁装饰器,防止分布式并发执行同一段逻辑。
|
|
4
|
+
"""
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import (
|
|
7
|
+
Any,
|
|
8
|
+
Awaitable,
|
|
9
|
+
Callable,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
import redis.asyncio as redis
|
|
13
|
+
|
|
14
|
+
from .errors import ClusterLockError
|
|
15
|
+
from .resource import Rmanager
|
|
16
|
+
from .types import LockKeyBuilder, OnLockFail, P, R
|
|
17
|
+
from ..utils.log import logger
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_redis_client() -> redis.Redis | None:
|
|
21
|
+
"""Lazily build Redis client from the global pool.
|
|
22
|
+
|
|
23
|
+
从 Rmanager 注入的连接池创建 Redis 实例,若池未初始化则返回 ``None``。
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
if not Rmanager.RedisPool:
|
|
27
|
+
return None
|
|
28
|
+
return redis.Redis(connection_pool=Rmanager.RedisPool)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _compose_lock_key(
|
|
32
|
+
lock_prefix: str,
|
|
33
|
+
func: Callable[..., Any],
|
|
34
|
+
include_func_name: bool,
|
|
35
|
+
extra_key: str,
|
|
36
|
+
key_builder: LockKeyBuilder | None,
|
|
37
|
+
args: tuple[Any, ...],
|
|
38
|
+
kwargs: dict[str, Any],
|
|
39
|
+
) -> str:
|
|
40
|
+
"""Build a stable lock key with optional custom suffixes.
|
|
41
|
+
|
|
42
|
+
将锁前缀、函数名、自定义 key 与用户提供的 ``key_builder`` 结果拼接为最终键。
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
key_parts: list[str] = [lock_prefix]
|
|
46
|
+
if include_func_name:
|
|
47
|
+
key_parts.append(func.__name__)
|
|
48
|
+
if extra_key:
|
|
49
|
+
key_parts.append(extra_key)
|
|
50
|
+
if key_builder:
|
|
51
|
+
custom = key_builder(func, args, kwargs)
|
|
52
|
+
if custom:
|
|
53
|
+
key_parts.append(custom)
|
|
54
|
+
return ":".join(key_parts)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def cluster_lock(
|
|
58
|
+
lock_prefix: str,
|
|
59
|
+
*,
|
|
60
|
+
timeout_seconds: float = 60.0,
|
|
61
|
+
blocking_timeout_seconds: float | None = None,
|
|
62
|
+
extra_key: str = "",
|
|
63
|
+
include_func_name: bool = True,
|
|
64
|
+
key_builder: LockKeyBuilder | None = None,
|
|
65
|
+
error_message: str | None = None,
|
|
66
|
+
on_fail: OnLockFail | None = None,
|
|
67
|
+
) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]:
|
|
68
|
+
"""Decorate a coroutine to enforce Redis distributed locking.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
lock_prefix: 锁键前缀 / Prefix for Redis lock key.
|
|
72
|
+
timeout_seconds: 锁自动过期时间 / Lock expiration in seconds.
|
|
73
|
+
blocking_timeout_seconds: 获取锁的最大等待时间 / Max wait when acquiring.
|
|
74
|
+
extra_key: 自定义后缀 / Optional static suffix.
|
|
75
|
+
include_func_name: 是否包含函数名 / Append function name or not.
|
|
76
|
+
key_builder: 用户自定义键构造 / Custom builder for dynamic parts.
|
|
77
|
+
error_message: 获取失败提示 / Optional override error text.
|
|
78
|
+
on_fail: 获取失败回调 / Optional coroutine executed on contention.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
if timeout_seconds <= 0:
|
|
82
|
+
raise ValueError("timeout_seconds must be positive")
|
|
83
|
+
|
|
84
|
+
def decorator(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
|
|
85
|
+
@wraps(func)
|
|
86
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
87
|
+
redis_client = _get_redis_client()
|
|
88
|
+
if redis_client is None:
|
|
89
|
+
logger.warning("Redis pool is not initialized, skip cluster lock for %s", func.__name__)
|
|
90
|
+
return await func(*args, **kwargs)
|
|
91
|
+
|
|
92
|
+
lock_key = _compose_lock_key(
|
|
93
|
+
lock_prefix=lock_prefix,
|
|
94
|
+
func=func,
|
|
95
|
+
include_func_name=include_func_name,
|
|
96
|
+
extra_key=extra_key,
|
|
97
|
+
key_builder=key_builder,
|
|
98
|
+
args=args,
|
|
99
|
+
kwargs=kwargs,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
wait_timeout = blocking_timeout_seconds if blocking_timeout_seconds is not None else timeout_seconds
|
|
103
|
+
lock = redis_client.lock(lock_key, timeout=timeout_seconds)
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
acquired = await lock.acquire(blocking=True, blocking_timeout=wait_timeout)
|
|
107
|
+
except Exception as exc: # noqa: BLE001
|
|
108
|
+
logger.error("Failed to acquire cluster lock %s: %s", lock_key, exc)
|
|
109
|
+
raise
|
|
110
|
+
|
|
111
|
+
if not acquired:
|
|
112
|
+
message = error_message or f"Resource is locked by another worker: {lock_key}"
|
|
113
|
+
if on_fail:
|
|
114
|
+
return await on_fail(message, func, args, kwargs)
|
|
115
|
+
raise ClusterLockError(message)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
return await func(*args, **kwargs)
|
|
119
|
+
finally:
|
|
120
|
+
if lock.locked():
|
|
121
|
+
try:
|
|
122
|
+
await lock.release()
|
|
123
|
+
except Exception as exc: # noqa: BLE001
|
|
124
|
+
logger.error("Failed to release cluster lock %s: %s", lock_key, exc)
|
|
125
|
+
|
|
126
|
+
return wrapper
|
|
127
|
+
|
|
128
|
+
return decorator
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Linglong Web 配置代理 / Configuration proxy."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from typing import (
|
|
6
|
+
Any,
|
|
7
|
+
Dict,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from ..utils.pj_struct import Singleton
|
|
11
|
+
from ..utils.async_read_write_lock import AsyncReadWriteLock
|
|
12
|
+
from ..utils.log import logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LinglongConfigBase:
|
|
16
|
+
"""框架默认配置 / Default Linglong configuration."""
|
|
17
|
+
|
|
18
|
+
DEBUG = False
|
|
19
|
+
ENV_MODE = "prod"
|
|
20
|
+
|
|
21
|
+
PGSQL_USER = "postgres"
|
|
22
|
+
PGSQL_PASSWORD = "postgres123"
|
|
23
|
+
PGSQL_DB = ""
|
|
24
|
+
PGSQL_HOST = "postgres.internal"
|
|
25
|
+
PGSQL_PORT = "5432"
|
|
26
|
+
PGSQL_POOL_SIZE = 1
|
|
27
|
+
PGSQL_MAX_OVERFLOW = 5
|
|
28
|
+
PGSQL_DATABASES: Dict[str, Dict[str, Any]] | None = None
|
|
29
|
+
DDL_REQUIRED_EXTENSIONS = ("pgcrypto",)
|
|
30
|
+
|
|
31
|
+
MONGODB_URI = ""
|
|
32
|
+
MONGODB_DB = ""
|
|
33
|
+
MONGODB_COLLECTION = ""
|
|
34
|
+
|
|
35
|
+
REDIS_HOST = "redis.internal"
|
|
36
|
+
REDIS_PORT = 6379
|
|
37
|
+
REDIS_PASSWORD = ""
|
|
38
|
+
REDIS_MAXSIZE = None
|
|
39
|
+
|
|
40
|
+
ENABLE_WORKFLOW_CELERY = False
|
|
41
|
+
CELERY_BROKER_URL = None
|
|
42
|
+
CELERY_RESULT_BACKEND = None
|
|
43
|
+
CELERY_RESULT_BACKEND_DB = 0
|
|
44
|
+
CELERY_APP_NAME = "linglong_app"
|
|
45
|
+
CELERY_TASK_SERIALIZER = "json"
|
|
46
|
+
CELERY_ACCEPT_CONTENT = ["json"]
|
|
47
|
+
CELERY_RESULT_SERIALIZER = "json"
|
|
48
|
+
CELERY_TIMEZONE = "UTC"
|
|
49
|
+
CELERY_ENABLE_UTC = True
|
|
50
|
+
CELERY_WORKER_LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
51
|
+
CELERY_WORKER_TASK_LOG_FORMAT = (
|
|
52
|
+
"%(asctime)s - %(name)s - %(levelname)s - task_id=%(task_id)s task_name=%(task_name)s - %(message)s"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
LOGGING_LEVEL = logging.DEBUG
|
|
56
|
+
LOGGING_ENABLE_FILE_HANDLER = False
|
|
57
|
+
LOGGING_FILE_ADDR_FORMAT = "/opt/logs/server_app/app-{}.log"
|
|
58
|
+
LOGGING_FILE_MAX_BYTES = 10 * 1024 * 1024
|
|
59
|
+
LOGGING_FILE_BACKUP_COUNT = 10
|
|
60
|
+
LOG_CONFIG = {"exchange_name": "log.topic.exchange"}
|
|
61
|
+
|
|
62
|
+
LOG_PIPELINE_ENABLED = None
|
|
63
|
+
LOG_PIPELINE_IGNORE_LEVELS = []
|
|
64
|
+
LOG_RETENTION_DAYS = 7
|
|
65
|
+
LOG_RETENTION_BATCH_LIMIT = 2000
|
|
66
|
+
|
|
67
|
+
RABBITMQ_HOST = ""
|
|
68
|
+
RABBITMQ_PORT = 5672
|
|
69
|
+
RABBITMQ_VHOST = "/"
|
|
70
|
+
RABBITMQ_USERNAME = ""
|
|
71
|
+
RABBITMQ_PASSWORD = ""
|
|
72
|
+
RABBITMQ_LOG_EXCHANGE = "logs.topic.exchange"
|
|
73
|
+
RABBITMQ_LOG_QUEUE = "logs.business.infra.q"
|
|
74
|
+
RABBITMQ_LOG_BINDING_KEY = "logs.business.#"
|
|
75
|
+
RABBITMQ_LOG_PREFETCH = 200
|
|
76
|
+
RABBITMQ_LOG_ROUTING_TEMPLATE = "logs.business.{service}.{level}"
|
|
77
|
+
RABBITMQ_LOG_CONNECTION_NAME = ""
|
|
78
|
+
|
|
79
|
+
CORS_ALLOWED_ORIGINS = ["http://localhost"]
|
|
80
|
+
|
|
81
|
+
CELERY_WORKER_AUTOSTART = True
|
|
82
|
+
CELERY_WORKER_LOG_LEVEL = "INFO"
|
|
83
|
+
CELERY_WORKER_CONCURRENCY = 4
|
|
84
|
+
CELERY_WORKER_POOL = None
|
|
85
|
+
CELERY_WORKER_ENABLE_BEAT = True
|
|
86
|
+
CELERY_WORKER_HOSTNAME_SUFFIX = ".inline"
|
|
87
|
+
WORKFLOW_INLINE_MAX_CONCURRENCY = 4
|
|
88
|
+
WORKFLOW_INLINE_QUEUE_LIMIT = 1024
|
|
89
|
+
|
|
90
|
+
GRACEFUL_SHUTDOWN_TIMEOUT = 30
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class LinglongConfigProxy(Singleton):
|
|
94
|
+
"""配置代理,支持线程安全读写 / Config proxy with thread-safe caching."""
|
|
95
|
+
|
|
96
|
+
def __init__(self) -> None:
|
|
97
|
+
self._active_config_class: type[LinglongConfigBase] = LinglongConfigBase
|
|
98
|
+
self._lock = AsyncReadWriteLock()
|
|
99
|
+
self._config_cache: Dict[str, Any] = {}
|
|
100
|
+
self._cache_initialized = False
|
|
101
|
+
|
|
102
|
+
def _ensure_cache_initialized(self) -> None:
|
|
103
|
+
if self._cache_initialized:
|
|
104
|
+
return
|
|
105
|
+
with self._lock.write_locked():
|
|
106
|
+
if self._cache_initialized:
|
|
107
|
+
return
|
|
108
|
+
for key in dir(self._active_config_class):
|
|
109
|
+
if not key.startswith('_') and key.isupper():
|
|
110
|
+
self._config_cache[key] = getattr(self._active_config_class, key)
|
|
111
|
+
self._cache_initialized = True
|
|
112
|
+
|
|
113
|
+
def _set_active_config_class(self, config_class: type[LinglongConfigBase]) -> None:
|
|
114
|
+
with self._lock.write_locked():
|
|
115
|
+
self._active_config_class = config_class
|
|
116
|
+
self._config_cache.clear()
|
|
117
|
+
self._cache_initialized = False
|
|
118
|
+
self._ensure_cache_initialized()
|
|
119
|
+
|
|
120
|
+
def __getattr__(self, name: str) -> Any:
|
|
121
|
+
if name.startswith('_'):
|
|
122
|
+
return object.__getattribute__(self, name)
|
|
123
|
+
self._ensure_cache_initialized()
|
|
124
|
+
with self._lock.read_locked():
|
|
125
|
+
if name in self._config_cache:
|
|
126
|
+
return self._config_cache[name]
|
|
127
|
+
if hasattr(self._active_config_class, name):
|
|
128
|
+
value = getattr(self._active_config_class, name)
|
|
129
|
+
with self._lock.write_locked():
|
|
130
|
+
self._config_cache[name] = value
|
|
131
|
+
return value
|
|
132
|
+
raise AttributeError(f"Config has no attribute '{name}'")
|
|
133
|
+
|
|
134
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
135
|
+
if name.startswith('_'):
|
|
136
|
+
object.__setattr__(self, name, value)
|
|
137
|
+
return
|
|
138
|
+
self._ensure_cache_initialized()
|
|
139
|
+
with self._lock.write_locked():
|
|
140
|
+
self._config_cache[name] = value
|
|
141
|
+
setattr(self._active_config_class, name, value)
|
|
142
|
+
|
|
143
|
+
def apply_updates(self, updates: Dict[str, Any]) -> None:
|
|
144
|
+
"""批量更新配置值 / Apply a batch of configuration updates."""
|
|
145
|
+
|
|
146
|
+
if not updates:
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
self._ensure_cache_initialized()
|
|
150
|
+
with self._lock.write_locked():
|
|
151
|
+
for key, value in updates.items():
|
|
152
|
+
self._config_cache[key] = value
|
|
153
|
+
setattr(self._active_config_class, key, value)
|
|
154
|
+
|
|
155
|
+
def snapshot(self) -> Dict[str, Any]:
|
|
156
|
+
self._ensure_cache_initialized()
|
|
157
|
+
with self._lock.read_locked():
|
|
158
|
+
return self._config_cache.copy()
|
|
159
|
+
|
|
160
|
+
def reset(self) -> None:
|
|
161
|
+
with self._lock.write_locked():
|
|
162
|
+
self._config_cache.clear()
|
|
163
|
+
self._cache_initialized = False
|
|
164
|
+
self._ensure_cache_initialized()
|
|
165
|
+
|
|
166
|
+
def load_from_dict(self, config_dict: Dict[str, Any]) -> None:
|
|
167
|
+
"""从字典批量加载配置 / Load all config values from a dict."""
|
|
168
|
+
|
|
169
|
+
with self._lock.write_locked():
|
|
170
|
+
self._config_cache.clear()
|
|
171
|
+
self._config_cache.update(config_dict)
|
|
172
|
+
for key, value in config_dict.items():
|
|
173
|
+
setattr(self._active_config_class, key, value)
|
|
174
|
+
self._cache_initialized = True
|
|
175
|
+
self._ensure_cache_initialized()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
LinglongConfig = LinglongConfigProxy()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def init_config(config_dict: Dict[str, type[LinglongConfigBase]], mode_name: str | None = None) -> LinglongConfigProxy:
|
|
182
|
+
"""初始化配置 / Initialize config based on NE_CONFIG or provided mode."""
|
|
183
|
+
|
|
184
|
+
if mode_name is None:
|
|
185
|
+
mode_name = os.getenv('LINGLONG_CONFIG', 'prod')
|
|
186
|
+
logger.info("Linglong config mode not specified, using NE_CONFIG: %s", mode_name)
|
|
187
|
+
|
|
188
|
+
config_cls = config_dict.get(mode_name)
|
|
189
|
+
if config_cls is None:
|
|
190
|
+
raise ValueError(
|
|
191
|
+
f"Config mode '{mode_name}' not found in config_dict. Available: {list(config_dict.keys())}"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
LinglongConfig._set_active_config_class(config_cls)
|
|
195
|
+
logger.info("Linglong Config initialized with mode: %s, class: %s", mode_name, config_cls.__name__)
|
|
196
|
+
return LinglongConfig
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Linglong Web 常量定义 / Linglong Web constants."""
|
|
2
|
+
import http
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
DEFAULT_DB_ALIAS = "default"
|
|
7
|
+
|
|
8
|
+
class HeaderKey(StrEnum):
|
|
9
|
+
"""请求头键常量 / HTTP header keys"""
|
|
10
|
+
|
|
11
|
+
REQUEST_ID = "x-linglong-reqid"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Environment(StrEnum):
|
|
15
|
+
"""运行环境常量 / Deployment environment constants"""
|
|
16
|
+
|
|
17
|
+
DEVELOPMENT = "development"
|
|
18
|
+
PRODUCTION = "production"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LinglongConst:
|
|
22
|
+
"""框架级别常量集合 / Framework-wide constants."""
|
|
23
|
+
|
|
24
|
+
OID_HEADER_KEY = HeaderKey.REQUEST_ID
|
|
25
|
+
HTTP_METHODS = tuple(method.value for method in http.HTTPMethod)
|
|
26
|
+
ENVIRONMENT = Environment
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def is_http_method_supported(cls, method: str) -> bool:
|
|
30
|
+
"""判断 HTTP Method 是否被支持 / Check if HTTP method is allowed."""
|
|
31
|
+
|
|
32
|
+
return method in cls.HTTP_METHODS
|
|
33
|
+
|
|
34
|
+
# 注意:linglong 只维护与 Web 框架基础能力相关的常量。
|
|
35
|
+
# Note: linglong keeps only core web-framework constants.
|
|
36
|
+
#
|
|
37
|
+
# 类似“服务注册 / 健康检查 / 远端配置 / 容器 hostname 判定”等微服务框架能力
|
|
38
|
+
# 应由 cancan 统一内聚管理,禁止通过调用方反向注入到 linglong。
|