Bug: Bun C++ Exception Crash with PartyWebSocket (partysocket library)

オープン
#304 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
5/5
見積もり時間
1週間以上
初心者へのやさしさ
20/100
issue の種類
バグ
明瞭さ
おおむね明確
活発さ
停滞
技術スタック
typescript
領域
networking

調査の方向性

backend/apps/support/src/lib/chat-api-client.ts から始め、issue で説明されている PartyWebSocket と native WebSocket のパスを比較します。Bun v1.3.5 上で [email protected] を使って長時間の再現を実行し、その後、parallel-GC crash が発生せず、報告されている 3–4 分の間隔を超えてアプリケーションが実行され続けることを確認します。関連する Bun issue が追加のコンテキストを提供する可能性があります。

索引モデルが issue の本文から書いたものです。

説明

Summary

Bun v1.3.5 crashes with a "pure virtual method called" C++ exception after running for 3-4 minutes when using PartyWebSocket from the partysocket library. The crash occurs during JavaScriptCore's parallel garbage collection when marking WebCore/DOM objects.

Environment

  • Bun version: v1.3.5 (1e86cebd)
  • OS: Linux x86_64 (baseline)
  • Kernel: v6.18.0 | glibc v2.42
  • Library: [email protected] (via catalog)
  • Crash time: Consistently after 3-4 minutes of runtime

Stack Trace

Bun v1.3.5 (1e86cebd) on linux x86_64_baseline [AutoCommand]

**panic**: A C++ exception occurred

- JSGlobalObject.zig:854: `Zig__GlobalObject__onCrash`
- compiler_builtins.6c1f39da0e88e811-cgu.247:0: `__cxxabiv1::__terminate`
- ClassInfo.h:124: `WTF::Ref<WTF::SharedTask<void (...)>, WTF::RawPtrTraits<WTF::SharedTask<void (...)> >, WTF::DefaultRefDerefTraits<WTF::SharedTask<void (...)> > > JSC::Subspace::forEachMarkedCellInParallel<JSC::SlotVisitor, void WebCore::DOMGCOutputConstraint::executeImplImpl<JSC::SlotVisitor>(...)::'lambda'(...)::operator(...) const::'lambda'(...)>(...)::'lambda'(...)::operator(...) const::'lambda'(...) const&)::Task::run`
- MarkingConstraint.cpp:96: `JSC::MarkingConstraintSolver::runExecutionThread`
- SlotVisitor.cpp:686: `JSC::SlotVisitor::drainFromShared`
- Heap.cpp:1502: `WTF::SharedTaskFunctor<void (...)::$_1>::run`
- ParallelHelperPool.cpp:110: `WTF::ParallelHelperPool::Thread::work`
- AutomaticThread.cpp:225: `WTF::Detail::CallableWrapper<WTF::AutomaticThread::start(...)::$_0, void>::call`
- Function.h:82: `WTF::wtfThreadEntryPoint`

Root Cause

The crash occurs during JavaScriptCore's parallel garbage collection when marking WebCore::DOMGCOutputConstraint objects. The PartyWebSocket class from the partysocket library creates WebCore/DOM objects that are destroyed while parallel GC threads are trying to mark them, causing the "pure virtual method called" error.

Key observation: The stack trace shows WebCore::DOMGCOutputConstraint, indicating this is related to DOM/WebCore objects, not just WebSocket connections. The partysocket library likely wraps or extends native WebSocket functionality in a way that creates additional WebCore objects that become invalid during GC.

Problematic Code

Before (Causing Crash)
// backend/apps/support/src/lib/chat-api-client.ts
import { WebSocket as PartyWebSocket } from 'partysocket'

// Create WebSocket connection using partysocket (with auto-reconnect)
let sharedWebSocket: PartyWebSocket | null = null
let sharedWsClient: ContractRouterClient<typeof chatContract> | null = null

function getSharedWebSocketClient(): ContractRouterClient<typeof chatContract> {
  // Return existing client if WebSocket is open
  if (
    sharedWsClient &&
    sharedWebSocket &&
    sharedWebSocket.readyState === PartyWebSocket.OPEN
  ) {
    return sharedWsClient
  }

  // Create new WebSocket connection with partysocket
  // partysocket provides automatic reconnection and message buffering
  sharedWebSocket = new PartyWebSocket(getWebSocketUrl())  // ❌ CRASHES HERE

  // Create oRPC client from WebSocket
  const link = new WSLink({
    websocket: sharedWebSocket as any,
  })

  sharedWsClient = createORPCClient(link) as ContractRouterClient<
    typeof chatContract
  >

  sharedWebSocket.addEventListener('close', () => {
    sharedWsClient = null
  })

  return sharedWsClient
}

// Created at module load time - WebSocket created immediately
export const chatApiClient: ContractRouterClient<typeof chatContract> =
  getSharedWebSocketClient()  // ❌ Creates PartyWebSocket immediately
After (Fixed)
// backend/apps/support/src/lib/chat-api-client.ts
// REMOVED: import { WebSocket as PartyWebSocket } from 'partysocket'
// Using native WebSocket instead

// Create WebSocket connection using native WebSocket
let sharedWebSocket: WebSocket | null = null
let sharedWsClient: ContractRouterClient<typeof chatContract> | null = null

function getSharedWebSocketClient(): ContractRouterClient<typeof chatContract> {
  // Return existing client if WebSocket is open
  if (
    sharedWsClient &&
    sharedWebSocket &&
    sharedWebSocket.readyState === WebSocket.OPEN  // ✅ Native WebSocket
  ) {
    return sharedWsClient
  }

  // Create new WebSocket connection with native WebSocket
  // Using native WebSocket instead of PartyWebSocket to avoid GC crashes
  sharedWebSocket = new WebSocket(getWebSocketUrl())  // ✅ Native WebSocket

  // Create oRPC client from WebSocket
  const link = new WSLink({
    websocket: sharedWebSocket as any,
  })

  sharedWsClient = createORPCClient(link) as ContractRouterClient<
    typeof chatContract
  >

  sharedWebSocket.addEventListener('close', () => {
    sharedWsClient = null
  })

  return sharedWsClient
}

// Lazy-load chatApiClient to avoid creating WebSocket at module load time
let _chatApiClient: ContractRouterClient<typeof chatContract> | null = null

export const chatApiClient: ContractRouterClient<typeof chatContract> =
  new Proxy({} as ContractRouterClient<typeof chatContract>, {
    get(_target, prop) {
      if (!_chatApiClient) {
        _chatApiClient = getSharedWebSocketClient()  // ✅ Lazy-loaded
      }
      return (_chatApiClient as any)[prop]
    },
  })

Workaround

Replace PartyWebSocket from partysocket with native WebSocket:

  1. Remove partysocket import:

    // REMOVED: import { WebSocket as PartyWebSocket } from 'partysocket'
    
  2. Use native WebSocket:

    const websocket = new WebSocket(wsUrl)  // Instead of new PartyWebSocket(wsUrl)
    
  3. Lazy-load WebSocket connections to avoid creating them at module load time:

    // Use Proxy to lazy-load instead of creating at module load
    export const chatApiClient = new Proxy({}, {
      get(_target, prop) {
        if (!_chatApiClient) {
          _chatApiClient = getSharedWebSocketClient()
        }
        return (_chatApiClient as any)[prop]
      },
    })
    

Related Issues

Additional Context

The crash is not related to:

  • Native WebSocket usage (works fine)
  • Async iterators with native WebSocket
  • EventPublisher or mergeIterators
  • AbortSignal usage

The crash is specifically related to:

  • PartyWebSocket from partysocket library
  • Objects created by partysocket that reference WebCore/DOM
  • These objects being destroyed during parallel GC marking

Expected Behavior

The application should run indefinitely without crashing. Native WebSocket works correctly, but PartyWebSocket causes crashes after 3-4 minutes.

Actual Behavior

Application crashes with C++ exception after 3-4 minutes when using PartyWebSocket. Replacing with native WebSocket resolves the issue.

Reproduction

  1. Create a WebSocket connection using PartyWebSocket from partysocket
  2. Keep the connection alive for 3-4 minutes
  3. Wait for JavaScriptCore's parallel GC to run
  4. Crash occurs when GC tries to mark WebCore objects created by partysocket

Impact

  • Severity: High - causes application crashes
  • Frequency: 100% reproducible after 3-4 minutes
  • Workaround: Use native WebSocket instead of PartyWebSocket
主要言語
TypeScript
スター
1.3k
フォーク
84
PR マージ指標
30日以内にマージされた PR はありません

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

cloudflare/partykit のほかの issue

cloudflare/partykit の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。