Commit d4364b2a1a9bec4a8d2fc423c23043fb91513aae

Authored by yibo
1 parent 299e0ef3

fix(lifelog/summary): 修 worker 并发把 llama-server 搞 502

实测:periodic + one-shot + RETRY 死循环并发打同一个 2b 上游 → llama-swap
proxy context-canceled → 502,纪要永远生成不出来(console 卡「运行中」)。

- 进程内 AtomicBoolean 互斥:同时只跑一个 worker body(enqueueUniqueWork
  只能去重同名 one-shot,挡不住 periodic)。
- 单轮时间预算 7min:超了停、剩余会话下轮继续,避开 WorkManager ~10min
  执行上限被中途 kill(→ context canceled → 502)。
- 失败不再 Result.retry():未处理会话靠下一轮 periodic / 手动重跑捡,
  不再 hammering。

验证:单 worker 跑 56 段会话,llama-swap 返 200,本地 2B 纪要落库。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/summary/SummaryWorker.kt
... ... @@ -51,6 +51,13 @@ class SummaryWorker(
51 51 * 端点/model 由 ChatConfig 解析(与对话共用配置)。 */
52 52 const val KEY_BACKEND = "summary_backend"
53 53  
  54 + /** 防并发:periodic + one-shot + RETRY 可能同时触发,多个 worker 并发打同一个
  55 + * llama-server 会把上游搞 502(实测)。进程内 flag 保证同时只跑一个 worker body。 */
  56 + private val isRunning = java.util.concurrent.atomic.AtomicBoolean(false)
  57 +
  58 + /** 单轮时间预算:超了就停、剩余会话下轮继续,避开 WorkManager ~10min 执行上限。 */
  59 + const val RUN_BUDGET_MS = 7 * 60 * 1000L
  60 +
54 61 /** 纯函数:把按时间排好序的段切成若干会话。 */
55 62 fun groupSessions(segments: List<Segment>, gapMs: Long): List<List<Segment>> {
56 63 val out = mutableListOf<MutableList<Segment>>()
... ... @@ -71,6 +78,19 @@ class SummaryWorker(
71 78 private val shortFmt = SimpleDateFormat("MM-dd HH:mm", Locale.US)
72 79  
73 80 override suspend fun doWork(): Result {
  81 + if (!isRunning.compareAndSet(false, true)) {
  82 + Log.i(TAG, "another run in progress, skip")
  83 + return Result.success()
  84 + }
  85 + return try {
  86 + runSessions()
  87 + } finally {
  88 + isRunning.set(false)
  89 + }
  90 + }
  91 +
  92 + private suspend fun runSessions(): Result {
  93 + val runStart = System.currentTimeMillis()
74 94 val now = inputData.getLong(INPUT_NOW_TS, System.currentTimeMillis())
75 95 val prefs = applicationContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
76 96 val gapMs = inputData.getLong(INPUT_GAP_MS, 0L).takeIf { it > 0 }
... ... @@ -111,6 +131,11 @@ class SummaryWorker(
111 131 // 已总结过(时间范围重叠)跳过。
112 132 if (existing.any { it.windowStartTs <= end && it.windowEndTs >= start }) { skipped++; continue }
113 133  
  134 + if (System.currentTimeMillis() - runStart > RUN_BUDGET_MS) {
  135 + SummaryProgress.line("⏸ 本轮时间到,剩余会话下次继续")
  136 + break
  137 + }
  138 +
114 139 val userPrompt = buildPrompt(repo, session, speakerNames, start, end)
115 140 if (userPrompt == null) { skipped++; continue } // 整段无有效转写
116 141  
... ... @@ -142,7 +167,9 @@ class SummaryWorker(
142 167  
143 168 SummaryProgress.finish("完成:$made 篇生成,$skipped 个会话跳过(共 ${sessions.size})")
144 169 Log.i(TAG, "done: ${sessions.size} sessions, $made summarized (gap=${gapMs / 60_000}min)")
145   - return if (anyFailed && made == 0) Result.retry() else Result.success()
  170 + // 不 retry:失败/未处理的会话下一轮 periodic 或手动「现在跑一次」会重新捡,
  171 + // 避免 502 后 WorkManager 死循环 hammering。
  172 + return Result.success()
146 173 }
147 174  
148 175 /** 拼 prompt;若会话内没有任何有效转写返回 null。 */
... ...