docs: 将仓库代码注释统一为中文
CI / docs-check (push) Canceled after 0s
CI / backend-test (push) Canceled after 0s
CI / service-test (push) Canceled after 0s
CI / frontend-test (push) Canceled after 0s
CI / rust-core (push) Canceled after 0s
CI / docs-check (pull_request) Canceled after 0s
CI / backend-test (pull_request) Canceled after 0s
CI / service-test (pull_request) Canceled after 0s
CI / frontend-test (pull_request) Canceled after 0s
CI / rust-core (pull_request) Canceled after 0s
CI / docs-check (push) Canceled after 0s
CI / backend-test (push) Canceled after 0s
CI / service-test (push) Canceled after 0s
CI / frontend-test (push) Canceled after 0s
CI / rust-core (push) Canceled after 0s
CI / docs-check (pull_request) Canceled after 0s
CI / backend-test (pull_request) Canceled after 0s
CI / service-test (pull_request) Canceled after 0s
CI / frontend-test (pull_request) Canceled after 0s
CI / rust-core (pull_request) Canceled after 0s
This commit is contained in:
@@ -29,7 +29,7 @@ class SyncError(Exception):
|
||||
|
||||
|
||||
class StagingDamaged(Exception):
|
||||
"""Internal signal used to commit cleanup before returning UPLOAD_DAMAGED."""
|
||||
"""用于在返回 UPLOAD_DAMAGED 之前提交清理的内部信号。"""
|
||||
|
||||
def __init__(self, upload):
|
||||
self.upload_id = upload["id"]
|
||||
@@ -131,8 +131,7 @@ def create_app(db: Database, objects, staging: Path, *, quota=1024**3, clock=tim
|
||||
|
||||
@app.get("/health")
|
||||
def health(response: Response):
|
||||
# An ephemeral identifier lets deployment probes prove that both
|
||||
# configured workers receive traffic without exposing host identity.
|
||||
# 临时标识符可以让部署探测证明两个配置的工作线程都接收流量,而不会暴露主机身份。
|
||||
response.headers["X-OpenNexus-Worker"] = worker_id
|
||||
return {"status": "ok"}
|
||||
|
||||
@@ -281,8 +280,7 @@ def create_app(db: Database, objects, staging: Path, *, quota=1024**3, clock=tim
|
||||
return path
|
||||
|
||||
def discard_damaged_upload(damaged):
|
||||
# Re-open after the failed operation released its transaction. Deleting
|
||||
# inside the failed transaction would be rolled back with the response.
|
||||
# 失败的操作释放其事务后重新打开。失败事务中的删除操作将随响应一起回滚。
|
||||
with db.transaction() as conn:
|
||||
suffix = " FOR UPDATE" if not db.sqlite else ""
|
||||
row(conn, "SELECT id FROM vaults WHERE id=:v" + suffix, v=damaged.vault_id)
|
||||
@@ -294,8 +292,7 @@ def create_app(db: Database, objects, staging: Path, *, quota=1024**3, clock=tim
|
||||
device=damaged.device_id,
|
||||
)
|
||||
if upload:
|
||||
# File first: interruption leaves a row whose quota reservation
|
||||
# can still be released by expiry maintenance.
|
||||
# 文件优先:中断留下一行,其配额保留仍可通过到期维护释放。
|
||||
(staging / upload["id"]).unlink(missing_ok=True)
|
||||
run(conn, "DELETE FROM uploads WHERE id=:id", id=upload["id"])
|
||||
|
||||
@@ -321,8 +318,7 @@ def create_app(db: Database, objects, staging: Path, *, quota=1024**3, clock=tim
|
||||
if len(chunk) > 1048576 - len(data):
|
||||
raise SyncError(413, "CHUNK_TOO_LARGE")
|
||||
data.extend(chunk)
|
||||
# Keep the transaction and durable write on one worker thread. A slow
|
||||
# database lock or fsync must not block this worker's ASGI event loop.
|
||||
# 将事务和持久写入保留在一个工作线程上。缓慢的数据库锁定或 fsync 不得阻止此工作线程的 ASGI 事件循环。
|
||||
return await run_in_threadpool(persist_upload_chunk, vault_id, upload_id,
|
||||
authorization, offset, data)
|
||||
|
||||
@@ -447,8 +443,7 @@ def create_app(db: Database, objects, staging: Path, *, quota=1024**3, clock=tim
|
||||
if not obj:
|
||||
raise SyncError(404, "OBJECT_NOT_FOUND")
|
||||
expected_size = obj["size"]
|
||||
# Verify before returning any bytes, without holding a database transaction
|
||||
# or buffering an entire attachment in RAM.
|
||||
# 在返回任何字节之前进行验证,而不保留数据库事务或在 RAM 中缓冲整个附件。
|
||||
temporary = tempfile.NamedTemporaryFile(prefix="download-", dir=staging, delete=False)
|
||||
path = Path(temporary.name)
|
||||
try:
|
||||
|
||||
@@ -36,7 +36,7 @@ class Database:
|
||||
def migrate(self):
|
||||
with self.transaction() as conn:
|
||||
if not self.sqlite:
|
||||
# Serialize factory startup migrations across the supported workers.
|
||||
# 在受支持的工作人员之间序列化工厂启动迁移。
|
||||
conn.execute(text("SELECT pg_advisory_xact_lock(1330534488)"))
|
||||
conn.execute(text(SCHEMA[0]))
|
||||
version = conn.execute(text("SELECT version FROM schema_version")).scalar()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Delete expired upload staging only; referenced historical objects are never GC'd."""
|
||||
"""仅删除过期的上传暂存;引用的历史对象永远不会是 GC'd。"""
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
@@ -11,7 +11,7 @@ def cleanup_expired_uploads(db, staging: Path, *, now=None, limit=500):
|
||||
expired = rows(conn, "SELECT id,vault_id FROM uploads WHERE expires<=:now ORDER BY expires LIMIT :limit", now=now, limit=limit)
|
||||
removed = 0
|
||||
for candidate in expired:
|
||||
# Same lock order as PUT/complete. Recheck expiry after acquiring the lock.
|
||||
# 与 PUT 相同的锁定顺序/完成。获取锁后重新检查过期时间。
|
||||
with db.transaction() as conn:
|
||||
suffix = " FOR UPDATE" if not db.sqlite else ""
|
||||
row(conn, "SELECT id FROM vaults WHERE id=:v" + suffix, v=candidate["vault_id"])
|
||||
@@ -19,7 +19,7 @@ def cleanup_expired_uploads(db, staging: Path, *, now=None, limit=500):
|
||||
if upload:
|
||||
if not re.fullmatch(r"[0-9a-f]{32}", upload["id"]):
|
||||
raise RuntimeError("UPLOAD_ID_INVALID")
|
||||
# File first: interruption leaves an expired row that can be retried.
|
||||
# 文件优先:中断留下可以重试的过期行。
|
||||
(staging / upload["id"]).unlink(missing_ok=True)
|
||||
run(conn, "DELETE FROM uploads WHERE id=:id", id=upload["id"])
|
||||
removed += 1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Consistent PostgreSQL/S3 backup and empty-instance restore operations."""
|
||||
"""一致的 PostgreSQL/S3 备份和空实例恢复操作。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -51,7 +51,7 @@ TABLES: dict[str, tuple[str, ...]] = {
|
||||
|
||||
|
||||
class OperationsError(RuntimeError):
|
||||
"""Stable operator-facing failure without credentials or response bodies."""
|
||||
"""稳定的面向操作员的故障,无需凭证或响应主体。"""
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Bound readiness work even when a synchronous dependency ignores its timeout."""
|
||||
"""即使同步依赖项忽略其超时,绑定准备工作也会起作用。"""
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
@@ -15,8 +15,7 @@ class Readiness:
|
||||
|
||||
@staticmethod
|
||||
def consume(task):
|
||||
# A request can time out or disconnect before the synchronous probe ends.
|
||||
# Retrieve late exceptions without logging dependency messages/secrets.
|
||||
# 在同步探测结束之前,请求可能会超时或断开连接。检索晚期异常而不记录依赖项消息/秘密。
|
||||
if not task.cancelled():
|
||||
task.exception()
|
||||
|
||||
@@ -28,8 +27,7 @@ class Readiness:
|
||||
self.running = asyncio.create_task(asyncio.to_thread(self.probe))
|
||||
self.running.add_done_callback(self.consume)
|
||||
try:
|
||||
# Cancelling a to_thread await does not stop its OS thread. Keep
|
||||
# the task alive so subsequent requests reuse the same probe.
|
||||
# 取消 to_thread 等待不会停止其 OS 线程。保持任务处于活动状态,以便后续请求重用相同的探测器。
|
||||
await asyncio.wait_for(asyncio.shield(self.running), self.timeout)
|
||||
self.ok = True
|
||||
except Exception:
|
||||
|
||||
@@ -67,7 +67,7 @@ class S3Objects:
|
||||
self.bucket = bucket
|
||||
|
||||
def ensure_bucket(self) -> bool:
|
||||
"""Create the configured bucket when absent; never alter an existing bucket."""
|
||||
"""不在时创建配置的桶;切勿更改现有存储桶。"""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
try:
|
||||
@@ -108,10 +108,7 @@ class S3Objects:
|
||||
|
||||
def put_file(self, key: str, path: Path, content_hash: str):
|
||||
with path.open("rb") as stream:
|
||||
# Objects are capped at 100 MiB, well below S3's 5 GiB single-PUT
|
||||
# limit. A direct streaming request has one explicit connection
|
||||
# lifetime; constructing a transfer manager per completion can
|
||||
# retain pooled MinIO connections under repeated multi-worker use.
|
||||
# 对象的上限为 100 MiB,远低于 S3 的 5 GiB 单 PUT 限制。直接流请求有一个显式的连接生命周期;每次完成构建一个传输管理器可以在重复的多工作线程使用下保留池化的 MinIO 连接。
|
||||
self.client.put_object(
|
||||
Bucket=self.bucket,
|
||||
Key=key,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Single-worker localhost fixture for real Rust HTTP interoperability, never deployment."""
|
||||
"""用于真正 Rust HTTP 互操作性的单工作程序本地主机固定装置,无需部署。"""
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Same-origin Vue console delivery and its public account workflow."""
|
||||
"""同源Vue控制台交付及其公众号工作流程。"""
|
||||
|
||||
import re
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Fault vectors for the production IO paths; database still uses an isolated fixture."""
|
||||
"""生产 IO 路径的故障向量;数据库仍然使用独立的固定装置。"""
|
||||
import hashlib
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
@@ -72,7 +72,7 @@ def test_download_is_verified_before_response_and_temp_files_are_removed(env):
|
||||
client, _, store, staging, _ = env
|
||||
auth, _, base = setup(client)
|
||||
sha = upload(client, base, auth)
|
||||
# This path must use streaming open(), never the full-object get().
|
||||
# 此路径必须使用流式 open(),而不是完整对象 get()。
|
||||
store.get = lambda key: pytest.fail("full-object read")
|
||||
response = client.get(base + "/objects/" + sha, headers=auth)
|
||||
assert response.content == b"controlled note"
|
||||
@@ -135,7 +135,7 @@ def test_slow_upload_fsync_does_not_block_worker_health(env, monkeypatch):
|
||||
pending = pool.submit(client.put, path + "?offset=0", headers=auth, content=b"abc")
|
||||
try:
|
||||
assert entered.wait(5)
|
||||
# Both requests use this TestClient's single ASGI event loop.
|
||||
# 两个请求都使用此 TestClient 的单个 ASGI 事件循环。
|
||||
health = pool.submit(client.get, "/health").result(timeout=2)
|
||||
assert health.status_code == 200
|
||||
assert not pending.done()
|
||||
@@ -167,7 +167,7 @@ def test_upload_limits_and_failed_fsync_preserve_durable_offset(env, monkeypatch
|
||||
patch.setattr(os, "fsync", failed)
|
||||
with pytest.raises(OSError, match="controlled fsync failure"):
|
||||
client.put(path + "?offset=0", headers=auth, content=data)
|
||||
# Failure propagated from the thread; the SQL transaction did not advance.
|
||||
# 从线程传播故障; SQL交易没有推进。
|
||||
assert client.get(path, headers=auth).json()["offset"] == 0
|
||||
assert (staging / info["upload_id"]).stat().st_size == 0
|
||||
assert client.put(path + "?offset=0", headers=auth, content=data).json() == {"offset": len(data)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Bounded worker use, request cancellation, cached failures and recovery."""
|
||||
"""有限制的工作线程使用、请求取消、缓存故障和恢复。"""
|
||||
import asyncio
|
||||
from threading import Event
|
||||
|
||||
@@ -30,7 +30,7 @@ def test_timeout_and_cancel_never_spawn_overlapping_dependency_probes():
|
||||
finally:
|
||||
release.set()
|
||||
await asyncio.gather(ready.running, return_exceptions=True)
|
||||
# A completed failed probe does not prevent a new healthy attempt.
|
||||
# 已完成的失败探测不会阻止新的健康尝试。
|
||||
ready.probe = lambda: None
|
||||
assert await ready.check() is True
|
||||
asyncio.run(run())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Real localhost HTTP harness; SQLite/DiskObjects, not production topology."""
|
||||
"""真实的 localhost HTTP 测试框架;使用 SQLite/DiskObjects,并非生产拓扑。"""
|
||||
import asyncio
|
||||
import json
|
||||
import socket
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
"""Four concurrent upload/download probes; run only against a disposable test service.
|
||||
|
||||
Credentials JSON is [{"username": "...", "password": "..."}, ...] for two
|
||||
existing test accounts. Never writes credentials, tokens or response bodies to reports.
|
||||
This is a transfer probe, not a claim of S-09 completion or an RSS measurement.
|
||||
"""
|
||||
"""四个并发上传/下载探针;仅针对一次性测试服务运行。两个现有测试帐户的凭据 JSON 为 [{"username": "...", "password": "..."}, ...]。切勿将凭证、令牌或响应正文写入报告。这是一个转移探针,不是 S-09 完成或 RSS 测量的声明。"""
|
||||
import argparse
|
||||
import asyncio
|
||||
import hashlib
|
||||
@@ -30,7 +25,7 @@ async def checked(client, method, path, **kwargs):
|
||||
|
||||
|
||||
def block(index, offset, length):
|
||||
# Distinct, repeatable contents per stream and offset, without whole-file buffers.
|
||||
# 每个流和偏移量具有独特的、可重复的内容,没有整个文件缓冲区。
|
||||
seed = hashlib.sha256(f"20260908:{index}:{offset}".encode()).digest()
|
||||
return (seed * ((length + len(seed) - 1) // len(seed)))[:length]
|
||||
|
||||
@@ -116,7 +111,7 @@ async def probe(base_url, credentials, *, size=100 * CHUNK):
|
||||
checksum.update(data)
|
||||
if received != size or checksum.hexdigest() != sha:
|
||||
raise ProbeFailure("DOWNLOAD_INTEGRITY")
|
||||
# Another account must not be able to read this object's bytes.
|
||||
# 另一个帐户必须无法读取此对象的字节。
|
||||
denied = await admin.get(base + "/objects/" + sha, headers=sessions[1 - index // 2])
|
||||
if denied.status_code not in (403, 404):
|
||||
raise ProbeFailure("ACCOUNT_ISOLATION_FAILED")
|
||||
@@ -127,7 +122,7 @@ async def probe(base_url, credentials, *, size=100 * CHUNK):
|
||||
|
||||
tasks = [asyncio.create_task(transfer(*job)) for job in jobs]
|
||||
try:
|
||||
# Bound preparation: a failed peer must not leave the others waiting forever.
|
||||
# 绑定准备:失败的对等体不能让其他对等体永远等待。
|
||||
async def all_ready():
|
||||
for _ in jobs:
|
||||
await ready.get()
|
||||
@@ -175,7 +170,7 @@ def main():
|
||||
credentials = json.loads(args.credentials.read_text(encoding="utf-8"))
|
||||
report = asyncio.run(asyncio.wait_for(probe(args.url, credentials), timeout=1800))
|
||||
except Exception as error:
|
||||
# Exception text may contain credentials or response data; log only its type.
|
||||
# 异常文本可能包含凭据或响应数据;仅记录其类型。
|
||||
report["error_type"] = type(error).__name__
|
||||
finally:
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
Reference in New Issue
Block a user