修復 AI embedding queue 原子取件
All checks were successful
CD Pipeline / deploy (push) Successful in 1m3s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m3s
This commit is contained in:
@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
# 系統版本與路徑
|
# 系統版本與路徑
|
||||||
# ==========================================
|
# ==========================================
|
||||||
SYSTEM_VERSION = "V10.256"
|
SYSTEM_VERSION = "V10.257"
|
||||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||||
public_url = PUBLIC_URL # 用於模板顯示
|
public_url = PUBLIC_URL # 用於模板顯示
|
||||||
|
|
||||||
|
|||||||
@@ -174,42 +174,65 @@ def _process_one_embedding(row_id: int, target_table: str, target_id: int,
|
|||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _claim_pending_embeddings(limit: int = EMBED_BATCH_SIZE,
|
||||||
|
max_attempts: int = EMBED_MAX_ATTEMPTS):
|
||||||
|
"""Atomically claim pending embedding jobs across multiple workers."""
|
||||||
|
session = get_session()
|
||||||
|
try:
|
||||||
|
session.execute(
|
||||||
|
text("""
|
||||||
|
UPDATE embedding_retry_queue
|
||||||
|
SET status = 'pending',
|
||||||
|
updated_at = :now,
|
||||||
|
last_error = COALESCE(last_error, '') || ' | reset stale processing'
|
||||||
|
WHERE status = 'processing'
|
||||||
|
AND updated_at < :cutoff
|
||||||
|
"""),
|
||||||
|
{
|
||||||
|
"now": datetime.now(),
|
||||||
|
"cutoff": datetime.fromtimestamp(time.time() - 15 * 60),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
rows = session.execute(
|
||||||
|
text("""
|
||||||
|
WITH picked AS (
|
||||||
|
SELECT id
|
||||||
|
FROM embedding_retry_queue
|
||||||
|
WHERE status = 'pending'
|
||||||
|
AND attempts < :max
|
||||||
|
ORDER BY created_at
|
||||||
|
FOR UPDATE SKIP LOCKED
|
||||||
|
LIMIT :lim
|
||||||
|
)
|
||||||
|
UPDATE embedding_retry_queue q
|
||||||
|
SET status = 'processing',
|
||||||
|
updated_at = :now
|
||||||
|
FROM picked
|
||||||
|
WHERE q.id = picked.id
|
||||||
|
RETURNING q.id, q.target_table, q.target_id, q.text_content, q.model
|
||||||
|
"""),
|
||||||
|
{
|
||||||
|
"now": datetime.now(),
|
||||||
|
"max": max_attempts,
|
||||||
|
"lim": limit,
|
||||||
|
},
|
||||||
|
).fetchall()
|
||||||
|
session.commit()
|
||||||
|
return rows
|
||||||
|
except Exception:
|
||||||
|
session.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
def _embedding_worker_loop():
|
def _embedding_worker_loop():
|
||||||
"""背景執行緒:輪詢 embedding_retry_queue,批次處理 pending 項目"""
|
"""背景執行緒:輪詢 embedding_retry_queue,批次處理 pending 項目"""
|
||||||
sys_log.info("[OCLearn] Hermes Embedding Worker (DB-backed) 啟動")
|
sys_log.info("[OCLearn] Hermes Embedding Worker (DB-backed) 啟動")
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
session = get_session()
|
rows = _claim_pending_embeddings()
|
||||||
try:
|
|
||||||
session.execute(
|
|
||||||
text("""
|
|
||||||
UPDATE embedding_retry_queue
|
|
||||||
SET status = 'pending',
|
|
||||||
updated_at = :now,
|
|
||||||
last_error = COALESCE(last_error, '') || ' | reset stale processing'
|
|
||||||
WHERE status = 'processing'
|
|
||||||
AND updated_at < :cutoff
|
|
||||||
"""),
|
|
||||||
{
|
|
||||||
"now": datetime.now(),
|
|
||||||
"cutoff": datetime.fromtimestamp(time.time() - 15 * 60),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
session.commit()
|
|
||||||
rows = session.execute(
|
|
||||||
text("""
|
|
||||||
SELECT id, target_table, target_id, text_content, model
|
|
||||||
FROM embedding_retry_queue
|
|
||||||
WHERE status = 'pending'
|
|
||||||
AND attempts < :max
|
|
||||||
ORDER BY created_at
|
|
||||||
LIMIT :lim
|
|
||||||
"""),
|
|
||||||
{"max": EMBED_MAX_ATTEMPTS, "lim": EMBED_BATCH_SIZE},
|
|
||||||
).fetchall()
|
|
||||||
finally:
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
for row in rows:
|
for row in rows:
|
||||||
_process_one_embedding(
|
_process_one_embedding(
|
||||||
row[0], row[1], row[2], row[3], row[4] or "bge-m3:latest"
|
row[0], row[1], row[2], row[3], row[4] or "bge-m3:latest"
|
||||||
|
|||||||
@@ -98,3 +98,41 @@ def test_process_one_embedding_writes_signature(monkeypatch):
|
|||||||
assert target_updates
|
assert target_updates
|
||||||
assert "embedding_signature" in target_updates[0][0]
|
assert "embedding_signature" in target_updates[0][0]
|
||||||
assert target_updates[0][1]["sig"] == get_embedding_signature(model="bge-m3:latest", dim=1024)
|
assert target_updates[0][1]["sig"] == get_embedding_signature(model="bge-m3:latest", dim=1024)
|
||||||
|
|
||||||
|
|
||||||
|
def test_claim_pending_embeddings_uses_skip_locked(monkeypatch):
|
||||||
|
import services.openclaw_learning_service as learning
|
||||||
|
|
||||||
|
executed = []
|
||||||
|
claimed_rows = [(7, "ai_insights", 42, "測試內容", "bge-m3:latest")]
|
||||||
|
|
||||||
|
class Result:
|
||||||
|
def fetchall(self):
|
||||||
|
return claimed_rows
|
||||||
|
|
||||||
|
class Session:
|
||||||
|
def execute(self, stmt, params=None):
|
||||||
|
executed.append((str(stmt), params or {}))
|
||||||
|
if "RETURNING q.id" in str(stmt):
|
||||||
|
return Result()
|
||||||
|
return Result()
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def rollback(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
monkeypatch.setattr(learning, "get_session", lambda: Session())
|
||||||
|
|
||||||
|
rows = learning._claim_pending_embeddings(limit=3, max_attempts=5)
|
||||||
|
|
||||||
|
assert rows == claimed_rows
|
||||||
|
claim_sql = [stmt for stmt, _ in executed if "RETURNING q.id" in stmt][0]
|
||||||
|
assert "FOR UPDATE SKIP LOCKED" in claim_sql
|
||||||
|
assert "UPDATE embedding_retry_queue q" in claim_sql
|
||||||
|
assert executed[-1][1]["lim"] == 3
|
||||||
|
assert executed[-1][1]["max"] == 5
|
||||||
|
|||||||
Reference in New Issue
Block a user