Skip to content

fix: harden background-thread coalesced runtime work across reload#64

Merged
huhuanming merged 6 commits into
mainfrom
codex/fix-background-thread-coalesced-drain
Jun 12, 2026
Merged

fix: harden background-thread coalesced runtime work across reload#64
huhuanming merged 6 commits into
mainfrom
codex/fix-background-thread-coalesced-drain

Conversation

@huhuanming

Copy link
Copy Markdown
Contributor

Summary

加固 background-thread 的 coalesced runtime work 机制,修复 reload 期间的若干线程/状态问题。

  • fix: coalesce background thread runtime work — 将 runtime work 合并入队,减少跨线程调度开销
  • fix: harden background-thread coalesced drain against threading bugs — 针对 coalesced drain 的线程安全加固
  • fix: prevent bg-eval replay and latch-stall across reload — 修复 reload 时 bg-eval 重放与 drain latch 卡死;reload 过程中保留 queue.items(main runtime 无 JS 重试网),仅复位 drain latch 让恢复后的 runtime 继续 drain
  • chore: bump version to 3.0.63

已合并最新 main(冲突以本分支为准)。

Test plan

  • Android background-thread reload 场景验证(重放/卡死)
  • iOS 回归

originalix and others added 6 commits June 12, 2026 10:49
Audit of the coalesced-drain change surfaced five concurrency issues,
all addressed here:

- gBgTimerExecutor was written without gTimerMutex while every reader
  holds it — a data race (UB) on std::function. Guard the assignment.
- A transient ptr==0 drain leak-cleared the ENTIRE coalesced queue,
  permanently dropping main-runtime SharedRPC deliveries (no JS retry
  net). Keep queue.items intact; only disarm the drain latch.
- nativeInvalidateSharedRpc left drainScheduled==true, stranding all
  future work after reload. Track the outstanding drain via
  scheduledDrainWorkId and clear the orphaned gPendingWork entry.
- Recovery relied on a later enqueue to re-arm the drain, so items
  carried over a ptr==0 reload could strand until nativeDestroy. Make
  recovery structural: re-arm a drain in nativeInstallSharedBridge when
  the freshly installed runtime's queue is non-empty.
- Closed a stale-id race where a concurrent invalidate empties the
  queue between drainRuntimeWorkQueue's remaining>0 check and the
  reschedule, by guarding scheduleRuntimeDrain against an empty queue.
Follow-up to the coalesced-drain hardening, addressing two reload-window
issues found by a second audit pass:

- Keeping queue.items across a ptr==0 reload (so the recovered runtime
  can drain them) let a background segment-eval lambda be replayed by
  install-recover AFTER drainPendingBgEvals had already settled it as
  retryable NO_RUNTIME — re-running evaluateJavaScript on the segment a
  second time. The eval lambda now treats an already-erased
  gPendingBgEvals entry as 'a drain claimed me, JS will retry' and skips
  the re-evaluation.

- A drain posted just before reload can be silently discarded by the
  dead pre-reload JS thread, so its nativeDropScheduledWork never fires
  and drainScheduled stays stuck true. install-recover gated on
  !drainScheduled would then skip recovery and strand the items. It now
  force-arms a drain on the freshly installed runtime whenever the queue
  is non-empty; a still-live stale drain self-cancels via the empty-queue
  guard in scheduleRuntimeDrain.
…thread-coalesced-drain

# Conflicts:
#	native-modules/react-native-background-thread/android/src/main/cpp/cpp-adapter.cpp
#	native-modules/react-native-background-thread/android/src/main/java/com/backgroundthread/BackgroundThreadManager.kt
@huhuanming huhuanming merged commit 3d7d741 into main Jun 12, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants