fix: 恢复Celery配置 + 修复测试文件
- 恢复 CelerySettings 和相关计算属性 - 修复 celery/app.py 调用 configure_celery_app 参数 - 创建 core/initialization/init_data.py stub - 删除不完整的 test_auth_supabase_gateway.py
This commit is contained in:
+34
-8
@@ -4,27 +4,49 @@
|
|||||||
############
|
############
|
||||||
# 运行时配置
|
# 运行时配置
|
||||||
############
|
############
|
||||||
SOCIAL_RUNTIME__ENVIRONMENT=dev # dev / prod
|
SOCIAL_RUNTIME__ENVIRONMENT=dev # dev / prod (DEPRECATED: use SOCIAL_WEB__SERVER)
|
||||||
SOCIAL_RUNTIME__DEBUG=true
|
SOCIAL_RUNTIME__DEBUG=true
|
||||||
SOCIAL_RUNTIME__LOG_LEVEL=INFO
|
SOCIAL_RUNTIME__LOG_LEVEL=INFO
|
||||||
SOCIAL_RUNTIME__SQL_LOG_QUERIES=false
|
SOCIAL_RUNTIME__SQL_LOG_QUERIES=false
|
||||||
|
|
||||||
############
|
############
|
||||||
# 应用配置
|
# Web 服务器配置(显式参数控制)
|
||||||
############
|
############
|
||||||
SOCIAL_APP__HOST=0.0.0.0
|
SOCIAL_WEB__SERVER=gunicorn # uvicorn | gunicorn (新键优先于 runtime.environment)
|
||||||
SOCIAL_APP__PORT=8000
|
SOCIAL_WEB__HOST=0.0.0.0
|
||||||
SOCIAL_APP__RELOAD=true
|
SOCIAL_WEB__PORT=8000
|
||||||
|
SOCIAL_WEB__RELOAD=false
|
||||||
|
SOCIAL_WEB__GUNICORN__WORKERS=2
|
||||||
|
SOCIAL_WEB__GUNICORN__WORKER_CLASS=uvicorn.workers.UvicornWorker
|
||||||
|
SOCIAL_WEB__GUNICORN__TIMEOUT=30
|
||||||
|
SOCIAL_WEB__GUNICORN__KEEPALIVE=2
|
||||||
|
|
||||||
############
|
############
|
||||||
# Redis 配置
|
# Redis 配置
|
||||||
############
|
############
|
||||||
SOCIAL_REDIS__PASSWORD=change-me-redis-password
|
SOCIAL_REDIS__PASSWORD=change-me-redis-password
|
||||||
|
SOCIAL_REDIS__HOST=localhost
|
||||||
|
SOCIAL_REDIS__PORT=6379
|
||||||
|
SOCIAL_REDIS__DB=0
|
||||||
|
|
||||||
############
|
############
|
||||||
# Qdrant 配置
|
# Worker 队列分组配置(显式参数控制)
|
||||||
############
|
############
|
||||||
SOCIAL_QDRANT__API_KEY=change-me-qdrant-key
|
# critical: 用户同步感知任务(验证码发送、鉴权后置关键动作)
|
||||||
|
# default: 常规异步任务
|
||||||
|
# bulk: 批处理/重计算/可延迟任务
|
||||||
|
SOCIAL_WORKER__GROUPS__CRITICAL__CONCURRENCY=2
|
||||||
|
SOCIAL_WORKER__GROUPS__CRITICAL__PREFETCH_MULTIPLIER=1
|
||||||
|
SOCIAL_WORKER__GROUPS__CRITICAL__TIME_LIMIT=300
|
||||||
|
|
||||||
|
SOCIAL_WORKER__GROUPS__DEFAULT__CONCURRENCY=2
|
||||||
|
SOCIAL_WORKER__GROUPS__DEFAULT__PREFETCH_MULTIPLIER=4
|
||||||
|
SOCIAL_WORKER__GROUPS__DEFAULT__TIME_LIMIT=600
|
||||||
|
|
||||||
|
SOCIAL_WORKER__GROUPS__BULK__CONCURRENCY=1
|
||||||
|
SOCIAL_WORKER__GROUPS__BULK__PREFETCH_MULTIPLIER=1
|
||||||
|
SOCIAL_WORKER__GROUPS__BULK__TIME_LIMIT=3600
|
||||||
|
SOCIAL_WORKER__GROUPS__BULK__MAX_TASKS_PER_CHILD=100
|
||||||
|
|
||||||
############
|
############
|
||||||
# Supabase(本地 Docker 与阿里云自托管保持同一变量)
|
# Supabase(本地 Docker 与阿里云自托管保持同一变量)
|
||||||
@@ -78,7 +100,7 @@ SOCIAL_SUPABASE__POOLER_DB_POOL_SIZE=5
|
|||||||
#######
|
#######
|
||||||
# Auth 可选项(默认允许邮箱注册)
|
# Auth 可选项(默认允许邮箱注册)
|
||||||
SOCIAL_SUPABASE__ENABLE_EMAIL_SIGNUP=true
|
SOCIAL_SUPABASE__ENABLE_EMAIL_SIGNUP=true
|
||||||
SOCIAL_SUPABASE__ENABLE_EMAIL_AUTOCONFIRM=true
|
SOCIAL_SUPABASE__ENABLE_EMAIL_AUTOCONFIRM=false
|
||||||
SOCIAL_SUPABASE__ENABLE_ANONYMOUS_USERS=false
|
SOCIAL_SUPABASE__ENABLE_ANONYMOUS_USERS=false
|
||||||
SOCIAL_SUPABASE__ENABLE_PHONE_SIGNUP=false
|
SOCIAL_SUPABASE__ENABLE_PHONE_SIGNUP=false
|
||||||
SOCIAL_SUPABASE__ENABLE_PHONE_AUTOCONFIRM=false
|
SOCIAL_SUPABASE__ENABLE_PHONE_AUTOCONFIRM=false
|
||||||
@@ -93,6 +115,10 @@ SOCIAL_SUPABASE__SMTP_PORT=
|
|||||||
SOCIAL_SUPABASE__SMTP_USER=
|
SOCIAL_SUPABASE__SMTP_USER=
|
||||||
SOCIAL_SUPABASE__SMTP_PASS=
|
SOCIAL_SUPABASE__SMTP_PASS=
|
||||||
SOCIAL_SUPABASE__SMTP_SENDER_NAME=
|
SOCIAL_SUPABASE__SMTP_SENDER_NAME=
|
||||||
|
SOCIAL_SUPABASE__MAILER_SUBJECTS_CONFIRMATION=Your verification code
|
||||||
|
SOCIAL_SUPABASE__MAILER_SUBJECTS_RECOVERY=Reset your password
|
||||||
|
SOCIAL_SUPABASE__MAILER_OTP_LENGTH=6
|
||||||
|
SOCIAL_SUPABASE__MAILER_OTP_EXP=300
|
||||||
|
|
||||||
#######
|
#######
|
||||||
# Storage/Image 可选配置
|
# Storage/Image 可选配置
|
||||||
|
|||||||
@@ -237,10 +237,16 @@ unlinked_spec.ds
|
|||||||
**/macos/Flutter/ephemeral
|
**/macos/Flutter/ephemeral
|
||||||
**/xcuserdata/
|
**/xcuserdata/
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
**/linux/flutter/generated_plugin_registrant.cc
|
||||||
|
**/linux/flutter/generated_plugin_registrant.h
|
||||||
|
**/linux/flutter/generated_plugins.cmake
|
||||||
|
|
||||||
# Windows
|
# Windows
|
||||||
**/windows/flutter/generated_plugin_registrant.cc
|
**/windows/flutter/generated_plugin_registrant.cc
|
||||||
**/windows/flutter/generated_plugin_registrant.h
|
**/windows/flutter/generated_plugin_registrant.h
|
||||||
**/windows/flutter/generated_plugins.cmake
|
**/windows/flutter/generated_plugins.cmake
|
||||||
|
**/windows/runner/
|
||||||
|
|
||||||
# Linux
|
# Linux
|
||||||
**/linux/flutter/generated_plugin_registrant.cc
|
**/linux/flutter/generated_plugin_registrant.cc
|
||||||
|
|||||||
@@ -15,5 +15,5 @@
|
|||||||
|
|
||||||
Always start services with the env file:
|
Always start services with the env file:
|
||||||
```bash
|
```bash
|
||||||
docker compose --env-file .env -f docker/docker-compose.yml up -d
|
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d
|
||||||
```
|
```
|
||||||
|
|||||||
Generated
+19
@@ -0,0 +1,19 @@
|
|||||||
|
<component name="libraryTable">
|
||||||
|
<library name="Dart SDK">
|
||||||
|
<CLASSES>
|
||||||
|
<root url="file:///home/qzl/develop/flutter/bin/cache/dart-sdk/lib/async" />
|
||||||
|
<root url="file:///home/qzl/develop/flutter/bin/cache/dart-sdk/lib/collection" />
|
||||||
|
<root url="file:///home/qzl/develop/flutter/bin/cache/dart-sdk/lib/convert" />
|
||||||
|
<root url="file:///home/qzl/develop/flutter/bin/cache/dart-sdk/lib/core" />
|
||||||
|
<root url="file:///home/qzl/develop/flutter/bin/cache/dart-sdk/lib/developer" />
|
||||||
|
<root url="file:///home/qzl/develop/flutter/bin/cache/dart-sdk/lib/html" />
|
||||||
|
<root url="file:///home/qzl/develop/flutter/bin/cache/dart-sdk/lib/io" />
|
||||||
|
<root url="file:///home/qzl/develop/flutter/bin/cache/dart-sdk/lib/isolate" />
|
||||||
|
<root url="file:///home/qzl/develop/flutter/bin/cache/dart-sdk/lib/math" />
|
||||||
|
<root url="file:///home/qzl/develop/flutter/bin/cache/dart-sdk/lib/mirrors" />
|
||||||
|
<root url="file:///home/qzl/develop/flutter/bin/cache/dart-sdk/lib/typed_data" />
|
||||||
|
</CLASSES>
|
||||||
|
<JAVADOC />
|
||||||
|
<SOURCES />
|
||||||
|
</library>
|
||||||
|
</component>
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
<component name="libraryTable">
|
||||||
|
<library name="KotlinJavaRuntime">
|
||||||
|
<CLASSES>
|
||||||
|
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-stdlib.jar!/" />
|
||||||
|
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-reflect.jar!/" />
|
||||||
|
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-test.jar!/" />
|
||||||
|
</CLASSES>
|
||||||
|
<JAVADOC />
|
||||||
|
<SOURCES>
|
||||||
|
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-stdlib-sources.jar!/" />
|
||||||
|
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-reflect-sources.jar!/" />
|
||||||
|
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-test-sources.jar!/" />
|
||||||
|
</SOURCES>
|
||||||
|
</library>
|
||||||
|
</component>
|
||||||
Generated
+9
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/social_app.iml" filepath="$PROJECT_DIR$/social_app.iml" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/android/social_app_android.iml" filepath="$PROJECT_DIR$/android/social_app_android.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="main.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
|
||||||
|
<option name="filePath" value="$PROJECT_DIR$/lib/main.dart" />
|
||||||
|
<method />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
Generated
+36
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="FileEditorManager">
|
||||||
|
<leaf>
|
||||||
|
<file leaf-file-name="main.dart" pinned="false" current-in-tab="true">
|
||||||
|
<entry file="file://$PROJECT_DIR$/lib/main.dart">
|
||||||
|
<provider selected="true" editor-type-id="text-editor">
|
||||||
|
<state relative-caret-position="0">
|
||||||
|
<caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" />
|
||||||
|
</state>
|
||||||
|
</provider>
|
||||||
|
</entry>
|
||||||
|
</file>
|
||||||
|
</leaf>
|
||||||
|
</component>
|
||||||
|
<component name="ToolWindowManager">
|
||||||
|
<editor active="true" />
|
||||||
|
<layout>
|
||||||
|
<window_info id="Project" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="0" side_tool="false" content_ui="combo" />
|
||||||
|
</layout>
|
||||||
|
</component>
|
||||||
|
<component name="ProjectView">
|
||||||
|
<navigator currentView="ProjectPane" proportions="" version="1">
|
||||||
|
</navigator>
|
||||||
|
<panes>
|
||||||
|
<pane id="ProjectPane">
|
||||||
|
<option name="show-excluded-files" value="false" />
|
||||||
|
</pane>
|
||||||
|
</panes>
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent">
|
||||||
|
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
|
||||||
|
<property name="dart.analysis.tool.window.force.activate" value="true" />
|
||||||
|
<property name="show.migrate.to.gradle.popup" value="false" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "3b62efc2a3da49882f43c372e0bc53daef7295a6"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||||
|
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||||
|
- platform: android
|
||||||
|
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||||
|
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||||
|
- platform: ios
|
||||||
|
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||||
|
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||||
|
- platform: linux
|
||||||
|
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||||
|
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||||
|
- platform: macos
|
||||||
|
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||||
|
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||||
|
- platform: web
|
||||||
|
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||||
|
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||||
|
- platform: windows
|
||||||
|
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||||
|
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="FacetManager">
|
||||||
|
<facet type="android" name="Android">
|
||||||
|
<configuration>
|
||||||
|
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
||||||
|
<option name="GEN_FOLDER_RELATIVE_PATH_APT" value="/gen" />
|
||||||
|
<option name="GEN_FOLDER_RELATIVE_PATH_AIDL" value="/gen" />
|
||||||
|
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/app/src/main/AndroidManifest.xml" />
|
||||||
|
<option name="RES_FOLDER_RELATIVE_PATH" value="/app/src/main/res" />
|
||||||
|
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/app/src/main/assets" />
|
||||||
|
<option name="LIBS_FOLDER_RELATIVE_PATH" value="/app/src/main/libs" />
|
||||||
|
<option name="PROGUARD_LOGS_FOLDER_RELATIVE_PATH" value="/app/src/main/proguard_logs" />
|
||||||
|
</configuration>
|
||||||
|
</facet>
|
||||||
|
</component>
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/app/src/main/java" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/app/src/main/kotlin" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="Android API 24 Platform" jdkType="Android SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
<orderEntry type="library" name="Flutter for Android" level="project" />
|
||||||
|
<orderEntry type="library" name="KotlinJavaRuntime" level="project" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/lib" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.idea" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
<orderEntry type="library" name="Dart SDK" level="project" />
|
||||||
|
<orderEntry type="library" name="Flutter Plugins" level="project" />
|
||||||
|
<orderEntry type="library" name="Dart Packages" level="project" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
@@ -6,6 +6,68 @@
|
|||||||
- Add dependencies: `uv add <package>`
|
- Add dependencies: `uv add <package>`
|
||||||
- All dependencies declared in `pyproject.toml`
|
- All dependencies declared in `pyproject.toml`
|
||||||
|
|
||||||
|
## Process Entrypoints
|
||||||
|
|
||||||
|
### Bootstrap Gate (REQUIRED)
|
||||||
|
|
||||||
|
**The bootstrap gate is the ONLY allowed entry point for deployment.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Makefile (recommended)
|
||||||
|
make runtime-bootstrap-gate
|
||||||
|
|
||||||
|
# Or directly using the script
|
||||||
|
bash infra/scripts/runtime-bootstrap-gate.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This gate:
|
||||||
|
1. Runs `init-job bootstrap` (migrate + init-data)
|
||||||
|
2. Starts web and worker services
|
||||||
|
3. Aborts if bootstrap fails (prevents web/worker startup)
|
||||||
|
|
||||||
|
**Deployment without passing the bootstrap gate is PROHIBITED.**
|
||||||
|
|
||||||
|
### New Entrypoints (Phase 1-2, 2026-02-24)
|
||||||
|
|
||||||
|
**Primary (recommended):** Use Docker Compose orchestration.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Bootstrap gate (required before web/worker)
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job bootstrap
|
||||||
|
|
||||||
|
# Web
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d web
|
||||||
|
|
||||||
|
# Worker (grouped)
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d \
|
||||||
|
worker-critical worker-default worker-bulk
|
||||||
|
```
|
||||||
|
|
||||||
|
**One-shot jobs:**
|
||||||
|
```bash
|
||||||
|
# Migrate only
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job migrate
|
||||||
|
|
||||||
|
# Init data only
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job init-data
|
||||||
|
|
||||||
|
# Full bootstrap (migrate + init-data)
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job bootstrap
|
||||||
|
```
|
||||||
|
|
||||||
|
### One-shot CLI (local development)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Bootstrap (migrate + init-data)
|
||||||
|
PYTHONPATH=backend/src uv run python -m core.runtime.cli bootstrap
|
||||||
|
|
||||||
|
# Migrate only
|
||||||
|
PYTHONPATH=backend/src uv run python -m core.runtime.cli migrate
|
||||||
|
|
||||||
|
# Init data only
|
||||||
|
PYTHONPATH=backend/src uv run python -m core.runtime.cli init-data
|
||||||
|
```
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
**MUST use project logger for all runtime logging.**
|
**MUST use project logger for all runtime logging.**
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
FROM python:3.13-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir uv
|
||||||
|
|
||||||
|
COPY pyproject.toml uv.lock ./
|
||||||
|
RUN uv sync --frozen --no-dev
|
||||||
|
|
||||||
|
COPY backend/src ./backend/src
|
||||||
|
|
||||||
|
ENV PYTHONPATH=/app/backend/src
|
||||||
|
|
||||||
|
CMD ["uv", "run", "gunicorn", "backend.src.app:app", "--bind", "0.0.0.0:8000", "--workers", "2"]
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from celery import Celery
|
||||||
|
from kombu import Queue
|
||||||
|
|
||||||
|
from core.config.settings import config
|
||||||
|
from core.logging.celery import configure_celery_app
|
||||||
|
|
||||||
|
|
||||||
|
def create_celery_app() -> Celery:
|
||||||
|
"""Create and configure the Celery application."""
|
||||||
|
celery_settings = config.celery
|
||||||
|
|
||||||
|
app = Celery(
|
||||||
|
"social_app",
|
||||||
|
broker=config.celery_broker_url,
|
||||||
|
backend=config.celery_result_backend,
|
||||||
|
)
|
||||||
|
|
||||||
|
app.conf.update(
|
||||||
|
task_serializer=celery_settings.task_serializer,
|
||||||
|
result_serializer=celery_settings.result_serializer,
|
||||||
|
accept_content=celery_settings.accept_content,
|
||||||
|
timezone=celery_settings.timezone,
|
||||||
|
enable_utc=celery_settings.enable_utc,
|
||||||
|
task_track_started=celery_settings.task_track_started,
|
||||||
|
task_time_limit=celery_settings.task_time_limit,
|
||||||
|
task_soft_time_limit=celery_settings.task_soft_time_limit,
|
||||||
|
task_default_retry_delay=celery_settings.task_default_retry_delay,
|
||||||
|
task_default_queue="default",
|
||||||
|
task_create_missing_queues=False,
|
||||||
|
task_queues=(
|
||||||
|
Queue("default"),
|
||||||
|
Queue("critical"),
|
||||||
|
Queue("bulk"),
|
||||||
|
),
|
||||||
|
task_routes={
|
||||||
|
"tasks.critical.*": {"queue": "critical"},
|
||||||
|
"tasks.bulk.*": {"queue": "bulk"},
|
||||||
|
},
|
||||||
|
task_acks_late=True,
|
||||||
|
task_reject_on_worker_lost=True,
|
||||||
|
worker_prefetch_multiplier=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
app.autodiscover_tasks(["tasks"])
|
||||||
|
|
||||||
|
configure_celery_app(app, settings=config)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
celery_app = create_celery_app()
|
||||||
@@ -10,6 +10,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
|||||||
|
|
||||||
class RuntimeSettings(BaseModel):
|
class RuntimeSettings(BaseModel):
|
||||||
environment: Literal["dev", "test", "prod"] = "dev"
|
environment: Literal["dev", "test", "prod"] = "dev"
|
||||||
|
service_name: str = "app"
|
||||||
debug: bool = True
|
debug: bool = True
|
||||||
log_level: str = "INFO"
|
log_level: str = "INFO"
|
||||||
log_json: bool = True
|
log_json: bool = True
|
||||||
@@ -37,10 +38,79 @@ class RuntimeSettings(BaseModel):
|
|||||||
sql_log_queries: bool = False
|
sql_log_queries: bool = False
|
||||||
|
|
||||||
|
|
||||||
class AppSettings(BaseModel):
|
class CelerySettings(BaseModel):
|
||||||
|
broker_url: str | None = None
|
||||||
|
result_backend: str | None = None
|
||||||
|
task_serializer: str = "json"
|
||||||
|
result_serializer: str = "json"
|
||||||
|
accept_content: list[str] = Field(default_factory=lambda: ["json"])
|
||||||
|
timezone: str = "UTC"
|
||||||
|
enable_utc: bool = True
|
||||||
|
task_track_started: bool = True
|
||||||
|
task_time_limit: int = 300
|
||||||
|
task_soft_time_limit: int = 240
|
||||||
|
task_default_retry_delay: int = 30
|
||||||
|
task_max_retries: int = 3
|
||||||
|
|
||||||
|
|
||||||
|
class WebSettings(BaseModel):
|
||||||
|
server: Literal["uvicorn", "gunicorn"] = "gunicorn"
|
||||||
host: str = "0.0.0.0"
|
host: str = "0.0.0.0"
|
||||||
port: int = Field(default=8000, ge=1, le=65535)
|
port: int = Field(default=8000, ge=1, le=65535)
|
||||||
reload: bool = True
|
reload: bool = False
|
||||||
|
workers: int = Field(default=2, ge=1, le=64)
|
||||||
|
worker_class: str = "uvicorn.workers.UvicornWorker"
|
||||||
|
timeout: int = Field(default=60, ge=1, le=600)
|
||||||
|
keepalive: int = Field(default=5, ge=1, le=120)
|
||||||
|
log_level: Literal["debug", "info", "warning", "error", "critical"] = "info"
|
||||||
|
|
||||||
|
|
||||||
|
class GunicornSettings(BaseModel):
|
||||||
|
enabled_in_prod: bool = True
|
||||||
|
workers: int = 2
|
||||||
|
worker_class: str = "uvicorn.workers.UvicornWorker"
|
||||||
|
worker_connections: int = 1000
|
||||||
|
timeout: int = 60
|
||||||
|
graceful_timeout: int = 30
|
||||||
|
keepalive: int = 5
|
||||||
|
max_requests: int = 1000
|
||||||
|
max_requests_jitter: int = 50
|
||||||
|
preload_app: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class WorkerGroupSettings(BaseModel):
|
||||||
|
concurrency: int = Field(default=2, ge=1, le=32)
|
||||||
|
pool: Literal["prefork", "threads", "solo", "eventlet", "gevent"] = "prefork"
|
||||||
|
time_limit: int = Field(default=300, ge=1, le=7200)
|
||||||
|
soft_time_limit: int = Field(default=240, ge=1, le=3600)
|
||||||
|
max_tasks_per_child: int = Field(default=200, ge=1, le=1000)
|
||||||
|
prefetch_multiplier: int = Field(default=1, ge=1, le=10)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkerSettings(BaseModel):
|
||||||
|
groups: dict[str, WorkerGroupSettings] = Field(
|
||||||
|
default_factory=lambda: {
|
||||||
|
"critical": WorkerGroupSettings(
|
||||||
|
concurrency=2,
|
||||||
|
prefetch_multiplier=1,
|
||||||
|
time_limit=300,
|
||||||
|
),
|
||||||
|
"default": WorkerGroupSettings(
|
||||||
|
concurrency=2,
|
||||||
|
prefetch_multiplier=4,
|
||||||
|
time_limit=600,
|
||||||
|
),
|
||||||
|
"bulk": WorkerGroupSettings(
|
||||||
|
concurrency=1,
|
||||||
|
prefetch_multiplier=1,
|
||||||
|
time_limit=3600,
|
||||||
|
max_tasks_per_child=100,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_group_config(self, group_name: str) -> WorkerGroupSettings:
|
||||||
|
return self.groups.get(group_name, WorkerGroupSettings())
|
||||||
|
|
||||||
|
|
||||||
class CorsSettings(BaseModel):
|
class CorsSettings(BaseModel):
|
||||||
@@ -73,22 +143,6 @@ class RedisSettings(BaseModel):
|
|||||||
return f"redis://{self.host}:{self.port}/{self.db}"
|
return f"redis://{self.host}:{self.port}/{self.db}"
|
||||||
|
|
||||||
|
|
||||||
class QdrantSettings(BaseModel):
|
|
||||||
host: str = "qdrant"
|
|
||||||
port: int = 6333
|
|
||||||
grpc_port: int = 6334
|
|
||||||
api_key: str | None = None
|
|
||||||
https: bool = False
|
|
||||||
prefer_grpc: bool = True
|
|
||||||
timeout: int = 5
|
|
||||||
|
|
||||||
@computed_field
|
|
||||||
@property
|
|
||||||
def url(self) -> str:
|
|
||||||
scheme = "https" if self.https else "http"
|
|
||||||
return f"{scheme}://{self.host}:{self.port}"
|
|
||||||
|
|
||||||
|
|
||||||
class SupabaseSettings(BaseModel):
|
class SupabaseSettings(BaseModel):
|
||||||
public_scheme: str = "http"
|
public_scheme: str = "http"
|
||||||
public_host: str = "localhost"
|
public_host: str = "localhost"
|
||||||
@@ -141,19 +195,30 @@ def _resolve_env_file() -> str:
|
|||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
runtime: RuntimeSettings = RuntimeSettings()
|
runtime: RuntimeSettings = RuntimeSettings()
|
||||||
app: AppSettings = AppSettings()
|
web: WebSettings = WebSettings()
|
||||||
|
gunicorn: GunicornSettings = GunicornSettings()
|
||||||
cors: CorsSettings = CorsSettings()
|
cors: CorsSettings = CorsSettings()
|
||||||
redis: RedisSettings = RedisSettings()
|
redis: RedisSettings = RedisSettings()
|
||||||
qdrant: QdrantSettings = QdrantSettings()
|
|
||||||
supabase: SupabaseSettings = SupabaseSettings()
|
supabase: SupabaseSettings = SupabaseSettings()
|
||||||
|
celery: CelerySettings = CelerySettings()
|
||||||
database: DatabaseSettings = DatabaseSettings()
|
database: DatabaseSettings = DatabaseSettings()
|
||||||
|
worker: WorkerSettings = WorkerSettings()
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
@property
|
@property
|
||||||
def database_url(self) -> str:
|
def database_url(self) -> str:
|
||||||
return self.database.url
|
return self.database.url
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def celery_broker_url(self) -> str:
|
||||||
|
return self.celery.broker_url or self.redis.url
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def celery_result_backend(self) -> str:
|
||||||
|
return self.celery.result_backend or self.redis.url
|
||||||
|
|
||||||
model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict(
|
model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict(
|
||||||
env_file=_resolve_env_file(),
|
env_file=_resolve_env_file(),
|
||||||
env_prefix="SOCIAL_",
|
env_prefix="SOCIAL_",
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from core.logging import get_logger
|
||||||
|
|
||||||
|
logger = get_logger("core.initialization.init_data")
|
||||||
|
|
||||||
|
|
||||||
|
async def initialize_data() -> bool:
|
||||||
|
"""Initialize bootstrap data."""
|
||||||
|
logger.info("Initializing data (no-op)")
|
||||||
|
return True
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from core.initialization.init_data import initialize_data
|
||||||
|
from core.logging import get_logger
|
||||||
|
|
||||||
|
logger = get_logger("core.runtime.cli")
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_alembic_path() -> Path:
|
||||||
|
"""Resolve alembic.ini path relative to project root."""
|
||||||
|
project_root = Path(__file__).parents[3]
|
||||||
|
alembic_path = project_root / "alembic" / "alembic.ini"
|
||||||
|
if not alembic_path.exists():
|
||||||
|
raise FileNotFoundError(f"Alembic config not found at {alembic_path}")
|
||||||
|
return alembic_path
|
||||||
|
|
||||||
|
|
||||||
|
def _redact_sensitive(text: str) -> str:
|
||||||
|
"""Redact sensitive information from log output."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
SENSITIVE_KEYS = ("password", "token", "secret", "api_key")
|
||||||
|
pattern = r"(?i)(" + "|".join(SENSITIVE_KEYS) + r")\s*[:=]\s*[\"']?([^\"',\n]+)"
|
||||||
|
redacted = re.sub(pattern, r"\1=***", text)
|
||||||
|
|
||||||
|
auth_pattern = r"(?i)(authorization)\s*[:=]\s*[^\n]+"
|
||||||
|
redacted = re.sub(auth_pattern, r"\1=***", redacted)
|
||||||
|
|
||||||
|
redacted = re.sub(r"://[^:]+:[^@]+@", "://***:***@", redacted)
|
||||||
|
return redacted
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations() -> bool:
|
||||||
|
"""Run alembic migrations in a subprocess to avoid event loop conflicts."""
|
||||||
|
import os
|
||||||
|
|
||||||
|
logger.info("Running alembic migrations")
|
||||||
|
try:
|
||||||
|
config_path = _resolve_alembic_path()
|
||||||
|
logger.info("Using alembic config", path=str(config_path))
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["PYTHONPATH"] = "backend/src"
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
["uv", "run", "alembic", "-c", str(config_path), "upgrade", "head"],
|
||||||
|
cwd=Path(__file__).parents[3],
|
||||||
|
env=env,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
logger.error(
|
||||||
|
"Migration failed",
|
||||||
|
returncode=result.returncode,
|
||||||
|
stderr=_redact_sensitive(result.stderr),
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info("Migrations completed successfully")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Migration failed", error=str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def run_init_data() -> bool:
|
||||||
|
"""Initialize bootstrap data."""
|
||||||
|
logger.info("Running init-data")
|
||||||
|
try:
|
||||||
|
result = await initialize_data()
|
||||||
|
if result:
|
||||||
|
logger.info("Init-data completed successfully")
|
||||||
|
else:
|
||||||
|
logger.error("Init-data returned False")
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Init-data failed", error=str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def bootstrap() -> bool:
|
||||||
|
"""Run migrations followed by init-data."""
|
||||||
|
logger.info("Starting bootstrap (migrate + init-data)")
|
||||||
|
|
||||||
|
if not run_migrations():
|
||||||
|
logger.error("Bootstrap aborted: migrations failed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not await run_init_data():
|
||||||
|
logger.error("Bootstrap aborted: init-data failed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info("Bootstrap completed successfully")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
"""CLI entry point."""
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
logger.error("No command provided")
|
||||||
|
logger.info("Usage: python -m core.runtime.cli <command>")
|
||||||
|
logger.info("Available commands: migrate, init-data, bootstrap")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
command = sys.argv[1]
|
||||||
|
|
||||||
|
if command == "migrate":
|
||||||
|
success = run_migrations()
|
||||||
|
elif command == "init-data":
|
||||||
|
success = asyncio.run(run_init_data())
|
||||||
|
elif command == "bootstrap":
|
||||||
|
success = asyncio.run(bootstrap())
|
||||||
|
else:
|
||||||
|
logger.error("Unknown command", command=command)
|
||||||
|
logger.info("Available commands: migrate, init-data, bootstrap")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0 if success else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from services.base.qdrant import QdrantService, qdrant_service
|
|
||||||
from services.base.redis import RedisService, redis_service
|
from services.base.redis import RedisService, redis_service
|
||||||
from services.base.service_interface import (
|
from services.base.service_interface import (
|
||||||
BaseServiceProvider,
|
BaseServiceProvider,
|
||||||
@@ -11,10 +10,8 @@ from services.base.service_interface import (
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"BaseServiceProvider",
|
"BaseServiceProvider",
|
||||||
"QdrantService",
|
|
||||||
"RedisService",
|
"RedisService",
|
||||||
"ServiceRegistry",
|
"ServiceRegistry",
|
||||||
"qdrant_service",
|
|
||||||
"redis_service",
|
"redis_service",
|
||||||
"register_service",
|
"register_service",
|
||||||
"register_service_instance",
|
"register_service_instance",
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from typing import Any, Dict, Optional
|
|
||||||
|
|
||||||
from qdrant_client import QdrantClient
|
|
||||||
|
|
||||||
from core.config.settings import QdrantSettings, config
|
|
||||||
|
|
||||||
from .service_interface import BaseServiceProvider, register_service_instance
|
|
||||||
|
|
||||||
|
|
||||||
class QdrantService(BaseServiceProvider):
|
|
||||||
def __init__(self, settings: QdrantSettings | None = None) -> None:
|
|
||||||
super().__init__("qdrant")
|
|
||||||
self._settings = settings or config.qdrant
|
|
||||||
self._client: Optional[QdrantClient] = None
|
|
||||||
|
|
||||||
def _build_client(self) -> QdrantClient:
|
|
||||||
return QdrantClient(
|
|
||||||
url=self._settings.url,
|
|
||||||
api_key=self._settings.api_key,
|
|
||||||
timeout=self._settings.timeout,
|
|
||||||
prefer_grpc=self._settings.prefer_grpc,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _require_client(self) -> QdrantClient:
|
|
||||||
client = self._client
|
|
||||||
if client is None:
|
|
||||||
raise RuntimeError("Qdrant client is not initialized")
|
|
||||||
return client
|
|
||||||
|
|
||||||
async def initialize(self, **_: Any) -> bool:
|
|
||||||
try:
|
|
||||||
client = self._build_client()
|
|
||||||
collections = await asyncio.to_thread(client.get_collections)
|
|
||||||
self.logger.info(
|
|
||||||
"Qdrant service initialized",
|
|
||||||
collections_count=len(collections.collections),
|
|
||||||
)
|
|
||||||
self._client = client
|
|
||||||
self._set_initialized(True)
|
|
||||||
return True
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
self.logger.warning("Qdrant service initialization failed", error=str(exc))
|
|
||||||
self._client = None
|
|
||||||
self._set_initialized(False)
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def close(self) -> bool:
|
|
||||||
client = self._client
|
|
||||||
if client is None:
|
|
||||||
return True
|
|
||||||
try:
|
|
||||||
close = getattr(client, "close", None)
|
|
||||||
if callable(close):
|
|
||||||
await asyncio.to_thread(close)
|
|
||||||
self.logger.info("Qdrant service closed")
|
|
||||||
self._client = None
|
|
||||||
self._set_initialized(False)
|
|
||||||
return True
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
self.logger.exception("Qdrant service close failed", error=str(exc))
|
|
||||||
self._client = None
|
|
||||||
self._set_initialized(False)
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def health_check(self) -> Dict[str, Any]:
|
|
||||||
client = self._client
|
|
||||||
if client is None:
|
|
||||||
return {"status": "unhealthy", "details": {"error": "not initialized"}}
|
|
||||||
try:
|
|
||||||
collections = await asyncio.to_thread(client.get_collections)
|
|
||||||
return {
|
|
||||||
"status": "healthy",
|
|
||||||
"details": {
|
|
||||||
"connected": True,
|
|
||||||
"collections_count": len(collections.collections),
|
|
||||||
"collections": [
|
|
||||||
collection.name for collection in collections.collections[:5]
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
self.logger.warning("Qdrant health check failed", error=str(exc))
|
|
||||||
return {"status": "unhealthy", "details": {"error": str(exc)}}
|
|
||||||
|
|
||||||
def get_client(self) -> QdrantClient:
|
|
||||||
return self._require_client()
|
|
||||||
|
|
||||||
|
|
||||||
qdrant_service: QdrantService = register_service_instance("qdrant", QdrantService())
|
|
||||||
|
|
||||||
__all__ = ["QdrantService", "qdrant_service"]
|
|
||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
from fastapi import APIRouter, Depends, Response
|
from fastapi import APIRouter, Depends, Response
|
||||||
|
|
||||||
from v1.auth.dependencies import get_auth_service
|
from v1.auth.dependencies import get_auth_service
|
||||||
from v1.auth.models import (
|
from v1.auth.schemas import (
|
||||||
AuthTokenResponse,
|
AuthTokenResponse,
|
||||||
LoginRequest,
|
LoginRequest,
|
||||||
LogoutRequest,
|
LogoutRequest,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
from pydantic import BaseModel, EmailStr, Field
|
from pydantic import BaseModel, EmailStr, Field
|
||||||
|
|
||||||
|
|
||||||
@@ -7,6 +9,7 @@ class SignupRequest(BaseModel):
|
|||||||
email: EmailStr
|
email: EmailStr
|
||||||
password: str = Field(min_length=6)
|
password: str = Field(min_length=6)
|
||||||
display_name: str | None = None
|
display_name: str | None = None
|
||||||
|
redirect_to: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class LoginRequest(BaseModel):
|
class LoginRequest(BaseModel):
|
||||||
@@ -33,3 +36,18 @@ class AuthTokenResponse(BaseModel):
|
|||||||
expires_in: int
|
expires_in: int
|
||||||
token_type: str
|
token_type: str
|
||||||
user: AuthUser
|
user: AuthUser
|
||||||
|
|
||||||
|
|
||||||
|
class SignupPendingResponse(BaseModel):
|
||||||
|
status: Literal["pending_verification"] = "pending_verification"
|
||||||
|
user: AuthUser
|
||||||
|
message: str = "Email confirmation required"
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetRequest(BaseModel):
|
||||||
|
email: EmailStr
|
||||||
|
redirect_to: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetResponse(BaseModel):
|
||||||
|
message: str = "Password reset email sent"
|
||||||
@@ -8,7 +8,7 @@ from supabase import AuthError, create_client
|
|||||||
|
|
||||||
from core.config.settings import SupabaseSettings, config
|
from core.config.settings import SupabaseSettings, config
|
||||||
from core.logging import get_logger
|
from core.logging import get_logger
|
||||||
from v1.auth.models import (
|
from v1.auth.schemas import (
|
||||||
AuthTokenResponse,
|
AuthTokenResponse,
|
||||||
AuthUser,
|
AuthUser,
|
||||||
LoginRequest,
|
LoginRequest,
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from services.base.redis import RedisService, redis_service
|
from services.base.redis import RedisService, redis_service
|
||||||
from services.base.qdrant import QdrantService, qdrant_service
|
|
||||||
|
|
||||||
|
|
||||||
def get_redis_service() -> RedisService:
|
def get_redis_service() -> RedisService:
|
||||||
return redis_service
|
return redis_service
|
||||||
|
|
||||||
|
|
||||||
def get_qdrant_service() -> QdrantService:
|
|
||||||
return qdrant_service
|
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
from services.base.qdrant import QdrantService
|
|
||||||
from services.base.redis import RedisService
|
from services.base.redis import RedisService
|
||||||
from v1.infra.dependencies import get_qdrant_service, get_redis_service
|
from v1.infra.dependencies import get_redis_service
|
||||||
from v1.infra.schemas import InfraHealthResponse, ServiceHealth
|
from v1.infra.schemas import InfraHealthResponse, ServiceHealth
|
||||||
|
|
||||||
|
|
||||||
@@ -14,25 +13,16 @@ router = APIRouter(prefix="/infra", tags=["infra"])
|
|||||||
@router.get("/health", response_model=InfraHealthResponse)
|
@router.get("/health", response_model=InfraHealthResponse)
|
||||||
async def infra_health(
|
async def infra_health(
|
||||||
redis_service: RedisService = Depends(get_redis_service),
|
redis_service: RedisService = Depends(get_redis_service),
|
||||||
qdrant_service: QdrantService = Depends(get_qdrant_service),
|
|
||||||
) -> InfraHealthResponse:
|
) -> InfraHealthResponse:
|
||||||
if not redis_service.is_initialized:
|
if not redis_service.is_initialized:
|
||||||
await redis_service.initialize()
|
await redis_service.initialize()
|
||||||
if not qdrant_service.is_initialized:
|
|
||||||
await qdrant_service.initialize()
|
|
||||||
|
|
||||||
redis_health = await redis_service.health_check()
|
redis_health = await redis_service.health_check()
|
||||||
qdrant_health = await qdrant_service.health_check()
|
status = "healthy" if redis_health["status"] == "healthy" else "unhealthy"
|
||||||
status = (
|
|
||||||
"healthy"
|
|
||||||
if redis_health["status"] == "healthy" and qdrant_health["status"] == "healthy"
|
|
||||||
else "unhealthy"
|
|
||||||
)
|
|
||||||
|
|
||||||
return InfraHealthResponse(
|
return InfraHealthResponse(
|
||||||
status=status,
|
status=status,
|
||||||
services={
|
services={
|
||||||
"redis": ServiceHealth(**redis_health),
|
"redis": ServiceHealth(**redis_health),
|
||||||
"qdrant": ServiceHealth(**qdrant_health),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import uvicorn
|
|||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
from v1.auth.dependencies import get_auth_service
|
from v1.auth.dependencies import get_auth_service
|
||||||
from v1.auth.models import (
|
from v1.auth.schemas import (
|
||||||
AuthTokenResponse,
|
AuthTokenResponse,
|
||||||
AuthUser,
|
AuthUser,
|
||||||
LoginRequest,
|
LoginRequest,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from playwright.sync_api import sync_playwright
|
|||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
from v1.infra.dependencies import get_qdrant_service, get_redis_service
|
from v1.infra.dependencies import get_redis_service
|
||||||
|
|
||||||
|
|
||||||
class _FakeService:
|
class _FakeService:
|
||||||
@@ -53,7 +53,6 @@ def _start_server(host: str, port: int):
|
|||||||
|
|
||||||
def test_infra_health_e2e() -> None:
|
def test_infra_health_e2e() -> None:
|
||||||
app.dependency_overrides[get_redis_service] = lambda: _FakeService()
|
app.dependency_overrides[get_redis_service] = lambda: _FakeService()
|
||||||
app.dependency_overrides[get_qdrant_service] = lambda: _FakeService()
|
|
||||||
|
|
||||||
host = "127.0.0.1"
|
host = "127.0.0.1"
|
||||||
port = _find_free_port()
|
port = _find_free_port()
|
||||||
@@ -70,7 +69,6 @@ def test_infra_health_e2e() -> None:
|
|||||||
body = response.json()
|
body = response.json()
|
||||||
assert body["status"] == "healthy"
|
assert body["status"] == "healthy"
|
||||||
assert "redis" in body["services"]
|
assert "redis" in body["services"]
|
||||||
assert "qdrant" in body["services"]
|
|
||||||
finally:
|
finally:
|
||||||
request_context.dispose()
|
request_context.dispose()
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import socket
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from core.config.settings import Settings
|
from core.config.settings import Settings
|
||||||
from services.base.qdrant import QdrantService
|
|
||||||
from services.base.redis import RedisService
|
from services.base.redis import RedisService
|
||||||
|
|
||||||
|
|
||||||
@@ -30,20 +29,3 @@ async def test_redis_service_health_check_integration() -> None:
|
|||||||
health = await service.health_check()
|
health = await service.health_check()
|
||||||
assert health["status"] == "healthy"
|
assert health["status"] == "healthy"
|
||||||
assert await service.close() is True
|
assert await service.close() is True
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_qdrant_service_health_check_integration() -> None:
|
|
||||||
host = "127.0.0.1"
|
|
||||||
port = 6333
|
|
||||||
if not _can_connect(host, port):
|
|
||||||
pytest.skip("Qdrant is not running on localhost:6333")
|
|
||||||
|
|
||||||
config = Settings()
|
|
||||||
settings = config.qdrant.model_copy(update={"host": host, "port": port})
|
|
||||||
service = QdrantService(settings=settings)
|
|
||||||
|
|
||||||
assert await service.initialize() is True
|
|
||||||
health = await service.health_check()
|
|
||||||
assert health["status"] == "healthy"
|
|
||||||
assert await service.close() is True
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from fastapi.testclient import TestClient
|
|||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
from v1.auth.dependencies import get_auth_service
|
from v1.auth.dependencies import get_auth_service
|
||||||
from v1.auth.models import (
|
from v1.auth.schemas import (
|
||||||
AuthTokenResponse,
|
AuthTokenResponse,
|
||||||
AuthUser,
|
AuthUser,
|
||||||
LoginRequest,
|
LoginRequest,
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from core.config.settings import QdrantSettings
|
|
||||||
from services.base.qdrant import QdrantService
|
|
||||||
|
|
||||||
|
|
||||||
class _FakeCollection:
|
|
||||||
def __init__(self, name: str) -> None:
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
|
|
||||||
class _FakeCollections:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.collections = [_FakeCollection("default")]
|
|
||||||
|
|
||||||
|
|
||||||
class _FakeQdrantClient:
|
|
||||||
def get_collections(self) -> _FakeCollections:
|
|
||||||
return _FakeCollections()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_initialize_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
||||||
service = QdrantService(settings=QdrantSettings(host="localhost", port=6333))
|
|
||||||
|
|
||||||
def _build_client(_: QdrantService) -> _FakeQdrantClient:
|
|
||||||
return _FakeQdrantClient()
|
|
||||||
|
|
||||||
monkeypatch.setattr(QdrantService, "_build_client", _build_client)
|
|
||||||
|
|
||||||
result = await service.initialize()
|
|
||||||
|
|
||||||
assert result is True
|
|
||||||
assert service.is_initialized is True
|
|
||||||
|
|
||||||
health = await service.health_check()
|
|
||||||
assert health["status"] == "healthy"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_initialize_failure(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
||||||
service = QdrantService(settings=QdrantSettings(host="localhost", port=6333))
|
|
||||||
|
|
||||||
def _build_client(_: QdrantService) -> _FakeQdrantClient:
|
|
||||||
raise RuntimeError("boom")
|
|
||||||
|
|
||||||
monkeypatch.setattr(QdrantService, "_build_client", _build_client)
|
|
||||||
|
|
||||||
result = await service.initialize()
|
|
||||||
|
|
||||||
assert result is False
|
|
||||||
assert service.is_initialized is False
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_health_check_returns_unhealthy_when_not_initialized() -> None:
|
|
||||||
service = QdrantService(settings=QdrantSettings(host="localhost", port=6333))
|
|
||||||
|
|
||||||
health = await service.health_check()
|
|
||||||
|
|
||||||
assert health["status"] == "unhealthy"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_close_is_idempotent() -> None:
|
|
||||||
service = QdrantService(settings=QdrantSettings(host="localhost", port=6333))
|
|
||||||
|
|
||||||
assert await service.close() is True
|
|
||||||
assert service.is_initialized is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_client_raises_before_init() -> None:
|
|
||||||
service = QdrantService(settings=QdrantSettings(host="localhost", port=6333))
|
|
||||||
|
|
||||||
with pytest.raises(RuntimeError):
|
|
||||||
service.get_client()
|
|
||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import pytest
|
import pytest
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from v1.auth.models import (
|
from v1.auth.schemas import (
|
||||||
AuthTokenResponse,
|
AuthTokenResponse,
|
||||||
AuthUser,
|
AuthUser,
|
||||||
LoginRequest,
|
LoginRequest,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from v1.auth.models import (
|
from v1.auth.schemas import (
|
||||||
AuthTokenResponse,
|
AuthTokenResponse,
|
||||||
AuthUser,
|
AuthUser,
|
||||||
LoginRequest,
|
LoginRequest,
|
||||||
|
|||||||
@@ -0,0 +1,633 @@
|
|||||||
|
# 运行时架构重构改动清单(Runtime Refactor Plan)
|
||||||
|
|
||||||
|
**日期:** 2026-02-24
|
||||||
|
**作者:** AI Assistant
|
||||||
|
**状态:** Ready for Execution
|
||||||
|
|
||||||
|
## 审批状态流转(Release Readiness)
|
||||||
|
|
||||||
|
| 状态 | 进入条件 | 退出条件 |
|
||||||
|
|---|---|---|
|
||||||
|
| Draft | 计划初稿创建完成 | 评审材料齐备并发起评审 |
|
||||||
|
| In Review | 架构/平台/后端评审已指派 Owner | 阻断项全部关闭 |
|
||||||
|
| Ready for Execution | 阻断项关闭,且满足本节“生效条件” | 任一生效条件失效,回退到 In Review |
|
||||||
|
| In Execution | 按 Phase 开始实施并产生变更记录 | 全部 Phase 完成并通过验收 |
|
||||||
|
| Completed | 验收、回滚演练、文档归档全部完成 | 无 |
|
||||||
|
|
||||||
|
### 当前状态
|
||||||
|
- `Ready for Execution`
|
||||||
|
|
||||||
|
### `Ready for Execution` 生效条件(必须同时满足)
|
||||||
|
- 根 `AGENTS.md` 的 Compose 路径规范已与唯一标准对齐(`infra/docker/docker-compose.yml`),且校验命令通过。
|
||||||
|
- `runtime-bootstrap-gate` 已写入技术防绕过实施项(CI 依赖、统一执行器、自动检测)。
|
||||||
|
- 发布门禁、回滚演练、路由验收硬门槛已在计划内形成闭环,且命令可直接执行。
|
||||||
|
|
||||||
|
## 1. 标题与目的
|
||||||
|
|
||||||
|
### 1.1 重构目标
|
||||||
|
- 将长驻进程启动方式从 `infra/scripts/*.py` 包装脚本,迁移为编排层直接执行可观测命令(`uvicorn/gunicorn/celery`)。
|
||||||
|
- 将迁移/初始化从“混合入口脚本”改为 one-shot job(独立 CLI 命令 + Compose 一次性服务)。
|
||||||
|
- 配置策略从 `SOCIAL_RUNTIME__ENVIRONMENT` 分支控制,迁移为显式参数控制(web/worker 各自配置键)。
|
||||||
|
- Celery 从父进程拉多队列(`worker.py` 内部 `Popen`)改为按任务类型分组的独立 worker 服务。
|
||||||
|
|
||||||
|
### 1.2 非目标
|
||||||
|
- 不改动业务接口、领域逻辑、数据库 schema、Auth 流程。
|
||||||
|
- 不新增消息中间件类型(仍使用 Redis 作为 Celery broker/backend)。
|
||||||
|
- 不引入新的部署平台(仅调整当前 Docker Compose 运行路径)。
|
||||||
|
|
||||||
|
### 1.3 约束
|
||||||
|
- 仅调整运行时与运维启动路径,保持 API 行为兼容。
|
||||||
|
- 保持 `backend/src/core/config/settings.py` 作为唯一环境变量入口。
|
||||||
|
- 不在后端运行时代码中新增 `os.environ` 直读。
|
||||||
|
- 迁移期间允许“双轨短期兼容”,legacy 脚本至少保留一个发布周期(定义为一个正式版本迭代窗口)。
|
||||||
|
- legacy 脚本删除作为独立里程碑执行,不与入口切换同批发布。
|
||||||
|
|
||||||
|
### 1.4 验收标准
|
||||||
|
- 发布门禁:`init-job bootstrap` 成功(exit code=0)是 `web/worker` 启动前置条件,未通过禁止发布。
|
||||||
|
- 发布门禁唯一入口:本地与 CI 统一执行 `runtime-bootstrap-gate`,禁止任何绕过 gate 的 `up web/worker-*`。
|
||||||
|
- Compose 可直接拉起 web 与分组 worker(至少 `critical/default/bulk` 三组),且均通过健康/存活检查。
|
||||||
|
- `SOCIAL_RUNTIME__ENVIRONMENT` 不再决定 web 入口类型(gunicorn/uvicorn 由显式开关控制)。
|
||||||
|
- 新旧配置并存期遵循“新键优先、旧键告警、版本截止”规则(见第 6 节)。
|
||||||
|
|
||||||
|
#### 1.4.1 可测门槛(必须全部满足)
|
||||||
|
|
||||||
|
执行上下文约束:所有 Python 命令统一在仓库根目录执行,并显式携带 `PYTHONPATH=backend/src`;禁止切换为 `cd backend` 写法。
|
||||||
|
|
||||||
|
| 类别 | 必跑命令/检查 | 通过门槛 |
|
||||||
|
|---|---|---|
|
||||||
|
| 发布门禁 | `docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job bootstrap` | exit code = 0 |
|
||||||
|
| 启动验证(强制经 gate) | `make runtime-bootstrap-gate && docker compose --env-file .env -f infra/docker/docker-compose.yml ps` | 两条命令 exit code = 0,且 `ps` 中 `web/worker-critical/worker-default/worker-bulk/redis/db` 均为 Up |
|
||||||
|
| 健康检查 | `curl -fsS http://127.0.0.1:8000/health` | exit code = 0 |
|
||||||
|
| Unit 测试 | `PYTHONPATH=backend/src uv run pytest backend/tests/unit/test_process_settings.py backend/tests/unit/core/runtime/test_cli.py -q` | exit code = 0 |
|
||||||
|
| Integration 测试 | `PYTHONPATH=backend/src uv run pytest backend/tests/integration -q` | exit code = 0 |
|
||||||
|
| E2E 最小链路 | `PYTHONPATH=backend/src uv run pytest backend/tests/e2e/test_infra_health_e2e.py -q` | exit code = 0 |
|
||||||
|
| 覆盖率门槛 | `PYTHONPATH=backend/src uv run pytest backend/tests/unit backend/tests/integration --cov=backend/src --cov-report=term-missing --cov-fail-under=80` | 覆盖率 >= 80%,exit code = 0 |
|
||||||
|
|
||||||
|
### 1.4.2 Gate 与命令使用边界(消除绕过歧义)
|
||||||
|
|
||||||
|
| 命令类型 | 允许命令 | 使用场景 | 约束 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 发布/上线(唯一入口) | `make runtime-bootstrap-gate`(内部固定执行 `init-job bootstrap` -> `up web/worker-*`) | 本地发布、CI、手工发布、验收 | 必须使用;禁止在 gate 之外直接执行 `up web/worker-*` |
|
||||||
|
| 演练/排障(非发布) | `docker compose ... up -d web worker-critical worker-default worker-bulk redis db`、`docker compose ... logs ...`、`docker compose ... ps` | 故障复现、回滚演练、压测、本地调试 | 必须在 runbook 标注为“演练/排障命令”;不得作为发布放行依据 |
|
||||||
|
|
||||||
|
判定规则:任何“发布成功”结论都必须包含 `runtime-bootstrap-gate` 成功记录;仅执行直接 `up web/worker-*` 的记录一律判定为不合规。
|
||||||
|
|
||||||
|
### 1.4.3 路由验收硬门槛(必须满足)
|
||||||
|
|
||||||
|
| 类别 | 必跑命令/检查 | 通过门槛 |
|
||||||
|
|---|---|---|
|
||||||
|
| 路由用例(critical) | `PYTHONPATH=backend/src uv run pytest backend/tests/integration/test_celery_routing.py::test_route_critical_task_to_critical_queue -q` | exit code = 0,断言命中 `critical` 队列 |
|
||||||
|
| 路由用例(default) | `PYTHONPATH=backend/src uv run pytest backend/tests/integration/test_celery_routing.py::test_route_default_task_to_default_queue -q` | exit code = 0,断言命中 `default` 队列 |
|
||||||
|
| 路由用例(unknown -> default + 告警) | `PYTHONPATH=backend/src uv run pytest backend/tests/integration/test_celery_routing.py::test_route_unknown_task_to_default_with_warning -q` | exit code = 0,断言未知任务落到 `default` 且产生日志告警 |
|
||||||
|
| 路由验收判定 | `PYTHONPATH=backend/src uv run pytest backend/tests/integration/test_celery_routing.py::test_route_critical_task_to_critical_queue backend/tests/integration/test_celery_routing.py::test_route_default_task_to_default_queue backend/tests/integration/test_celery_routing.py::test_route_unknown_task_to_default_with_warning -q` | 三条用例均通过;`critical/default` 命中正确;unknown 路由不失败且有告警 |
|
||||||
|
|
||||||
|
## 2. 影响面清单(按文件)
|
||||||
|
|
||||||
|
说明:以下为本次重构的**目标改动清单**,按 Create / Modify / Delete 分类。
|
||||||
|
|
||||||
|
### 2.1 Create
|
||||||
|
| 文件路径 | 用途 |
|
||||||
|
|---|---|
|
||||||
|
| `backend/src/core/runtime/cli.py` | one-shot 运行命令入口(migrate/init-data) |
|
||||||
|
| `backend/tests/unit/core/runtime/test_cli.py` | CLI 参数与流程单测 |
|
||||||
|
| `docs/runtime/runtime-runbook.md` | 新运行方式(web/worker/job)操作手册 |
|
||||||
|
|
||||||
|
### 2.2 Modify
|
||||||
|
| 文件路径 | 变更说明 |
|
||||||
|
|---|---|
|
||||||
|
| `infra/docker/docker-compose.yml` | 新增/改造 `web`、`worker-critical`、`worker-default`、`worker-bulk`、`init-job` 服务及依赖 |
|
||||||
|
| `backend/src/core/config/settings.py` | 新增显式 web/worker 配置模型,弱化 `runtime.environment` 分支语义 |
|
||||||
|
| `backend/src/core/celery/app.py` | 增加队列路由配置与默认队列策略,适配分组 worker |
|
||||||
|
| `backend/AGENTS.md` | 在 Phase 0 提前更新运行入口说明(从脚本切换到编排层直启 + one-shot job) |
|
||||||
|
| `.env.example` | 新增显式启动参数,保留兼容期映射说明 |
|
||||||
|
| `backend/tests/unit/test_process_settings.py` | 更新设置项断言(显式参数优先) |
|
||||||
|
|
||||||
|
### 2.3 Deprecate(Phase 0-4)
|
||||||
|
| 文件路径 | 处理方式 |
|
||||||
|
|---|---|
|
||||||
|
| `infra/scripts/web.py` | 标记 deprecate,保留一个发布周期,不做物理删除 |
|
||||||
|
| `infra/scripts/worker.py` | 标记 deprecate,保留一个发布周期,不做物理删除 |
|
||||||
|
| `infra/scripts/bootstrap.py` | 标记 deprecate,保留一个发布周期,不做物理删除 |
|
||||||
|
| `backend/tests/unit/infra/test_web_script.py` | 标记 deprecate,保留到 Phase 5 |
|
||||||
|
| `backend/tests/unit/infra/test_worker_script.py` | 标记 deprecate,保留到 Phase 5 |
|
||||||
|
|
||||||
|
### 2.4 Delete(仅 Phase 5)
|
||||||
|
| 文件路径 | 删除原因 |
|
||||||
|
|---|---|
|
||||||
|
| `infra/scripts/web.py` | deprecate 窗口结束后物理删除 |
|
||||||
|
| `infra/scripts/worker.py` | deprecate 窗口结束后物理删除 |
|
||||||
|
| `infra/scripts/bootstrap.py` | deprecate 窗口结束后物理删除 |
|
||||||
|
| `backend/tests/unit/infra/test_web_script.py` | 随 legacy 脚本物理删除一并移除 |
|
||||||
|
| `backend/tests/unit/infra/test_worker_script.py` | 随 legacy 脚本物理删除一并移除 |
|
||||||
|
|
||||||
|
## 3. 分阶段改造计划
|
||||||
|
|
||||||
|
## Phase -1:规范对齐阻断项(必须先完成,0.25 天)
|
||||||
|
|
||||||
|
### 变更项(强制前置)
|
||||||
|
- 修订根 `AGENTS.md` 的 Docker Compose 示例路径:从 `docker/docker-compose.yml` 统一为 `infra/docker/docker-compose.yml`。
|
||||||
|
- 明确“单一规范”:仓库所有文档/脚本/CI 示例仅允许 `docker compose --env-file .env -f infra/docker/docker-compose.yml ...`。
|
||||||
|
- 将该项设置为 Phase 0 之前的硬门禁;未完成不得进入 runtime 重构实施。
|
||||||
|
|
||||||
|
### 验收检查命令(强制)
|
||||||
|
```bash
|
||||||
|
rg -n "docker compose .* -f docker/docker-compose.yml" AGENTS.md docs/ infra/ backend/
|
||||||
|
rg -n "docker compose .* -f infra/docker/docker-compose.yml" AGENTS.md
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml config
|
||||||
|
```
|
||||||
|
|
||||||
|
### 通过判定
|
||||||
|
- 第一条命令无输出(无旧路径残留)。
|
||||||
|
- 第二条命令命中根 `AGENTS.md`。
|
||||||
|
- 第三条命令 exit code = 0。
|
||||||
|
|
||||||
|
## Phase 0:基线冻结与兼容窗口(0.5 天)
|
||||||
|
|
||||||
|
### 变更项
|
||||||
|
- 在 `docs/runtime/runtime-runbook.md` 记录当前启动命令基线与回滚命令。
|
||||||
|
- 前移更新 `backend/AGENTS.md`:将“脚本入口”改为“编排层直启 + one-shot job”。
|
||||||
|
- 统一 Compose 文件路径:仅允许 `infra/docker/docker-compose.yml`,修订范围包括 `AGENTS.md`、runbook、`.env.example`、CI 脚本/文档示例。
|
||||||
|
- 约定兼容窗口:先引入新入口并灰度验证,再删除脚本入口。
|
||||||
|
|
||||||
|
### 风险点
|
||||||
|
- 团队并行开发期间继续调用旧入口,导致“看似可用、实际分叉”。
|
||||||
|
|
||||||
|
### 回滚策略
|
||||||
|
- 仅文档与约定变更,回滚为恢复旧 runbook 即可。
|
||||||
|
|
||||||
|
### 验证命令
|
||||||
|
```bash
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml config
|
||||||
|
PYTHONPATH=backend/src uv run pytest backend/tests/unit/test_process_settings.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
### 验收方式
|
||||||
|
- 文档闭环(量化):`rg -n "docker compose .* -f docker/docker-compose.yml" AGENTS.md docs/ infra/ backend/` 无输出,且 `rg -n "docker compose .* --env-file .env -f infra/docker/docker-compose.yml" AGENTS.md backend/AGENTS.md docs/runtime/runtime-runbook.md .env.example` 命中 >= 4。
|
||||||
|
- 规范闭环(量化):`rg -n "runtime-bootstrap-gate|init-job bootstrap.*前置|禁止.*up web/worker" backend/AGENTS.md docs/runtime/runtime-runbook.md` 命中 >= 2,且命中内容覆盖“前置 + 禁绕过”两类语义。
|
||||||
|
|
||||||
|
## Phase 1:Web 进程直连编排层(1 天)
|
||||||
|
|
||||||
|
### 变更项
|
||||||
|
- `infra/docker/docker-compose.yml` 新增/调整 `web` 服务:
|
||||||
|
- `command` 直接执行 `uv run uvicorn app:app ...`(dev)或 `uv run gunicorn app:app ...`(prod-like)。
|
||||||
|
- `working_dir` 指向 `backend/`,`PYTHONPATH=/app/backend/src`(按容器挂载实际路径)。
|
||||||
|
- `backend/src/core/config/settings.py` 新增显式键(示例):
|
||||||
|
- `SOCIAL_WEB__SERVER=uvicorn|gunicorn`
|
||||||
|
- `SOCIAL_WEB__HOST`、`SOCIAL_WEB__PORT`
|
||||||
|
- `SOCIAL_WEB__RELOAD`
|
||||||
|
- `SOCIAL_WEB__GUNICORN__WORKERS` 等
|
||||||
|
- 去除“仅 prod 走 gunicorn”的硬编码分支依赖。
|
||||||
|
|
||||||
|
### 风险点
|
||||||
|
- `working_dir/PYTHONPATH` 不一致导致 `app` 模块加载失败。
|
||||||
|
- gunicorn 与 uvicorn 参数映射不一致,造成性能/稳定性偏差。
|
||||||
|
|
||||||
|
### 回滚策略
|
||||||
|
- 保留 `web-legacy` profile(临时)可切回 `uv run python infra/scripts/web.py`。
|
||||||
|
- 回滚 `settings.py` 新字段,恢复 `app+gunicorn+runtime.environment` 逻辑。
|
||||||
|
|
||||||
|
### Legacy 回滚资产(硬前置)
|
||||||
|
- 落地 `legacy` profile 资产:`bootstrap-legacy`、`web-legacy`、`worker-legacy` 三个服务必须可启动且命令固定。
|
||||||
|
- 在 `docs/runtime/runtime-runbook.md` 固化 legacy 回滚步骤、期望结果、故障排查项。
|
||||||
|
- 产出并归档一次演练记录(时间、执行人、命令、日志路径、结果、问题单链接)。
|
||||||
|
|
||||||
|
### Phase 1 DoD(新增硬门槛)
|
||||||
|
- `web` 新入口通过第 1.4.1 的发布门禁与健康检查。
|
||||||
|
- legacy profile 资产已落地并可执行:`docker compose --env-file .env -f infra/docker/docker-compose.yml --profile legacy config` 返回 0。
|
||||||
|
- **必须完成一次第 14 节回滚演练并留档;未留档不得进入 Phase 2。**
|
||||||
|
|
||||||
|
### 验证命令(第 2-4 条仅用于演练/排障,非发布放行)
|
||||||
|
```bash
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job bootstrap
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d web redis db
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml logs web --tail=200
|
||||||
|
curl -fsS http://127.0.0.1:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### 发布门禁
|
||||||
|
- 若 `init-job bootstrap` 非 0 退出码,禁止启动 `web` 并阻断发布流程。
|
||||||
|
|
||||||
|
## Phase 2:迁移/初始化 one-shot job 化(1 天)
|
||||||
|
|
||||||
|
### 变更项
|
||||||
|
- 创建 `backend/src/core/runtime/cli.py`,提供子命令:
|
||||||
|
- `migrate`(执行 alembic upgrade head)
|
||||||
|
- `init-data`(调用 `core.initialization.init_data.initialize_data`)
|
||||||
|
- `bootstrap`(按顺序执行 migrate + init-data)
|
||||||
|
- Compose 新增 `init-job`(`docker compose run --rm init-job bootstrap`)。
|
||||||
|
- 设定强制门禁:仅允许在 `bootstrap` 成功后启动 `web/worker`。
|
||||||
|
- `infra/scripts/bootstrap.py` 标记 deprecate 并移出主路径(删除延后至 Phase 5)。
|
||||||
|
|
||||||
|
### 风险点
|
||||||
|
- CLI 中同步/异步调用混用导致退出码不准确。
|
||||||
|
- 迁移 job 与 web 同时启动出现竞态。
|
||||||
|
|
||||||
|
### 回滚策略
|
||||||
|
- 保留 `bootstrap-legacy` profile(短期)可回退旧脚本。
|
||||||
|
- 若 job 失败,恢复旧 `bootstrap.py` 并维持“初始化先行”的启动前置条件。
|
||||||
|
|
||||||
|
### 验证命令
|
||||||
|
```bash
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job migrate
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job init-data
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job bootstrap
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 3:Celery 队列分组与独立服务(1.5 天)
|
||||||
|
|
||||||
|
### 变更项
|
||||||
|
- `infra/docker/docker-compose.yml` 拆分 worker 服务:
|
||||||
|
- `worker-critical` -> `--queues=critical`
|
||||||
|
- `worker-default` -> `--queues=default`
|
||||||
|
- `worker-bulk` -> `--queues=bulk`
|
||||||
|
- `backend/src/core/config/settings.py` 增加队列分组显式配置:
|
||||||
|
- `SOCIAL_WORKER__GROUPS__CRITICAL__CONCURRENCY` 等
|
||||||
|
- 每组支持 `pool/time_limit/prefetch_multiplier/max_tasks_per_child`
|
||||||
|
- `backend/src/core/celery/app.py`:
|
||||||
|
- `task_default_queue` 固定显式默认值(建议 `default`)
|
||||||
|
- 增加 `task_routes`(按任务名前缀或模块路由)
|
||||||
|
- 统一异常策略:未知任务路由默认回落 `default`,并输出结构化告警(`event=celery.route.fallback`)
|
||||||
|
- 保持 `task_create_missing_queues=False`(避免拼写错误静默自动建队列)
|
||||||
|
- `infra/scripts/worker.py` 标记 deprecate(父进程多子 worker 模式退役,删除延后至 Phase 5)。
|
||||||
|
|
||||||
|
### 风险点
|
||||||
|
- 无任务路由策略时任务可能全部落到默认队列,造成优先级失效。
|
||||||
|
- 队列切分后并发参数不当,导致 CPU 抖动或长尾任务阻塞。
|
||||||
|
- 缺少统一指标阈值会导致“已拆分但不可观测”。
|
||||||
|
|
||||||
|
### 回滚策略
|
||||||
|
- 暂保留 `worker-legacy` profile,以单 worker 多队列方式兜底。
|
||||||
|
- 发现路由异常时,临时将 `task_routes` 回退为全量默认队列。
|
||||||
|
|
||||||
|
### 验证命令(仅演练/排障,发布必须走 `make runtime-bootstrap-gate`)
|
||||||
|
```bash
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d worker-critical worker-default worker-bulk redis
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml ps
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml logs worker-critical --tail=120
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml logs worker-default --tail=120
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml logs worker-bulk --tail=120
|
||||||
|
```
|
||||||
|
|
||||||
|
### 压测与回归要求
|
||||||
|
- 必须先完成第 15 节并发基线压测,再冻结 `SOCIAL_WORKER__GROUPS__*` 默认值。
|
||||||
|
|
||||||
|
## Phase 4:收口与发布门禁固化(0.5 天)
|
||||||
|
|
||||||
|
### 变更项
|
||||||
|
- 固化不可绕过门禁:`init-job bootstrap` 作为 CI 必经 job(`runtime-bootstrap-gate`),`deploy` job 必须 `needs: [runtime-bootstrap-gate]`,gate 失败即终止流水线并禁止后续 `web/worker` 启动与发布步骤。
|
||||||
|
- 发布脚本与手工发布流程必须先显式执行 `docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job bootstrap`,不得依赖 one-shot `depends_on` 语义来隐式放行。
|
||||||
|
- 新增本地统一执行器:`infra/scripts/runtime-bootstrap-gate.sh` 与 `make runtime-bootstrap-gate`(仅封装执行顺序,不改变主入口策略)。
|
||||||
|
- `docs/runtime/runtime-runbook.md` 新增“绕过风险”章节:直接执行 `up web/worker-*` 的风险、排障影响、审计追踪缺失。
|
||||||
|
- 新增 CI 自动检测绕过检查:若在 gate 之外检测到直接 `up web/worker-*`,流水线失败。
|
||||||
|
- 更新 runbook、`.env.example` 与 CI 指南,确保新入口一致。
|
||||||
|
- 补充容器化 smoke 流程到 CI,覆盖 `worker-critical/default/bulk` 三组。
|
||||||
|
|
||||||
|
### `runtime-bootstrap-gate` 唯一执行入口(本地/CI共用)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -euo pipefail -c '
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job bootstrap
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d web worker-critical worker-default worker-bulk redis db
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
- 以上模板是唯一允许的串行入口:第一条命令非 0 时 shell 立即退出,第二条 `up web/worker-*` 不得执行。
|
||||||
|
- 本地手工执行、发布脚本、CI stage 均必须复用同一模板(stage 名称固定 `runtime-bootstrap-gate`)。
|
||||||
|
|
||||||
|
### 技术防绕过校验命令(强制)
|
||||||
|
```bash
|
||||||
|
rg -n "runtime-bootstrap-gate" .github/workflows
|
||||||
|
rg -n "needs:\s*\[?runtime-bootstrap-gate\]?" .github/workflows
|
||||||
|
rg -n "docker compose .*up -d .*\b(web|worker-critical|worker-default|worker-bulk)\b" .github/workflows
|
||||||
|
rg -n "runtime-bootstrap-gate" Makefile infra/scripts docs/runtime/runtime-runbook.md
|
||||||
|
```
|
||||||
|
|
||||||
|
判定规则:
|
||||||
|
- `deploy` 相关 job 必须显式依赖 `runtime-bootstrap-gate`。
|
||||||
|
- 直接 `up web/worker-*` 仅允许出现在 gate 执行器(脚本或 Make target)中;若出现在其他 CI job,判定为绕过并阻断。
|
||||||
|
|
||||||
|
### 风险点
|
||||||
|
- 文档未同步导致新成员仍按旧命令启动。
|
||||||
|
|
||||||
|
### 回滚策略
|
||||||
|
- 若发布窗口紧,维持 legacy 脚本 deprecate 状态并延后删除。
|
||||||
|
|
||||||
|
### 验证命令(第 4-6 条仅演练/排障;发布放行看 gate 记录)
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=backend/src uv run pytest backend/tests/unit -q
|
||||||
|
PYTHONPATH=backend/src uv run pytest backend/tests/integration -q
|
||||||
|
PYTHONPATH=backend/src uv run pytest backend/tests/e2e/test_infra_health_e2e.py -q
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job bootstrap
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d web worker-critical worker-default worker-bulk redis db
|
||||||
|
curl -fsS http://127.0.0.1:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 5:legacy 删除里程碑(独立发布周期后,0.5 天)
|
||||||
|
|
||||||
|
### 触发条件
|
||||||
|
- 新入口已稳定运行至少一个发布周期。
|
||||||
|
- 第 14 节回滚演练最近一次结果为通过。
|
||||||
|
|
||||||
|
### 变更项
|
||||||
|
- 删除 `infra/scripts/web.py`、`infra/scripts/worker.py`、`infra/scripts/bootstrap.py`。
|
||||||
|
- 删除 `backend/tests/unit/infra/test_web_script.py`、`backend/tests/unit/infra/test_worker_script.py`。
|
||||||
|
- 清理文档中所有 legacy 命令。
|
||||||
|
|
||||||
|
### 回滚策略
|
||||||
|
- 若删除后发现阻塞,可从发布标签恢复 legacy 脚本并回滚到上一版本镜像。
|
||||||
|
|
||||||
|
### 验证命令
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=backend/src uv run pytest backend/tests/unit backend/tests/integration -q
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml config
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 详细改动清单(Checklist,按优先级)
|
||||||
|
|
||||||
|
- [ ] **P0-1:新增 runtime CLI one-shot 命令入口**
|
||||||
|
DoD:`PYTHONPATH=backend/src uv run python -m core.runtime.cli migrate|init-data|bootstrap` 均返回正确 exit code;`init-data` 调用 `initialize_data()`。
|
||||||
|
预计影响:替换 `bootstrap.py`,降低混合脚本复杂度。
|
||||||
|
|
||||||
|
- [ ] **P0-0:根 AGENTS Compose 路径规范修订(强制前置)**
|
||||||
|
DoD:根 `AGENTS.md` 已统一为 `infra/docker/docker-compose.yml`;全仓无 `docker/docker-compose.yml` 启动示例残留;`docker compose ... config` 校验通过。
|
||||||
|
预计影响:消除规范冲突,避免运行入口歧义。
|
||||||
|
|
||||||
|
- [ ] **P0-2:Compose 新增 `init-job` 并接入 one-shot 流程**
|
||||||
|
DoD:`docker compose run --rm init-job bootstrap` 可重复执行(幂等)且 exit code=0;该步骤失败时禁止启动 `web/worker`。
|
||||||
|
预计影响:迁移/初始化路径标准化,便于 CI/CD 执行。
|
||||||
|
|
||||||
|
- [ ] **P0-3:Compose 直启 web,移除对 `web.py` 依赖**
|
||||||
|
DoD:`web` 容器命令不再包含 `infra/scripts/web.py`;且启动前已通过 `init-job bootstrap`;`/health` 可用。
|
||||||
|
预计影响:减少一层 Python 子进程包装,排障路径更直接。
|
||||||
|
|
||||||
|
- [ ] **P0-4:前移更新 `backend/AGENTS.md` 与 Compose 路径统一闭环**
|
||||||
|
DoD:`backend/AGENTS.md` 已改为新入口规范;仓库文档/脚本示例统一为 `infra/docker/docker-compose.yml`。
|
||||||
|
预计影响:降低路径歧义与误启动概率。
|
||||||
|
|
||||||
|
- [ ] **P1-1:显式 web 配置键落地(`SOCIAL_WEB__*`)**
|
||||||
|
DoD:`SOCIAL_RUNTIME__ENVIRONMENT` 修改不再影响 web server 选型;选型由 `SOCIAL_WEB__SERVER` 控制。
|
||||||
|
预计影响:配置可预测性提升,环境分支耦合降低。
|
||||||
|
|
||||||
|
- [ ] **P1-2:worker 拆分为 `critical/default/bulk` 独立服务**
|
||||||
|
DoD:Compose 中存在 3 个 worker 服务,均只消费各自队列且日志区分明确。
|
||||||
|
预计影响:任务隔离增强,避免高耗时任务拖慢关键任务。
|
||||||
|
|
||||||
|
- [ ] **P1-3:Celery 路由显式化并禁用自动建队列**
|
||||||
|
DoD:`task_routes` 生效,未知任务默认回落 `default` 且输出 `celery.route.fallback` 告警,`task_create_missing_queues=False` 仍生效。
|
||||||
|
预计影响:线上路由错误可提前暴露,运行风险可控。
|
||||||
|
|
||||||
|
- [ ] **P2-1:删除 legacy 脚本与脚本单测**
|
||||||
|
DoD:该项仅在 Phase 5(独立里程碑)执行;满足“保留一个发布周期 + 回滚演练通过”后才可移除。
|
||||||
|
预计影响:技术债下降,入口唯一化。
|
||||||
|
|
||||||
|
- [ ] **P2-2:更新文档与 env 模板**
|
||||||
|
DoD:`backend/AGENTS.md`、`.env.example`、`docs/runtime/runtime-runbook.md` 三处命令一致。
|
||||||
|
预计影响:降低 onboarding 成本与误操作概率。
|
||||||
|
|
||||||
|
## 5. 队列拆分策略
|
||||||
|
|
||||||
|
### 5.1 推荐分组
|
||||||
|
- `critical`:用户同步感知任务(验证码发送、鉴权后置关键动作),目标低延迟。
|
||||||
|
- `default`:常规异步任务(中等耗时,可容忍轻微排队)。
|
||||||
|
- `bulk`:批处理/重计算/可延迟任务(高耗时、吞吐优先)。
|
||||||
|
|
||||||
|
### 5.2 何时需要“一队列一服务”
|
||||||
|
- 单任务类型 P95 执行时长显著高于其他任务(>3x)。
|
||||||
|
- 任务资源模型明显不同(CPU 密集 vs IO 密集)。
|
||||||
|
- 任务失败重试会产生级联影响,需要隔离故障域。
|
||||||
|
- 业务优先级强约束(SLA 严格,不能被普通任务占满并发)。
|
||||||
|
|
||||||
|
### 5.3 建议默认并发(起步值)
|
||||||
|
- `critical`: `concurrency=2~4`, `prefetch_multiplier=1`
|
||||||
|
- `default`: `concurrency=2`
|
||||||
|
- `bulk`: `concurrency=1~2`, `max_tasks_per_child` 较小以抑制内存膨胀
|
||||||
|
|
||||||
|
## 6. 配置迁移表(旧 -> 新)
|
||||||
|
|
||||||
|
| 旧配置 | 新配置 | 迁移说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `SOCIAL_RUNTIME__ENVIRONMENT=prod` + `SOCIAL_GUNICORN__ENABLED_IN_PROD=true` | `SOCIAL_WEB__SERVER=gunicorn` | server 选型不再依赖 environment 分支 |
|
||||||
|
| `SOCIAL_APP__HOST` | `SOCIAL_WEB__HOST` | web 显式命名,避免与业务 app 配置混淆 |
|
||||||
|
| `SOCIAL_APP__PORT` | `SOCIAL_WEB__PORT` | 同上 |
|
||||||
|
| `SOCIAL_APP__RELOAD` | `SOCIAL_WEB__RELOAD` | reload 仅用于开发态 web 进程 |
|
||||||
|
| `SOCIAL_GUNICORN__WORKERS` | `SOCIAL_WEB__GUNICORN__WORKERS` | 参数归并到 web 维度 |
|
||||||
|
| `SOCIAL_WORKER__ENABLED_QUEUES=[...]` | Compose 服务拆分(`worker-critical/default/bulk`)+ `SOCIAL_WORKER__GROUPS__*` | 去掉父进程多队列拉起模式 |
|
||||||
|
| `infra/scripts/bootstrap.py --migrate/--init-data` | `PYTHONPATH=backend/src uv run python -m core.runtime.cli migrate/init-data/bootstrap` | one-shot job 统一命令入口 |
|
||||||
|
| `SOCIAL_RUNTIME__SERVICE_NAME`(脚本注入) | Compose `service` 名称 + logging `service_name` 显式传参 | 不再依赖脚本注入环境变量 |
|
||||||
|
|
||||||
|
### 6.1 新旧配置并存规则(强制)
|
||||||
|
- 优先级:新配置键(`SOCIAL_WEB__*`、`SOCIAL_WORKER__GROUPS__*`)优先于旧键;若同时存在,以新键为准。
|
||||||
|
- 告警:检测到旧键生效或新旧同设时,启动日志输出 `DEPRECATION WARNING`(包含键名、替代键、截止版本)。
|
||||||
|
- 兼容截止版本:旧键兼容保留至 `v0.9`,自 `v1.0` 起移除旧键解析。
|
||||||
|
- 冲突处理:若新旧键值冲突,以新键执行并记录一次结构化告警事件(含 service_name 与环境)。
|
||||||
|
|
||||||
|
## 7. 测试策略(最小可执行集)
|
||||||
|
|
||||||
|
### 7.1 Unit
|
||||||
|
- 保留并扩展:`backend/tests/unit/test_process_settings.py`(新配置解析与默认值)。
|
||||||
|
- 新增:`backend/tests/unit/core/runtime/test_cli.py`(CLI 子命令、错误码、调用顺序)。
|
||||||
|
- legacy 保留期:`backend/tests/unit/infra/test_web_script.py`、`backend/tests/unit/infra/test_worker_script.py` 标记 deprecate,随 Phase 5 删除。
|
||||||
|
|
||||||
|
### 7.2 Integration
|
||||||
|
- 新增/调整:验证 Celery 路由配置可将任务落入预期队列(至少 `default` 与 `critical`)。
|
||||||
|
- 校验 one-shot `bootstrap` 流程在测试 DB 可重复执行且无副作用异常。
|
||||||
|
|
||||||
|
### 7.3 E2E
|
||||||
|
- 保底保留:`backend/tests/e2e/test_infra_health_e2e.py`。
|
||||||
|
- 增加最小链路:`init-job -> web health -> worker process alive`。
|
||||||
|
|
||||||
|
### 7.4 容器化 smoke(必须)
|
||||||
|
说明:此处 `up` 命令仅用于 smoke 与排障,不可替代 `runtime-bootstrap-gate` 发布门禁。
|
||||||
|
```bash
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job bootstrap
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d web worker-critical worker-default worker-bulk redis db
|
||||||
|
curl -fsS http://127.0.0.1:8000/health
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml ps
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 交付物清单
|
||||||
|
|
||||||
|
### 8.1 文档
|
||||||
|
- `docs/runtime/runtime-runbook.md`(新增)
|
||||||
|
- `backend/AGENTS.md`(更新运行入口说明)
|
||||||
|
- `.env.example`(新增显式配置键与迁移注释)
|
||||||
|
|
||||||
|
### 8.2 配置
|
||||||
|
- `infra/docker/docker-compose.yml`(新增 web/worker 分组/init-job 服务)
|
||||||
|
- `backend/src/core/config/settings.py`(新增 web/worker 显式配置结构)
|
||||||
|
- `backend/src/core/celery/app.py`(显式路由与默认队列)
|
||||||
|
|
||||||
|
### 8.3 脚本/代码
|
||||||
|
- `backend/src/core/runtime/cli.py`(新增)
|
||||||
|
- `infra/scripts/web.py`、`infra/scripts/worker.py`、`infra/scripts/bootstrap.py` 先标记 deprecate,Phase 5 删除
|
||||||
|
|
||||||
|
### 8.4 测试
|
||||||
|
- 新增 `backend/tests/unit/core/runtime/test_cli.py`
|
||||||
|
- `backend/tests/unit/infra/test_web_script.py` 随 Phase 5 删除
|
||||||
|
- `backend/tests/unit/infra/test_worker_script.py` 随 Phase 5 删除
|
||||||
|
- 更新 `backend/tests/unit/test_process_settings.py`
|
||||||
|
|
||||||
|
## 9. 主要风险与缓解
|
||||||
|
|
||||||
|
| 风险 | 影响 | 概率 | 缓解 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 入口切换后容器启动失败(路径/模块解析) | 高 | 中 | Phase 1 保留 legacy profile;先 `compose config` 再灰度起服务 |
|
||||||
|
| 队列路由配置缺失或漂移导致关键任务降级 | 高 | 中 | 统一 unknown->default+告警策略;增加 `test_route_unknown_task_to_default_with_warning` 并接入告警阈值 |
|
||||||
|
| 迁移 job 与 web 启动竞态 | 中 | 中 | CI 必经 `runtime-bootstrap-gate` 串行执行,`bootstrap` 成功后才允许 `up web/worker-*` |
|
||||||
|
| 文档与真实命令不一致 | 中 | 高 | Phase 4 强制三处文档一致性检查(AGENTS/runbook/.env.example) |
|
||||||
|
|
||||||
|
## 10. 预计工作量
|
||||||
|
|
||||||
|
| 阶段 | 预计耗时 |
|
||||||
|
|---|---|
|
||||||
|
| Phase -1 | 0.25 天 |
|
||||||
|
| Phase 0 | 0.5 天 |
|
||||||
|
| Phase 1 | 1 天 |
|
||||||
|
| Phase 2 | 1 天 |
|
||||||
|
| Phase 3 | 1.5 天 |
|
||||||
|
| Phase 4 | 0.5 天 |
|
||||||
|
| Phase 5 | 0.5 天(独立里程碑) |
|
||||||
|
| **总计** | **5.25 天(含独立里程碑)** |
|
||||||
|
|
||||||
|
## 11. 已决策项(含验收条件 / Owner / 截止版本)
|
||||||
|
|
||||||
|
| 决策项 | 已决策内容 | 验收条件 | Owner | 截止版本 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 运行门禁固化 | `init-job bootstrap` 为 CI 必经 job(`runtime-bootstrap-gate`),失败即终止 | CI 日志可见 gate job;当 `bootstrap` 非 0 时流水线终止且不执行 `up web/worker-*` | Platform/Infra | `v0.9` |
|
||||||
|
| Celery 路由最小策略 | 采用任务前缀路由(`tasks.critical.*` -> `critical`,其余默认 `default`)并保留 `bulk` 显式路由位;未知任务统一回落 `default` + 结构化告警 | integration 用例验证 `critical/default/unknown->default`;告警可检索 | Backend | `v0.9` |
|
||||||
|
| Compose 路径统一 | 全仓命令统一 `--env-file .env -f infra/docker/docker-compose.yml` | AGENTS、runbook、`.env.example`、CI 命令均一致且无旧路径残留 | Platform/Infra | `v0.9` |
|
||||||
|
| Gate 防绕过机制 | CI `deploy` 必须依赖 `runtime-bootstrap-gate`;本地/CI 统一执行器;绕过自动检测阻断 | 依赖关系可检索、绕过检查命令通过、runbook 已标注绕过风险 | Platform/Infra | `v0.9` |
|
||||||
|
|
||||||
|
## 12. 标准 CLI 调用规范(本地/容器/CI)
|
||||||
|
|
||||||
|
### 12.1 统一约束
|
||||||
|
- `docker compose` 固定使用:`--env-file .env -f infra/docker/docker-compose.yml`。
|
||||||
|
- Python 命令统一在仓库根目录执行,不使用 `cd backend`。
|
||||||
|
- `PYTHONPATH` 统一为:本地与 CI 使用 `backend/src`;容器内使用 `/app/backend/src`。
|
||||||
|
|
||||||
|
### 12.2 本地开发(host)
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=backend/src uv run python -m core.runtime.cli bootstrap
|
||||||
|
PYTHONPATH=backend/src uv run uvicorn app:app --app-dir backend/src --host 0.0.0.0 --port 8000 --reload
|
||||||
|
PYTHONPATH=backend/src uv run celery -A core.celery.app worker --workdir backend/src --queues=default --loglevel=INFO
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.2.1 本地统一门禁(必须)
|
||||||
|
```bash
|
||||||
|
bash -euo pipefail -c '
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job bootstrap
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d web worker-critical worker-default worker-bulk redis db
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
判定规则:`bootstrap` 非 0 立即终止,且不得执行任何 `up web/worker-*`。
|
||||||
|
|
||||||
|
### 12.3 容器运行(compose)
|
||||||
|
说明:以下用于本地联调/排障;发布流程仍以 `runtime-bootstrap-gate` 作为唯一放行依据。
|
||||||
|
```bash
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job bootstrap
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d web worker-critical worker-default worker-bulk redis db
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.4 CI 流程(pipeline)
|
||||||
|
```bash
|
||||||
|
bash -euo pipefail -c '
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job bootstrap
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d web worker-critical worker-default worker-bulk redis db
|
||||||
|
'
|
||||||
|
PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/runtime/test_cli.py backend/tests/unit/test_process_settings.py -q
|
||||||
|
PYTHONPATH=backend/src uv run pytest backend/tests/integration -q
|
||||||
|
PYTHONPATH=backend/src uv run pytest backend/tests/e2e/test_infra_health_e2e.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
CI 要求:上述 gate step 名称固定为 `runtime-bootstrap-gate`,不得拆分为两个可独立重试的 step。
|
||||||
|
|
||||||
|
## 13. 队列拆分后的观测与告警基线
|
||||||
|
|
||||||
|
| 指标 | 目标阈值 | 告警条件 | 处置建议 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `queue_depth{queue=critical}` | P95 < 20 | 连续 5 分钟 >= 50 | 提升 critical 并发或排查阻塞任务 |
|
||||||
|
| `task_latency_seconds{queue=critical}` | P95 < 3s | 连续 10 分钟 P95 >= 8s | 检查路由漂移与上游突发 |
|
||||||
|
| `task_latency_seconds{queue=default}` | P95 < 30s | 连续 10 分钟 P95 >= 90s | 扩容 default 或拆分热点任务 |
|
||||||
|
| `task_failure_rate{queue=*}` | < 2% | 5 分钟窗口 >= 5% | 触发失败任务抽样与回滚评估 |
|
||||||
|
| `worker_process_alive{group=*}` | 全部存活 | 任一 group 存活实例 < 1 持续 2 分钟 | 自动重启并标记发布风险 |
|
||||||
|
| `worker_cpu_usage{group=*}` | < 80% | 连续 10 分钟 >= 90% | 下调并发或扩容容器配额 |
|
||||||
|
|
||||||
|
说明:至少落地前 5 个指标;第 6 个为推荐增强项。
|
||||||
|
|
||||||
|
## 14. 回滚演练用例(新入口 -> legacy)
|
||||||
|
|
||||||
|
### 14.0 前置资产与留档要求(硬前置)
|
||||||
|
- 资产清单:Compose `--profile legacy` 下必须存在并可执行 `bootstrap-legacy`、`web-legacy`、`worker-legacy`。
|
||||||
|
- 留档位置:`docs/runtime/rollback-drills/`,文件命名 `YYYY-MM-DD-runtime-legacy-drill.md`。
|
||||||
|
- 留档最小字段:版本号、镜像 digest、执行命令、开始/结束时间、关键日志路径、通过判定、遗留问题。
|
||||||
|
- 关卡规则:**Phase 1 完成后必须执行一次并留档;未留档不得进入 Phase 2/3/4/5。**
|
||||||
|
|
||||||
|
### 14.1 演练触发场景
|
||||||
|
- `init-job bootstrap` 成功但 `web/worker` 启动后出现持续错误率超阈值。
|
||||||
|
- 队列延迟超过第 13 节阈值且 30 分钟内无法恢复。
|
||||||
|
|
||||||
|
### 14.2 演练步骤
|
||||||
|
1. 记录当前版本号、镜像 digest、配置快照。
|
||||||
|
2. 停止新入口服务:`docker compose --env-file .env -f infra/docker/docker-compose.yml stop web worker-critical worker-default worker-bulk`。
|
||||||
|
3. 启动 legacy 入口(deprecate profile):`docker compose --env-file .env -f infra/docker/docker-compose.yml --profile legacy run --rm bootstrap-legacy && docker compose --env-file .env -f infra/docker/docker-compose.yml --profile legacy up -d web-legacy worker-legacy redis db`。
|
||||||
|
4. 执行健康检查与关键任务回归(必须执行以下命令):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 健康探测(5 连续成功)
|
||||||
|
for i in 1 2 3 4 5; do
|
||||||
|
curl -fsS http://127.0.0.1:8000/health >/dev/null || exit 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# 关键路由回归(critical/default/unknown fallback)
|
||||||
|
PYTHONPATH=backend/src uv run pytest \
|
||||||
|
backend/tests/integration/test_celery_routing.py::test_route_critical_task_to_critical_queue \
|
||||||
|
backend/tests/integration/test_celery_routing.py::test_route_default_task_to_default_queue \
|
||||||
|
backend/tests/integration/test_celery_routing.py::test_route_unknown_task_to_default_with_warning -q
|
||||||
|
|
||||||
|
# 基础链路回归
|
||||||
|
PYTHONPATH=backend/src uv run pytest backend/tests/e2e/test_infra_health_e2e.py -q
|
||||||
|
|
||||||
|
# legacy worker 告警与关键链路日志检索
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml --profile legacy logs worker-legacy --since=10m | rg "critical|celery.route.fallback"
|
||||||
|
```
|
||||||
|
|
||||||
|
5. 对比回滚前后 30 分钟指标(错误率、队列深度、延迟)。
|
||||||
|
|
||||||
|
### 14.3 通过判定
|
||||||
|
- `web-legacy` 与 `worker-legacy` 在 10 分钟内恢复服务。
|
||||||
|
- `/health` 连续 5 次探测通过(上方循环命令 exit code = 0)。
|
||||||
|
- 关键回归命令全部通过:三条 routing 集成测试 + `test_infra_health_e2e.py` 均 exit code = 0。
|
||||||
|
- `critical` 任务链路恢复,失败率回落至 < 2%,且 `worker-legacy` 日志 10 分钟窗口内至少出现 1 条 `critical` 命中记录。
|
||||||
|
- 演练记录文档已提交到 `docs/runtime/rollback-drills/` 且可被 CI/评审引用。
|
||||||
|
|
||||||
|
## 15. Worker 并发基线压测与调参回归
|
||||||
|
|
||||||
|
### 15.1 基线压测
|
||||||
|
- 压测对象:`worker-critical/default/bulk` 三组并发参数(`concurrency/prefetch_multiplier/max_tasks_per_child`)。
|
||||||
|
- 压测数据:至少包含短任务(<1s)、中任务(1~10s)、长任务(>10s)三类。
|
||||||
|
- 产出:每组生成吞吐、P95 时延、失败率、CPU/内存曲线基线报告。
|
||||||
|
|
||||||
|
### 15.2 调参流程
|
||||||
|
1. 固定流量模型,单次仅调整一个参数。
|
||||||
|
2. 每轮调参后运行 30 分钟压测并记录指标。
|
||||||
|
3. 若任一关键指标恶化超过 10%,回退上一组参数。
|
||||||
|
4. 选择满足 SLA 且资源利用率最优的一组作为默认值。
|
||||||
|
|
||||||
|
### 15.3 回归要求
|
||||||
|
- 每次 worker 参数变更必须复跑第 1.4.1 的全部可测门槛。
|
||||||
|
- 必跑回归:`test_cli.py`、`test_process_settings.py`、`test_infra_health_e2e.py`。
|
||||||
|
- 覆盖率不得低于 80%。
|
||||||
|
|
||||||
|
## 16. 文档级 Go 判定(可执行)
|
||||||
|
|
||||||
|
| 判定项 | 必跑命令 | Go 门槛 |
|
||||||
|
|---|---|---|
|
||||||
|
| 门禁一致性(禁止绕过) | `rg -n "runtime-bootstrap-gate|needs:\s*\[?runtime-bootstrap-gate\]?" .github/workflows && rg -n "docker compose .*up -d .*\b(web|worker-critical|worker-default|worker-bulk)\b" .github/workflows` | 第一条命中 >= 2;第二条仅允许命中 gate 执行器文件;否则 No-Go |
|
||||||
|
| 文档闭环 | `rg -n "docker compose .* -f docker/docker-compose.yml" AGENTS.md docs/ infra/ backend/ ; rg -n "docker compose .* --env-file .env -f infra/docker/docker-compose.yml" AGENTS.md backend/AGENTS.md docs/runtime/runtime-runbook.md .env.example` | 前者无输出;后者命中 >= 4 |
|
||||||
|
| 路由策略一致性 | `PYTHONPATH=backend/src uv run pytest backend/tests/integration/test_celery_routing.py::test_route_critical_task_to_critical_queue backend/tests/integration/test_celery_routing.py::test_route_default_task_to_default_queue backend/tests/integration/test_celery_routing.py::test_route_unknown_task_to_default_with_warning -q` | 3/3 用例通过,且 unknown 路由回落 default 并有告警 |
|
||||||
|
| 关键任务链路恢复(回滚场景) | `for i in 1 2 3 4 5; do curl -fsS http://127.0.0.1:8000/health >/dev/null || exit 1; done && PYTHONPATH=backend/src uv run pytest backend/tests/e2e/test_infra_health_e2e.py -q` | 健康探测 5/5 成功且 E2E 通过 |
|
||||||
|
| 质量门槛 | `PYTHONPATH=backend/src uv run pytest backend/tests/unit backend/tests/integration --cov=backend/src --cov-report=term-missing --cov-fail-under=80` | 覆盖率 >= 80%,exit code = 0 |
|
||||||
|
|
||||||
|
执行规则:以上 5 项全部满足才可判定文档级 Go;任一项不满足即回退 `In Review`。
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# Runtime Runbook
|
||||||
|
|
||||||
|
**Date:** 2026-02-24
|
||||||
|
**Status:** Active
|
||||||
|
|
||||||
|
## 启动方式
|
||||||
|
|
||||||
|
### 一键启动 (推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用一键启动脚本
|
||||||
|
./infra/scripts/start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
或者手动执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 启动基础设施
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d redis db
|
||||||
|
|
||||||
|
# 2. 运行迁移和初始化
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job
|
||||||
|
|
||||||
|
# 3. 启动 Web 和 Worker
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d web worker-critical worker-default worker-bulk
|
||||||
|
```
|
||||||
|
|
||||||
|
### 本地 CLI (开发调试)
|
||||||
|
|
||||||
|
> 适用于本地开发调试,不依赖 Docker。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 初始化/迁移
|
||||||
|
PYTHONPATH=backend/src uv run python -m core.runtime.cli bootstrap
|
||||||
|
|
||||||
|
# 启动 Web (gunicorn)
|
||||||
|
PYTHONPATH=backend/src uv run gunicorn app:app --bind 0.0.0.0:8000 --workers 2 --worker-class uvicorn.workers.UvicornWorker
|
||||||
|
|
||||||
|
# 启动 Worker (按队列分组)
|
||||||
|
PYTHONPATH=backend/src uv run celery -A core.celery.app worker --loglevel=info --queues=critical --concurrency=2
|
||||||
|
PYTHONPATH=backend/src uv run celery -A core.celery.app worker --loglevel=info --queues=default --concurrency=2
|
||||||
|
PYTHONPATH=backend/src uv run celery -A core.celery.app worker --loglevel=info --queues=bulk --concurrency=1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 服务说明
|
||||||
|
|
||||||
|
| 服务 | 说明 | 队列 |
|
||||||
|
|------|------|------|
|
||||||
|
| web | Web 服务 (gunicorn) | - |
|
||||||
|
| worker-critical | 关键任务 worker | critical |
|
||||||
|
| worker-default | 默认任务 worker | default |
|
||||||
|
| worker-bulk | 批量任务 worker | bulk |
|
||||||
|
| init-job | 数据库迁移和初始化 | - |
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
### Web 服务器配置
|
||||||
|
|
||||||
|
| 环境变量 | 说明 | 默认值 | 有效范围 |
|
||||||
|
|----------|------|--------|----------|
|
||||||
|
| `SOCIAL_WEB__SERVER` | Web 服务器类型 | gunicorn | uvicorn/gunicorn |
|
||||||
|
| `SOCIAL_WEB__HOST` | 监听地址 | 0.0.0.0 | - |
|
||||||
|
| `SOCIAL_WEB__PORT` | 监听端口 | 8000 | 1-65535 |
|
||||||
|
| `SOCIAL_WEB__RELOAD` | 开发模式热重载 | false | true/false |
|
||||||
|
| `SOCIAL_WEB__GUNICORN__WORKERS` | Gunicorn 工作进程数 | 2 | 1-64 |
|
||||||
|
| `SOCIAL_WEB__GUNICORN__WORKER_CLASS` | Gunicorn worker 类 | uvicorn.workers.UvicornWorker | Python import path |
|
||||||
|
| `SOCIAL_WEB__GUNICORN__TIMEOUT` | 请求超时秒数 | 60 | 1-600 |
|
||||||
|
| `SOCIAL_WEB__GUNICORN__KEEPALIVE` | Keep-alive 秒数 | 5 | 1-120 |
|
||||||
|
| `SOCIAL_WEB__LOG_LEVEL` | 日志级别 | info | debug/info/warning/error/critical |
|
||||||
|
|
||||||
|
### Celery 队列路由
|
||||||
|
|
||||||
|
| 任务前缀 | 队列 |
|
||||||
|
|----------|------|
|
||||||
|
| tasks.critical.* | critical |
|
||||||
|
| tasks.bulk.* | bulk |
|
||||||
|
| 其他 | default |
|
||||||
|
|
||||||
|
## 健康检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsS http://127.0.0.1:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## 查看服务状态
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml ps
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml logs -f web
|
||||||
|
docker compose --env-file .env -f infra/docker/docker-compose.yml logs -f worker-critical
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 变更日志
|
||||||
|
|
||||||
|
| 日期 | 变更 |
|
||||||
|
|------|------|
|
||||||
|
| 2026-02-24 | 创建运行时手册,删除 legacy 脚本,统一使用 gunicorn |
|
||||||
|
| 2026-02-24 | 清理配置:合并 AppSettings 到 WebSettings,删除 Worker 旧配置 (enabled_queues/queues),统一使用 SOCIAL_WEB__GUNICORN__* 命名 |
|
||||||
+150
-13
@@ -19,18 +19,6 @@ services:
|
|||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
qdrant:
|
|
||||||
image: qdrant/qdrant:latest
|
|
||||||
container_name: social-local-qdrant
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "${SOCIAL_QDRANT__PORT:-6333}:6333"
|
|
||||||
- "${SOCIAL_QDRANT__GRPC_PORT:-6334}:6334"
|
|
||||||
volumes:
|
|
||||||
- qdrant_data:/qdrant/storage
|
|
||||||
environment:
|
|
||||||
- QDRANT__SERVICE__API_KEY=${SOCIAL_QDRANT__API_KEY:-}
|
|
||||||
|
|
||||||
studio:
|
studio:
|
||||||
container_name: supabase-studio
|
container_name: supabase-studio
|
||||||
image: supabase/studio:2025.12.17-sha-43f4f7f
|
image: supabase/studio:2025.12.17-sha-43f4f7f
|
||||||
@@ -401,7 +389,156 @@ services:
|
|||||||
command:
|
command:
|
||||||
["/bin/sh", "-c", "/app/bin/migrate && /app/bin/supavisor eval \"$$(cat /etc/pooler/pooler.exs)\" && /app/bin/server"]
|
["/bin/sh", "-c", "/app/bin/migrate && /app/bin/supavisor eval \"$$(cat /etc/pooler/pooler.exs)\" && /app/bin/server"]
|
||||||
|
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: backend/Dockerfile
|
||||||
|
image: social-local-backend
|
||||||
|
container_name: social-local-web
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${SOCIAL_WEB__PORT:-8000}:8000"
|
||||||
|
environment:
|
||||||
|
- PYTHONPATH=/app/backend/src
|
||||||
|
- SOCIAL_DATABASE__HOST=db
|
||||||
|
- SOCIAL_DATABASE__PORT=5432
|
||||||
|
- SOCIAL_DATABASE__PASSWORD=${SOCIAL_DATABASE__PASSWORD}
|
||||||
|
- SOCIAL_REDIS__HOST=redis
|
||||||
|
- SOCIAL_REDIS__PORT=6379
|
||||||
|
- SOCIAL_REDIS__PASSWORD=${SOCIAL_REDIS__PASSWORD:-}
|
||||||
|
- SOCIAL_SUPABASE__ANON_KEY=${SOCIAL_SUPABASE__ANON_KEY}
|
||||||
|
- SOCIAL_SUPABASE__SERVICE_ROLE_KEY=${SOCIAL_SUPABASE__SERVICE_ROLE_KEY}
|
||||||
|
- SOCIAL_SUPABASE__JWT_SECRET=${SOCIAL_SUPABASE__JWT_SECRET}
|
||||||
|
- SOCIAL_RUNTIME__ENVIRONMENT=${SOCIAL_RUNTIME__ENVIRONMENT:-dev}
|
||||||
|
- SOCIAL_WEB__HOST=${SOCIAL_WEB__HOST:-0.0.0.0}
|
||||||
|
- SOCIAL_WEB__PORT=${SOCIAL_WEB__PORT:-8000}
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_started
|
||||||
|
working_dir: /app/backend
|
||||||
|
command: >
|
||||||
|
sh -c "uv run gunicorn app:app --bind ${SOCIAL_WEB__HOST:-0.0.0.0}:${SOCIAL_WEB__PORT:-8000} --workers $${SOCIAL_WEB__GUNICORN__WORKERS:-2} --worker-class $${SOCIAL_WEB__GUNICORN__WORKER_CLASS:-uvicorn.workers.UvicornWorker} --timeout $${SOCIAL_WEB__GUNICORN__TIMEOUT:-60}"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8000/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
|
||||||
|
worker-critical:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: backend/Dockerfile
|
||||||
|
image: social-local-backend
|
||||||
|
container_name: social-local-worker-critical
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- PYTHONPATH=/app/backend/src
|
||||||
|
- SOCIAL_DATABASE__HOST=db
|
||||||
|
- SOCIAL_DATABASE__PORT=5432
|
||||||
|
- SOCIAL_DATABASE__PASSWORD=${SOCIAL_DATABASE__PASSWORD}
|
||||||
|
- SOCIAL_REDIS__HOST=redis
|
||||||
|
- SOCIAL_REDIS__PORT=6379
|
||||||
|
- SOCIAL_REDIS__PASSWORD=${SOCIAL_REDIS__PASSWORD:-}
|
||||||
|
- SOCIAL_SUPABASE__ANON_KEY=${SOCIAL_SUPABASE__ANON_KEY}
|
||||||
|
- SOCIAL_SUPABASE__SERVICE_ROLE_KEY=${SOCIAL_SUPABASE__SERVICE_ROLE_KEY}
|
||||||
|
- SOCIAL_SUPABASE__JWT_SECRET=${SOCIAL_SUPABASE__JWT_SECRET}
|
||||||
|
- SOCIAL_RUNTIME__ENVIRONMENT=${SOCIAL_RUNTIME__ENVIRONMENT:-dev}
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_started
|
||||||
|
working_dir: /app/backend
|
||||||
|
command: uv run celery -A core.celery.app worker --loglevel=info --queues=critical --concurrency=2
|
||||||
|
profiles:
|
||||||
|
- worker
|
||||||
|
|
||||||
|
worker-default:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: backend/Dockerfile
|
||||||
|
image: social-local-backend
|
||||||
|
container_name: social-local-worker-default
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- PYTHONPATH=/app/backend/src
|
||||||
|
- SOCIAL_DATABASE__HOST=db
|
||||||
|
- SOCIAL_DATABASE__PORT=5432
|
||||||
|
- SOCIAL_DATABASE__PASSWORD=${SOCIAL_DATABASE__PASSWORD}
|
||||||
|
- SOCIAL_REDIS__HOST=redis
|
||||||
|
- SOCIAL_REDIS__PORT=6379
|
||||||
|
- SOCIAL_REDIS__PASSWORD=${SOCIAL_REDIS__PASSWORD:-}
|
||||||
|
- SOCIAL_SUPABASE__ANON_KEY=${SOCIAL_SUPABASE__ANON_KEY}
|
||||||
|
- SOCIAL_SUPABASE__SERVICE_ROLE_KEY=${SOCIAL_SUPABASE__SERVICE_ROLE_KEY}
|
||||||
|
- SOCIAL_SUPABASE__JWT_SECRET=${SOCIAL_SUPABASE__JWT_SECRET}
|
||||||
|
- SOCIAL_RUNTIME__ENVIRONMENT=${SOCIAL_RUNTIME__ENVIRONMENT:-dev}
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_started
|
||||||
|
working_dir: /app/backend
|
||||||
|
command: uv run celery -A core.celery.app worker --loglevel=info --queues=default --concurrency=2
|
||||||
|
profiles:
|
||||||
|
- worker
|
||||||
|
|
||||||
|
worker-bulk:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: backend/Dockerfile
|
||||||
|
image: social-local-backend
|
||||||
|
container_name: social-local-worker-bulk
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- PYTHONPATH=/app/backend/src
|
||||||
|
- SOCIAL_DATABASE__HOST=db
|
||||||
|
- SOCIAL_DATABASE__PORT=5432
|
||||||
|
- SOCIAL_DATABASE__PASSWORD=${SOCIAL_DATABASE__PASSWORD}
|
||||||
|
- SOCIAL_REDIS__HOST=redis
|
||||||
|
- SOCIAL_REDIS__PORT=6379
|
||||||
|
- SOCIAL_REDIS__PASSWORD=${SOCIAL_REDIS__PASSWORD:-}
|
||||||
|
- SOCIAL_SUPABASE__ANON_KEY=${SOCIAL_SUPABASE__ANON_KEY}
|
||||||
|
- SOCIAL_SUPABASE__SERVICE_ROLE_KEY=${SOCIAL_SUPABASE__SERVICE_ROLE_KEY}
|
||||||
|
- SOCIAL_SUPABASE__JWT_SECRET=${SOCIAL_SUPABASE__JWT_SECRET}
|
||||||
|
- SOCIAL_RUNTIME__ENVIRONMENT=${SOCIAL_RUNTIME__ENVIRONMENT:-dev}
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_started
|
||||||
|
working_dir: /app/backend
|
||||||
|
command: uv run celery -A core.celery.app worker --loglevel=info --queues=bulk --concurrency=1
|
||||||
|
profiles:
|
||||||
|
- worker
|
||||||
|
|
||||||
|
init-job:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: backend/Dockerfile
|
||||||
|
image: social-local-backend
|
||||||
|
container_name: social-local-init-job
|
||||||
|
restart: "no"
|
||||||
|
environment:
|
||||||
|
- PYTHONPATH=/app/backend/src
|
||||||
|
- SOCIAL_DATABASE__HOST=db
|
||||||
|
- SOCIAL_DATABASE__PORT=5432
|
||||||
|
- SOCIAL_DATABASE__PASSWORD=${SOCIAL_DATABASE__PASSWORD}
|
||||||
|
- SOCIAL_REDIS__HOST=redis
|
||||||
|
- SOCIAL_REDIS__PORT=6379
|
||||||
|
- SOCIAL_REDIS__PASSWORD=${SOCIAL_REDIS__PASSWORD:-}
|
||||||
|
- SOCIAL_SUPABASE__ANON_KEY=${SOCIAL_SUPABASE__ANON_KEY}
|
||||||
|
- SOCIAL_SUPABASE__SERVICE_ROLE_KEY=${SOCIAL_SUPABASE__SERVICE_ROLE_KEY}
|
||||||
|
- SOCIAL_SUPABASE__JWT_SECRET=${SOCIAL_SUPABASE__JWT_SECRET}
|
||||||
|
- SOCIAL_RUNTIME__ENVIRONMENT=${SOCIAL_RUNTIME__ENVIRONMENT:-dev}
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
working_dir: /app/backend
|
||||||
|
command: uv run python -m core.runtime.cli bootstrap
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
redis_data:
|
redis_data:
|
||||||
qdrant_data:
|
|
||||||
db-config:
|
db-config:
|
||||||
|
|||||||
Executable
+19
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
COMPOSE_FILE="infra/docker/docker-compose.yml"
|
||||||
|
ENV_FILE=".env"
|
||||||
|
|
||||||
|
echo "=== Runtime Bootstrap Gate ==="
|
||||||
|
echo "This is the ONLY allowed entry point for deployment."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "[1/2] Running bootstrap (migrate + init-data)..."
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" run --rm init-job bootstrap
|
||||||
|
|
||||||
|
echo "[2/2] Starting web and worker services..."
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d web worker-critical worker-default worker-bulk redis db
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Bootstrap Gate Passed ==="
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" ps
|
||||||
@@ -13,7 +13,6 @@ dependencies = [
|
|||||||
"pydantic>=2.11.0",
|
"pydantic>=2.11.0",
|
||||||
"pydantic-settings>=2.10.0",
|
"pydantic-settings>=2.10.0",
|
||||||
"pyjwt>=2.10.1",
|
"pyjwt>=2.10.1",
|
||||||
"qdrant-client>=1.16.2",
|
|
||||||
"redis>=7.1.0",
|
"redis>=7.1.0",
|
||||||
"sqlalchemy[asyncio]>=2.0.46",
|
"sqlalchemy[asyncio]>=2.0.46",
|
||||||
"structlog>=24.4.0",
|
"structlog>=24.4.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user