Skip to content

Comments

Support multi-process debugging: sync breakpoints and coordinate instances #1172

Draft
st0012 wants to merge 5 commits intoruby:masterfrom
Shopify:support-multi-process-breakpoints
Draft

Support multi-process debugging: sync breakpoints and coordinate instances #1172
st0012 wants to merge 5 commits intoruby:masterfrom
Shopify:support-multi-process-breakpoints

Conversation

@st0012
Copy link
Member

@st0012 st0012 commented Feb 21, 2026

Problem 1: Breakpoints not shared between forked processes (#714)

Closes #714

Summary

When a Ruby app forks workers (Puma, Unicorn), breakpoints set in the debugger only fire in some workers. Users must toggle breakpoints multiple times to get them to register, and hits are inconsistent.

Cause

In fork_mode: :both (the default), after fork() each process gets an independent copy of @bps. The existing ProcessGroup flock only serializes which process talks to the debugger — it never synchronizes breakpoint state.

Solution

Store serialized breakpoint specs in a shared JSON tempfile alongside the existing flock tempfile. Publish on subsession leave, check on subsession enter. LineBreakpoint, CatchBreakpoint, and MethodBreakpoint are synced as descriptors. Writes are atomic (tmp file + File.rename).


Problem 2: Multiple debugger instances competing for STDIN

Summary

When running parallel test workers (parallel_tests, ci-queue), all workers that hit debugger enter the debug prompt simultaneously. Output is clobbered and input goes to random processes.

Cause

Parallel test runners fork workers before the debugger loads. Each worker creates its own SESSION with no shared coordination — they all compete for the same STDIN.

Solution

Add a well-known lock file keyed by process group ID. On enter_subsession, acquire with blocking flock(LOCK_EX). The lock is held for the worker's entire session and released by the kernel on process exit. Skipped when MultiProcessGroup is active (fork_mode: :both already handles coordination).

…ances

Two fixes for debugging multi-process Ruby applications:

1. Breakpoint synchronization across forked processes (fixes ruby#714):
   Store serialized breakpoint specs in a shared JSON tempfile alongside
   the existing flock tempfile. Publish on subsession leave, check on
   subsession enter, and in the socket reader retry paths for both DAP
   and console protocols. Breakpoints define to_sync_data for
   serialization. Only LineBreakpoint and CatchBreakpoint are synced.

2. Coordination of independent debugger instances:
   When parallel test runners fork workers before the debugger loads,
   each worker gets its own SESSION with no coordination. Add a
   well-known lock file keyed by process group ID
   (/tmp/ruby-debug-{uid}-pgrp-{getpgrp}.lock) that all sibling
   instances discover automatically. On enter_subsession, acquire
   the lock (blocking flock) so only one process enters the debugger
   at a time. While blocked, no prompt is shown and IRB/Reline never
   reads STDIN.
@launchable-app
Copy link

launchable-app bot commented Feb 21, 2026

2/707 Tests Failed

/home/runner/work/debug/debug/test/protocol/hover_raw_dap_test.rb#test_hover_works_correctly
-------------------------
| All Protocol Messages |
-------------------------

V>D {"seq":1,"command":"initialize","arguments":{"clientID":"vscode","clientName":"Visual Studio Code","adapterID":"rdbg","pathFormat":"path","linesStartAt1":true,"columnsStartAt1":true,"supportsVariableType":true,"supportsVariablePaging":true,"supportsRunInTerminalRequest":true,"locale":"en-us","supportsProgressReporting":true,"supportsInvalidatedEvent":true,"supportsMemoryReferences":true},"type":"request"}
V>D {"seq":2,"command":"attach","arguments":{"type":"rdbg","name":"Attach with rdbg","request":"attach","rdbgPath":"/home/runner/work/debug/debug/exe/rdbg","debugPort":"/var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/ruby-debug-sock-501/ruby-debug-naotto-8845","autoAttach":true,"__sessionId":"141d9c79-3669-43ec-ac1f-e62598c5a65a"},"type":"request"}
V>D {"seq":3,"command":"setFunctionBreakpoints","arguments":{"breakpoints":[]},"type":"request"}
V>D {"seq":4,"command":"setExceptionBreakpoints","arguments":{"filters":[],"filterOptions":[{"filterId":"RuntimeError"}]},"type":"request"}
V>D {"seq":5,"command":"configurationDone","type":"request"}
V<D {"type":"response","command":"initialize","request_seq":1,"success":true,"message":"Success","body":{"supportsConfigurationDoneRequest":true,"supportsFunctionBreakpoints":true,"supportsConditionalBreakpoints":true,"supportTerminateDebuggee":true,"supportsTerminateRequest":true,"exceptionBreakpointFilters":[{"filter":"any","label":"rescue any exception","supportsCondition":true},{"filter":"RuntimeError","label":"rescue RuntimeError","supportsCondition":true}],"supportsExceptionFilterOptions":true,"supportsStepBack":true,"supportsEvaluateForHovers":true,"supportsCompletionsRequest":true},"seq":1}
V<D {"type":"event","event":"initialized","seq":2}
V<D {"type":"event","event":"output","body":{"category":"console","output":"Ruby REPL: You can run any Ruby expression here.\nNote that output to the STDOUT/ERR printed on the TERMINAL.\n[experimental]\n  `,COMMAND` runs `COMMAND` debug command (ex: `,info`).\n  `,help` to list all debug commands.\n"},"seq":3}
V<D {"type":"response","command":"attach","request_seq":2,"success":true,"message":"Success","seq":4}
V<D {"type":"response","command":"setFunctionBreakpoints","request_seq":3,"success":true,"message":"Success","seq":5}
V<D {"type":"response","command":"setExceptionBreakpoints","request_seq":4,"success":true,"message":"Success","body":{"breakpoints":[{"verified":true,"message":"#<DEBUGGER__::CatchBreakpoint:0x00007fec18136b98 @pat=\"RuntimeError\", @key=[:catch, \"RuntimeError\"], @last_exc=nil, @deleted=false, @cond=nil, @command=nil, @path=nil, @tp=#<TracePoint:enabled>>"}]},"seq":6}
V<D {"type":"response","command":"configurationDone","request_seq":5,"success":true,"message":"Success","seq":7}
V<D {"type":"event","event":"stopped","body":{"reason":"pause","threadId":1,"allThreadsStopped":true},"seq":8}
V>D {"seq":6,"command":"threads","type":"request"}
V<D {"type":"response","command":"threads","request_seq":6,"success":true,"message":"Success","body":{"threads":[{"id":1,"name":"#1 /tmp/debug-20260223-2479-5s8vsw.rb:1:in `<main>'"}]},"seq":9}
V>D {"seq":7,"command":"threads","type":"request"}
V<D {"type":"response","command":"threads","request_seq":7,"success":true,"message":"Success","body":{"threads":[{"id":1,"name":"#1 /tmp/debug-20260223-2479-5s8vsw.rb:1:in `<main>'"}]},"seq":10}
V>D {"seq":8,"command":"stackTrace","arguments":{"threadId":1,"startFrame":0,"levels":20},"type":"request"}
V<D {"type":"response","command":"stackTrace","request_seq":8,"success":true,"message":"Success","body":{"stackFrames":[{"id":1,"name":"<main>","line":1,"column":1,"source":{"name":"debug-20260223-2479-5s8vsw.rb","path":"/tmp/debug-20260223-2479-5s8vsw.rb","sourceReference":0}}],"totalFrames":1},"seq":11}
V>D {"seq":9,"command":"scopes","arguments":{"frameId":1},"type":"request"}
V<D {"type":"response","command":"scopes","request_seq":9,"success":true,"message":"Success","body":{"scopes":[{"name":"Local variables","presentationHint":"locals","namedVariables":5,"indexedVariables":0,"expensive":false,"variablesReference":2},{"name":"Global variables","presentationHint":"globals","variablesReference":1,"namedVariables":41,"indexedVariables":0,"expensive":false}]},"seq":12}
V>D {"seq":10,"command":"variables","arguments":{"variablesReference":2},"type":"request"}
V<D {"type":"response","command":"variables","request_seq":10,"success":true,"message":"Success","body":{"variables":[{"name":"%self","value":"main","type":"Object","variablesReference":3,"indexedVariables":0,"namedVariables":1},{"name":"a","value":"nil","type":"NilClass","variablesReference":4,"indexedVariables":0,"namedVariables":1},{"name":"b","value":"nil","type":"NilClass","variablesReference":5,"indexedVariables":0,"namedVariables":1},{"name":"c","value":"nil","type":"NilClass","variablesReference":6,"indexedVariables":0,"namedVariables":1},{"name":"d","value":"nil","type":"NilClass","variablesReference":7,"indexedVariables":0,"namedVariables":1},{"name":"e","value":"nil","type":"NilClass","variablesReference":8,"indexedVariables":0,"namedVariables":1}]},"seq":13}
V>D {"seq":11,"command":"setBreakpoints","arguments":{"source":{"name":"target.rb","path":"/tmp/debug-20260223-2479-5s8vsw.rb","sourceReference":0},"lines":[4],"breakpoints":[{"line":4}],"sourceModified":false},"type":"request"}
V<D {"type":"response","command":"setBreakpoints","request_seq":11,"success":true,"message":"Success","body":{"breakpoints":[{"verified":true}]},"seq":14}
V>D {"seq":12,"command":"continue","arguments":{"threadId":1},"type":"request"}
V<D {"type":"response","command":"continue","request_seq":12,"success":true,"message":"Success","body":{"allThreadsContinued":true},"seq":15}
V<D {"type":"event","event":"stopped","body":{"reason":"breakpoint","description":" BP - Line  /tmp/debug-20260223-2479-5s8vsw.rb:4 (line)","text":" BP - Line  /tmp/debug-20260223-2479-5s8vsw.rb:4 (line)","threadId":1,"allThreadsStopped":true},"seq":16}
V>D {"seq":13,"command":"threads","type":"request"}
V<D {"type":"response","command":"threads","request_seq":13,"success":true,"message":"Success","body":{"threads":[{"id":1,"name":"#1 /tmp/debug-20260223-2479-5s8vsw.rb:4:in `<main>'"}]},"seq":17}
V>D {"seq":14,"command":"stackTrace","arguments":{"threadId":1,"startFrame":0,"levels":20},"type":"request"}
V<D {"type":"response","command":"stackTrace","request_seq":14,"success":true,"message":"Success","body":{"stackFrames":[{"id":2,"name":"<main>","line":4,"column":1,"source":{"name":"debug-20260223-2479-5s8vsw.rb","path":"/tmp/debug-20260223-2479-5s8vsw.rb","sourceReference":0}}],"totalFrames":1},"seq":18}
V>D {"seq":15,"command":"scopes","arguments":{"frameId":2},"type":"request"}
V<D {"type":"response","command":"scopes","request_seq":15,"success":true,"message":"Success","body":{"scopes":[{"name":"Local variables","presentationHint":"locals","namedVariables":5,"indexedVariables":0,"expensive":false,"variablesReference":9},{"name":"Global variables","presentationHint":"globals","variablesReference":1,"namedVariables":41,"indexedVariables":0,"expensive":false}]},"seq":19}
V>D {"seq":16,"command":"variables","arguments":{"variablesReference":9},"type":"request"}
V<D {"type":"response","command":"variables","request_seq":16,"success":true,"message":"Success","body":{"variables":[{"name":"%self","value":"main","type":"Object","variablesReference":10,"indexedVariables":0,"namedVariables":1},{"name":"a","value":"1","type":"Integer","variablesReference":11,"indexedVariables":0,"namedVariables":1},{"name":"b","value":"2","type":"Integer","variablesReference":12,"indexedVariables":0,"namedVariables":1},{"name":"c","value":"3","type":"Integer","variablesReference":13,"indexedVariables":0,"namedVariables":1},{"name":"d","value":"nil","type":"NilClass","variablesReference":14,"indexedVariables":0,"namedVariables":1},{"name":"e","value":"nil","type":"NilClass","variablesReference":15,"indexedVariables":0,"namedVariables":1}]},"seq":20}
V>D {"seq":17,"command":"evaluate","arguments":{"expression":"b","frameId":2,"context":"hover"},"type":"request"}
V<D {"type":"response","command":"evaluate","request_seq":17,"success":true,"message":"Success","body":{"result":"2","type":"Integer","variablesReference":16,"indexedVariables":0,"namedVariables":1},"seq":21}
V>D {"seq":18,"command":"variables","arguments":{"variablesReference":16},"type":"request"}
V<D {"type":"response","command":"variables","request_seq":18,"success":true,"message":"Success","body":{"variables":[{"name":"#class","value":"Integer","type":"Class","variablesReference":17,"indexedVariables":0,"namedVariables":1}]},"seq":22}
V>D {"seq":19,"command":"variables","arguments":{"variablesReference":17},"type":"request"}
V<D {"type":"response","command":"variables","request_seq":19,"success":true,"message":"Success","body":{"variables":[{"name":"#class","value":"Class","type":"Class","variablesReference":18,"indexedVariables":0,"namedVariables":1},{"name":"%ancestors","value":"[JSON::Ext::Generator::GeneratorMethods::Integer, Numeric, Comparable, #<Module:0x000055cf7a12cec0>, Object, JSON::Ext::Generator::GeneratorMethods::Object, DEBUGGER__::TrapIntercep...","type":"Array","variablesReference":19,"indexedVariables":11,"namedVariables":0}]},"seq":23}

--------------------------
| Last Protocol Messages |
--------------------------

{
  "seq": 18,
  "command": "variables",
  "arguments": {
    "variablesReference": 16
  },
  "type": "request"
}
{
  "type": "response",
  "command": "variables",
  "request_seq": 18,
  "success": true,
  "message": "Success",
  "body": {
    "variables": [
      {
        "name": "#class",
        "value": "Integer",
        "type": "Class",
        "variablesReference": 17,
        "indexedVariables": 0,
        "namedVariables": 1
      }
    ]
  },
  "seq": 22
}
{
  "seq": 19,
  "command": "variables",
  "arguments": {
    "variablesReference": 17
  },
  "type": "request"
}
{
  "type": "response",
  "command": "variables",
  "request_seq": 19,
  "success": true,
  "message": "Success",
  "body": {
    "variables": [
      {
        "name": "#class",
        "value": "Class",
        "type": "Class",
        "variablesReference": 18,
        "indexedVariables": 0,
        "namedVariables": 1
      },
      {
        "name": "%ancestors",
        "value": "[JSON::Ext::Generator::GeneratorMethods::Integer, Numeric, Comparable, #<Module:0x000055cf7a12cec0>, Object, JSON::Ext::Generator::GeneratorMethods::Object, DEBUGGER__::TrapIntercep...",
        "type": "Array",
        "variablesReference": 19,
        "indexedVariables": 11,
        "namedVariables": 0
      }
    ]
  },
  "seq": 23
}

--------------------
| Debuggee Session |
--------------------

> DEBUGGER: Debugger can attach via UNIX domain socket (/run/user/1001/rdbg-2479-33)
> DEBUGGER: wait for debugger connection...
> DEBUGGER: Connected.


-------------------
| Failure Message |
-------------------

expected:
{
  "type": "response",
  "command": "variables",
  "request_seq": 19,
  "success": true,
  "message": "Success",
  "body": {
    "variables": [
      {
        "name": "#class",
        "value": "Class",
        "type": "Class",
        "variablesReference": 18,
        "indexedVariables": 0,
        "namedVariables": "(?-mix:\\d+)"
      },
      {
        "name": "%ancestors",
        "value": "(?-mix:JSON::Ext::Generator::GeneratorMethods::Integer)",
        "type": "Array",
        "variablesReference": 19,
        "indexedVariables": "(?-mix:(9|10))",
        "namedVariables": "(?-mix:\\d+)"
      }
    ]
  }
}

result:
{
  "type": "response",
  "command": "variables",
  "request_seq": 19,
  "success": true,
  "message": "Success",
  "body": {
    "variables": [
      {
        "name": "#class",
        "value": "Class",
        "type": "Class",
        "variablesReference": 18,
        "indexedVariables": 0,
        "namedVariables": 1
      },
      {
        "name": "%ancestors",
        "value": "[JSON::Ext::Generator::GeneratorMethods::Integer, Numeric, Comparable, #<Module:0x000055cf7a12cec0>, Object, JSON::Ext::Generator::GeneratorMethods::Object, DEBUGGER__::TrapIntercep...",
        "type": "Array",
        "variablesReference": 19,
        "indexedVariables": 11,
        "namedVariables": 0
      }
    ]
  },
  "seq": 23
}.
</(9|10)/> was expected to be =~
<"11">.
/home/runner/work/debug/debug/test/protocol/hover_raw_dap_test.rb#test_1641198331
-------------------------
| All Protocol Messages |
-------------------------

V>D {"seq":1,"command":"initialize","arguments":{"clientID":"vscode","clientName":"Visual Studio Code","adapterID":"rdbg","pathFormat":"path","linesStartAt1":true,"columnsStartAt1":true,"supportsVariableType":true,"supportsVariablePaging":true,"supportsRunInTerminalRequest":true,"locale":"en-us","supportsProgressReporting":true,"supportsInvalidatedEvent":true,"supportsMemoryReferences":true},"type":"request"}
V>D {"seq":2,"command":"attach","arguments":{"type":"rdbg","name":"Attach with rdbg","request":"attach","rdbgPath":"/home/runner/work/debug/debug/exe/rdbg","debugPort":"/var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/ruby-debug-sock-501/ruby-debug-naotto-8845","autoAttach":true,"__sessionId":"141d9c79-3669-43ec-ac1f-e62598c5a65a"},"type":"request"}
V>D {"seq":3,"command":"setFunctionBreakpoints","arguments":{"breakpoints":[]},"type":"request"}
V>D {"seq":4,"command":"setExceptionBreakpoints","arguments":{"filters":[],"filterOptions":[{"filterId":"RuntimeError"}]},"type":"request"}
V>D {"seq":5,"command":"configurationDone","type":"request"}
V<D {"type":"response","command":"initialize","request_seq":1,"success":true,"message":"Success","body":{"supportsConfigurationDoneRequest":true,"supportsFunctionBreakpoints":true,"supportsConditionalBreakpoints":true,"supportTerminateDebuggee":true,"supportsTerminateRequest":true,"exceptionBreakpointFilters":[{"filter":"any","label":"rescue any exception","supportsCondition":true},{"filter":"RuntimeError","label":"rescue RuntimeError","supportsCondition":true}],"supportsExceptionFilterOptions":true,"supportsStepBack":true,"supportsEvaluateForHovers":true,"supportsCompletionsRequest":true},"seq":1}
V<D {"type":"event","event":"initialized","seq":2}
V<D {"type":"event","event":"output","body":{"category":"console","output":"Ruby REPL: You can run any Ruby expression here.\nNote that output to the STDOUT/ERR printed on the TERMINAL.\n[experimental]\n  `,COMMAND` runs `COMMAND` debug command (ex: `,info`).\n  `,help` to list all debug commands.\n"},"seq":3}
V<D {"type":"response","command":"attach","request_seq":2,"success":true,"message":"Success","seq":4}
V<D {"type":"response","command":"setFunctionBreakpoints","request_seq":3,"success":true,"message":"Success","seq":5}
V<D {"type":"response","command":"setExceptionBreakpoints","request_seq":4,"success":true,"message":"Success","body":{"breakpoints":[{"verified":true,"message":"#<DEBUGGER__::CatchBreakpoint:0x00007f70f815fdb8 @pat=\"RuntimeError\", @key=[:catch, \"RuntimeError\"], @last_exc=nil, @deleted=false, @cond=nil, @command=nil, @path=nil, @tp=#<TracePoint:enabled>>"}]},"seq":6}
V<D {"type":"response","command":"configurationDone","request_seq":5,"success":true,"message":"Success","seq":7}
V<D {"type":"event","event":"stopped","body":{"reason":"pause","threadId":1,"allThreadsStopped":true},"seq":8}
V>D {"seq":6,"command":"threads","type":"request"}
V<D {"type":"response","command":"threads","request_seq":6,"success":true,"message":"Success","body":{"threads":[{"id":1,"name":"#1 /tmp/debug-20260223-2479-4kca3o.rb:1:in `<main>'"}]},"seq":9}
V>D {"seq":7,"command":"threads","type":"request"}
V<D {"type":"response","command":"threads","request_seq":7,"success":true,"message":"Success","body":{"threads":[{"id":1,"name":"#1 /tmp/debug-20260223-2479-4kca3o.rb:1:in `<main>'"}]},"seq":10}
V>D {"seq":8,"command":"stackTrace","arguments":{"threadId":1,"startFrame":0,"levels":20},"type":"request"}
V<D {"type":"response","command":"stackTrace","request_seq":8,"success":true,"message":"Success","body":{"stackFrames":[{"id":1,"name":"<main>","line":1,"column":1,"source":{"name":"debug-20260223-2479-4kca3o.rb","path":"/tmp/debug-20260223-2479-4kca3o.rb","sourceReference":0}}],"totalFrames":1},"seq":11}
V>D {"seq":9,"command":"scopes","arguments":{"frameId":1},"type":"request"}
V<D {"type":"response","command":"scopes","request_seq":9,"success":true,"message":"Success","body":{"scopes":[{"name":"Local variables","presentationHint":"locals","namedVariables":1,"indexedVariables":0,"expensive":false,"variablesReference":2},{"name":"Global variables","presentationHint":"globals","variablesReference":1,"namedVariables":41,"indexedVariables":0,"expensive":false}]},"seq":12}
V>D {"seq":10,"command":"variables","arguments":{"variablesReference":2},"type":"request"}
V<D {"type":"response","command":"variables","request_seq":10,"success":true,"message":"Success","body":{"variables":[{"name":"%self","value":"main","type":"Object","variablesReference":3,"indexedVariables":0,"namedVariables":1},{"name":"ghi","value":"nil","type":"NilClass","variablesReference":4,"indexedVariables":0,"namedVariables":1}]},"seq":13}
V>D {"seq":11,"command":"setBreakpoints","arguments":{"source":{"name":"target.rb","path":"/tmp/debug-20260223-2479-4kca3o.rb","sourceReference":0},"lines":[29],"breakpoints":[{"line":29}],"sourceModified":false},"type":"request"}
V<D {"type":"response","command":"setBreakpoints","request_seq":11,"success":true,"message":"Success","body":{"breakpoints":[{"verified":true}]},"seq":14}
V>D {"seq":12,"command":"continue","arguments":{"threadId":1},"type":"request"}
V<D {"type":"response","command":"continue","request_seq":12,"success":true,"message":"Success","body":{"allThreadsContinued":true},"seq":15}
V<D {"type":"event","event":"stopped","body":{"reason":"breakpoint","description":" BP - Line  /tmp/debug-20260223-2479-4kca3o.rb:29 (line)","text":" BP - Line  /tmp/debug-20260223-2479-4kca3o.rb:29 (line)","threadId":1,"allThreadsStopped":true},"seq":16}
V>D {"seq":13,"command":"threads","type":"request"}
V<D {"type":"response","command":"threads","request_seq":13,"success":true,"message":"Success","body":{"threads":[{"id":1,"name":"#1 /tmp/debug-20260223-2479-4kca3o.rb:29:in `<main>'"}]},"seq":17}
V>D {"seq":14,"command":"stackTrace","arguments":{"threadId":1,"startFrame":0,"levels":20},"type":"request"}
V<D {"type":"response","command":"stackTrace","request_seq":14,"success":true,"message":"Success","body":{"stackFrames":[{"id":2,"name":"<main>","line":29,"column":1,"source":{"name":"debug-20260223-2479-4kca3o.rb","path":"/tmp/debug-20260223-2479-4kca3o.rb","sourceReference":0}}],"totalFrames":1},"seq":18}
V>D {"seq":15,"command":"scopes","arguments":{"frameId":2},"type":"request"}
V<D {"type":"response","command":"scopes","request_seq":15,"success":true,"message":"Success","body":{"scopes":[{"name":"Local variables","presentationHint":"locals","namedVariables":1,"indexedVariables":0,"expensive":false,"variablesReference":5},{"name":"Global variables","presentationHint":"globals","variablesReference":1,"namedVariables":41,"indexedVariables":0,"expensive":false}]},"seq":19}
V>D {"seq":16,"command":"variables","arguments":{"variablesReference":5},"type":"request"}
V<D {"type":"response","command":"variables","request_seq":16,"success":true,"message":"Success","body":{"variables":[{"name":"%self","value":"main","type":"Object","variablesReference":6,"indexedVariables":0,"namedVariables":1},{"name":"ghi","value":"nil","type":"NilClass","variablesReference":7,"indexedVariables":0,"namedVariables":1}]},"seq":20}
V>D {"seq":17,"command":"evaluate","arguments":{"expression":"Abc","frameId":2,"context":"hover"},"type":"request"}
V<D {"type":"response","command":"evaluate","request_seq":17,"success":true,"message":"Success","body":{"result":"Abc","type":"Module","variablesReference":8,"indexedVariables":0,"namedVariables":1},"seq":21}
V>D {"seq":18,"command":"variables","arguments":{"variablesReference":8},"type":"request"}
V<D {"type":"response","command":"variables","request_seq":18,"success":true,"message":"Success","body":{"variables":[{"name":"#class","value":"Module","type":"Class","variablesReference":9,"indexedVariables":0,"namedVariables":1},{"name":"%ancestors","value":"[]","type":"Array","variablesReference":0,"indexedVariables":0,"namedVariables":0}]},"seq":22}
V>D {"seq":19,"command":"evaluate","arguments":{"expression":"Abc::Def123","frameId":2,"context":"hover"},"type":"request"}
V<D {"type":"response","command":"evaluate","request_seq":19,"success":true,"message":"Success","body":{"result":"Abc::Def123","type":"Class","variablesReference":10,"indexedVariables":0,"namedVariables":1},"seq":23}
V>D {"seq":20,"command":"variables","arguments":{"variablesReference":10},"type":"request"}
V<D {"type":"response","command":"variables","request_seq":20,"success":true,"message":"Success","body":{"variables":[{"name":"#class","value":"Class","type":"Class","variablesReference":11,"indexedVariables":0,"namedVariables":1},{"name":"%ancestors","value":"[#<Module:0x0000563f36a7e628>, Object, JSON::Ext::Generator::GeneratorMethods::Object, DEBUGGER__::TrapInterceptor, DEBUGGER__::ForkInterceptor, PP::ObjectMixin, Kernel, BasicObject...","type":"Array","variablesReference":12,"indexedVariables":8,"namedVariables":0}]},"seq":24}

--------------------------
| Last Protocol Messages |
--------------------------

{
  "seq": 19,
  "command": "evaluate",
  "arguments": {
    "expression": "Abc::Def123",
    "frameId": 2,
    "context": "hover"
  },
  "type": "request"
}
{
  "type": "response",
  "command": "evaluate",
  "request_seq": 19,
  "success": true,
  "message": "Success",
  "body": {
    "result": "Abc::Def123",
    "type": "Class",
    "variablesReference": 10,
    "indexedVariables": 0,
    "namedVariables": 1
  },
  "seq": 23
}
{
  "seq": 20,
  "command": "variables",
  "arguments": {
    "variablesReference": 10
  },
  "type": "request"
}
{
  "type": "response",
  "command": "variables",
  "request_seq": 20,
  "success": true,
  "message": "Success",
  "body": {
    "variables": [
      {
        "name": "#class",
        "value": "Class",
        "type": "Class",
        "variablesReference": 11,
        "indexedVariables": 0,
        "namedVariables": 1
      },
      {
        "name": "%ancestors",
        "value": "[#<Module:0x0000563f36a7e628>, Object, JSON::Ext::Generator::GeneratorMethods::Object, DEBUGGER__::TrapInterceptor, DEBUGGER__::ForkInterceptor, PP::ObjectMixin, Kernel, BasicObject...",
        "type": "Array",
        "variablesReference": 12,
        "indexedVariables": 8,
        "namedVariables": 0
      }
    ]
  },
  "seq": 24
}

--------------------
| Debuggee Session |
--------------------

> DEBUGGER: Debugger can attach via UNIX domain socket (/run/user/1001/rdbg-2479-34)
> DEBUGGER: wait for debugger connection...
> DEBUGGER: Connected.


-------------------
| Failure Message |
-------------------

expected:
{
  "type": "response",
  "command": "variables",
  "request_seq": 20,
  "success": true,
  "message": "Success",
  "body": {
    "variables": [
      {
        "name": "#class",
        "value": "Class",
        "type": "Class",
        "variablesReference": 11,
        "indexedVariables": 0,
        "namedVariables": "(?-mix:\\d+)"
      },
      {
        "name": "%ancestors",
        "value": "(?-mix:Object)",
        "type": "Array",
        "variablesReference": 12,
        "indexedVariables": "(?-mix:(6|7))",
        "namedVariables": "(?-mix:\\d+)"
      }
    ]
  }
}

result:
{
  "type": "response",
  "command": "variables",
  "request_seq": 20,
  "success": true,
  "message": "Success",
  "body": {
    "variables": [
      {
        "name": "#class",
        "value": "Class",
        "type": "Class",
        "variablesReference": 11,
        "indexedVariables": 0,
        "namedVariables": 1
      },
      {
        "name": "%ancestors",
        "value": "[#<Module:0x0000563f36a7e628>, Object, JSON::Ext::Generator::GeneratorMethods::Object, DEBUGGER__::TrapInterceptor, DEBUGGER__::ForkInterceptor, PP::ObjectMixin, Kernel, BasicObject...",
        "type": "Array",
        "variablesReference": 12,
        "indexedVariables": 8,
        "namedVariables": 0
      }
    ]
  },
  "seq": 24
}.
</(6|7)/> was expected to be =~
<"8">.

[-> View Test suite health in main branch]

Tests for breakpoint sync (fork_bp_sync_test.rb):
- Breakpoint set/deleted after fork syncs to child
- Multiple children receive synced breakpoints
- Catch breakpoint syncs to child
- Late-forked child catches up
- Stress test with binding.break

Tests for well-known lock (wk_lock_test.rb):
- Single-process debugging unaffected
- fork_mode: :both uses ProcessGroup not well-known lock
- Independent workers serialized by well-known lock
@st0012 st0012 force-pushed the support-multi-process-breakpoints branch from 1395d43 to cdd7e8e Compare February 21, 2026 15:40
- Fix version counter drift: read file version before writing to
  prevent processes from missing each other's updates
- Add MethodBreakpoint sync support (to_sync_data + reconciliation)
- Fix CatchBreakpoint sync to preserve command and path attributes
- Add syncable? predicate to avoid unnecessary hash allocation
- Add type validation in create_bp_from_spec for defense-in-depth
- Use Dir.tmpdir instead of hardcoded /tmp for portability
- Set explicit 0600 permissions on temp state file writes
- Broaden error handling to SystemCallError in read/write state
- Add error handling to ensure_wk_lock! for disk-full/read-only
- Publish breakpoint changes on DAP disconnect
When multiple independent workers share the well-known lock, releasing
it on step/next/finish allowed a sibling worker to grab the lock before
the stepping worker could re-enter its subsession. This caused the user
to need 2 next commands to actually advance — the first one would
inadvertently drive the other worker.

Only release wk_lock on :continue, which is expected to run for an
extended period. Step commands hold the lock so the same worker
immediately re-enters without yielding.
The previous fix only held the lock during step commands, but continue
between breakpoints had the same ping-pong problem — another worker
could grab the lock before the current one hit its next breakpoint.

Now the wk_lock is never released in leave_subsession. Each worker
keeps exclusive debugger access for its entire lifetime. Other workers
queue up and get their turn when the current one exits. The kernel
releases flock automatically on process exit.
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.

Line breakpoints aren't shared between processes

1 participant