feat(tplus): 新增 T+ 只读同步服务与同步管理
This commit is contained in:
1 parent
78bef43c06
commit
4fe5916c2c
43 files changed
+3309
-388
No files matched your search
@@ -0,0 +1,100 @@
|
||||
"""排队任务要有实际执行者;忙锁、断连和进程中断不能被误记成成功。"""
|
||||
|
||||
from contextlib import contextmanager, nullcontext
|
||||
from unittest.mock import Mock
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.tplus_lock import TPlusBusyError
|
||||
from app.models import TPlusSyncError
|
||||
from app.schemas.tplus import TPlusCreateIn
|
||||
from app.services.tplus import task_service, worker
|
||||
|
||||
|
||||
def _queued(session, audit=False):
|
||||
return task_service.create_task(
|
||||
session,
|
||||
TPlusCreateIn(
|
||||
request_key=uuid4(),
|
||||
task_type="AUDIT" if audit else "READ",
|
||||
entity_types=[] if audit else ["products"],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _worker_setup(monkeypatch):
|
||||
monkeypatch.setattr(worker, "sync_lock", lambda session: nullcontext())
|
||||
monkeypatch.setattr(
|
||||
"app.services.tplus.reader.TPlusReader.iter_entity",
|
||||
lambda *a, **kw: iter([{"id": 1, "code": "A"}]),
|
||||
)
|
||||
return lambda: nullcontext(Mock())
|
||||
|
||||
|
||||
def test_读取任务由worker处理并可观察重复数(tplus_session, monkeypatch):
|
||||
source = _worker_setup(monkeypatch)
|
||||
first = _queued(tplus_session)
|
||||
assert worker.run_once(tplus_session, source_factory=source) == first.batch_no
|
||||
assert first.status == "SUCCEEDED" and first.started_at is not None
|
||||
second = _queued(tplus_session)
|
||||
worker.run_once(tplus_session, source_factory=source)
|
||||
assert second.staged_record_count == 0
|
||||
assert second.skipped_record_count == 1
|
||||
|
||||
|
||||
def test_忙锁时不领取任务也不恢复其他运行记录(tplus_session, monkeypatch):
|
||||
source = _worker_setup(monkeypatch)
|
||||
batch = _queued(tplus_session)
|
||||
|
||||
@contextmanager
|
||||
def busy(_session):
|
||||
raise TPlusBusyError("已有任务")
|
||||
yield
|
||||
|
||||
monkeypatch.setattr(worker, "sync_lock", busy)
|
||||
assert worker.run_once(tplus_session, source_factory=source) is None
|
||||
assert batch.status == "QUEUED"
|
||||
|
||||
|
||||
def test_源连接失败写入固定错误不泄漏凭据(tplus_session, monkeypatch):
|
||||
_worker_setup(monkeypatch)
|
||||
batch = _queued(tplus_session)
|
||||
|
||||
def unavailable():
|
||||
raise RuntimeError("password=private-secret")
|
||||
|
||||
worker.run_once(tplus_session, source_factory=unavailable)
|
||||
assert batch.status == "FAILED"
|
||||
error = tplus_session.scalar(select(TPlusSyncError))
|
||||
assert "private-secret" not in error.error_message
|
||||
assert error.error_code == "EXECUTION_ERROR"
|
||||
|
||||
|
||||
def test_取得锁后把遗留运行中任务记为中断失败(tplus_session, monkeypatch):
|
||||
source = _worker_setup(monkeypatch)
|
||||
batch = _queued(tplus_session)
|
||||
batch.status = "RUNNING"
|
||||
tplus_session.commit()
|
||||
assert worker.run_once(tplus_session, source_factory=source) is None
|
||||
assert batch.status == "FAILED"
|
||||
assert tplus_session.scalar(select(TPlusSyncError.error_code)) == "INTERRUPTED"
|
||||
|
||||
|
||||
def test_核验任务持久化报告但不修改业务数据(tplus_session, monkeypatch):
|
||||
source = _worker_setup(monkeypatch)
|
||||
batch = _queued(tplus_session, audit=True)
|
||||
monkeypatch.setattr(
|
||||
worker.TPlusRelationshipAudit,
|
||||
"run",
|
||||
lambda self: {
|
||||
"relationship_status": "OBSERVED",
|
||||
"receipt_kinds": [{"row_count": 10}],
|
||||
"compatibility_status": "PENDING",
|
||||
"is_target_engine": False,
|
||||
},
|
||||
)
|
||||
worker.run_once(tplus_session, source_factory=source)
|
||||
assert batch.status == "SUCCEEDED"
|
||||
assert batch.audit_result_json["compatibility_status"] == "PENDING"
|
||||
assert batch.staged_record_count == 0
|
||||
Reference in new issue
Block a user