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

Aperta
#304 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
5/5
Tempo stimato
Più di una settimana
Idoneità per principianti
20/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Ferma
Stack tecnologico
typescript
Ambito
networking

Direzione di ricerca

Inizia da backend/apps/support/src/lib/chat-api-client.ts e confronta i percorsi PartyWebSocket e native WebSocket descritti nell’issue. Esegui la riproduzione prolungata con [email protected] su Bun v1.3.5, quindi conferma che l’applicazione continui a essere in esecuzione oltre l’intervallo segnalato di 3–4 minuti senza il crash di parallel-GC; la relativa issue di Bun può fornire ulteriore contesto.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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
Lingua principale
TypeScript
Stelle
1.3k
Fork
84
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di cloudflare/partykit

Tutte le issue di cloudflare/partykit

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.