Skip to content

Avoid deadlock with multiple connections when executing code and new connection opens - #294

Merged
felixfbecker merged 12 commits into
xdebug:masterfrom
jonyo:deadlock
Aug 28, 2018
Merged

Avoid deadlock with multiple connections when executing code and new connection opens#294
felixfbecker merged 12 commits into
xdebug:masterfrom
jonyo:deadlock

Conversation

@jonyo

@jonyo jonyo commented Aug 15, 2018

Copy link
Copy Markdown
Contributor

Fixes #164

Original PR: #292

This PR reworks how the original one does things after feedback and as noted below. It made it a lot cleaner to start fresh on a new branch which is why I didn't add the changes on to the original PR...

This introduces a new concept in the xDebug connection: "execution commands": these are commands that result in PHP being executed. The way xDebug works, if one of these commands is run, that connection is hung until the PHP code is done executing (until it gets to the next stopping point). By keeping track of when such commands are still in progress, and making it something that the adapter can check, it allows the adapter to specifically code for when the connection is currently hung. Which allows preventing the deadlock situation described in #164

The adapter also now emits before-execution-command since it results in possibly going into a hung state for a while. The adapter uses that event to send a ContinueEvent, this is just a side advantage that makes the interface more responsive. Before when you click continue, if the PHP code is hung for whatever reason (it is slow, it has a lot to do, etc), it appears that it did not work as it appears to still be stopped on a breakpoint (but it is not, which you can tell by trying to view any of the details in the debug pane). With the change, now when you click continue it immediately changes the process back to "running" state which is more truthful.

EDIT: Since #164 has a lot of comments, here is the tl;dr: deadlock explanation:

  1. Connection 1 makes a second connection with curl.
  2. It sends command to all connections to set breakpoints, and waits for response.
  3. The second connection hangs as it is waiting for connection 1 to set breakpoints, connection 1 hangs without setting those breakpoints because it is waiting for connection 2 to respond and can't do anything until that completes.

This PR makes it not wait for the breakpoints to be set on a connection if the connection is waiting for code to execute. So now connection 2 can finish initializing and run, no more deadlock.

Comment thread src/xdebugConnection.ts Outdated
/** callback that gets called if an error happened while parsing the response */
rejectFn: (error?: Error) => any
/** whether command results in PHP code being executed or not */
isExecuteCommand?: boolean

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this optional?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😨 that was an artifact of how I started out implementing, I started making it an optional parameter on the main _enqueueCommand method. It no longer needs to be optional, I'll change it.

Comment thread src/xdebugConnection.ts Outdated
const command = this._pendingCommands.get(transactionId)!
this._pendingCommands.delete(transactionId)
command.resolveFn(response)
this._pendingExecuteCommand = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be set to false before resolving?

Comment thread src/phpDebug.ts Outdated
try {
const fileUri = convertClientPathToDebugger(args.source.path!, this._args.pathMappings)
const connections = Array.from(this._connections.values())
const hasWaitingConnections = this._waitingConnections.size > 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not clear to me why the existence of a waiting connection is of significance for this behavior. Could you explain?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well as you said, the way I'm doing it now, just skipping the connection altogether if it is blocked, is rather brittle. So my thought was, the deadlock specifically happens because the configuration of a new connection is waiting on the configuration to be initialized, which involves calling this code to set up breakpoints. So if _waitingConnections has something in it, that is when we should "care" that there could potentially be a command blocking it from continuing.

That said, if I change it's behavior to instead do a delayed update instead of just skipping the connection, this would no longer be needed. So if I get that working I'll remove this condition as well.

Comment thread src/phpDebug.ts Outdated
// skip this if there is a connection that is being initialized, and this
// connection is in middle of running PHP code. This avoids a deadlock that
// can happen if the PHP code initializes a new connection that uses xdebug
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simply ignoring this setBreakpoint command seems brittle. The way I would imagine it is to still send the command (queue it) but don't wait for the response (and send a potential error to the debug console).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll see if I can get it to work like that. I was hesitant to try that way as I think it would involve the exact same code (so a lot of duplicated code), just without the await, instead maybe using .then().

I'll try it and see if it works like that.

@jonyo

jonyo commented Aug 15, 2018

Copy link
Copy Markdown
Contributor Author

@felixfbecker Changes in, thanks!

@codecov

codecov Bot commented Aug 15, 2018

Copy link
Copy Markdown

Codecov Report

Merging #294 into master will increase coverage by 0.83%.
The diff coverage is 81.35%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #294      +/-   ##
==========================================
+ Coverage   69.34%   70.17%   +0.83%     
==========================================
  Files           5        5              
  Lines         995     1016      +21     
  Branches      161      161              
==========================================
+ Hits          690      713      +23     
+ Misses        305      303       -2
Impacted Files Coverage Δ
src/phpDebug.ts 66.93% <76.92%> (+0.48%) ⬆️
src/xdebugConnection.ts 84.64% <90%> (+1.58%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 08a5500...31948fa. Read the comment docs.

@jonyo jonyo changed the title Avoid deadlock with multiple connections when executing code and new connection opens WIP Avoid deadlock with multiple connections when executing code and new connection opens Aug 16, 2018
@jonyo jonyo changed the title WIP Avoid deadlock with multiple connections when executing code and new connection opens Avoid deadlock with multiple connections when executing code and new connection opens Aug 16, 2018
@Mancy

Mancy commented Aug 16, 2018

Copy link
Copy Markdown

Thank you Jonyo, you have saved my time with this fix

@macbookandrew

Copy link
Copy Markdown

Hate to ask a noob question, but how can I use this change on my machine?

Comment thread src/phpDebug.ts
try {
await connection.sendBreakpointSetCommand(breakpoint)
// only capture each breakpoint once
if (connectionIndex === 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I simplified the code and noticed that you removed these if checks. Was that intentional?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@felixfbecker Yes, because connectionIndex of 0 could be the connection that is waiting for PHP code to execute, connectionIndex 1 would be the "new" connection that is able to execute right away so is able to set the value in time for it to matter.

Plus all it is doing is setting a value and the value is fixed, it doesn't save time by skipping it if already set.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I understand. There is a slight behaviour change here in that now if any of the connections errored, the breakpoint will be marked us unverified, whereas before, only the first connection determined the fate of the breakpoint. This is fine I guess.

@felixfbecker
felixfbecker merged commit 7fa2d28 into xdebug:master Aug 28, 2018
@felixfbecker

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.12.5 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

towhidabsar pushed a commit to towhidabsar/vscode-php-debug that referenced this pull request Sep 12, 2018
…ebug#294)

This introduces a new concept in the xDebug connection:  "execution commands": these are commands that result in PHP being executed.  The way xDebug works, if one of these commands is run, that connection is hung until the PHP code is done executing (until it gets to the next stopping point).  By keeping track of when such commands are still in progress, and making it something that the adapter can check, it allows the adapter to specifically code for when the connection is currently hung.  Which allows preventing the deadlock situation described in xdebug#164 

The adapter also now emits `before-execution-command` since it results in possibly going into a hung state for a while.  The adapter uses that event to send a `ContinueEvent`, this is just a side advantage that makes the interface more responsive.  Before when you click continue, if the PHP code is hung for whatever reason (it is slow, it has a lot to do, etc), it appears that it did not work as it appears to still be stopped on a breakpoint (but it is not, which you can tell by trying to view any of the details in the debug pane).  With the change, now when you click continue it immediately changes the process back to "running" state which is more truthful.

Since xdebug#164 has a lot of comments, here is the **tl;dr:** deadlock explanation: 
1. Connection 1 makes a second connection with curl.
2. It sends command to all connections to set breakpoints, and waits for response.
3. The second connection hangs as it is waiting for connection 1 to set breakpoints, connection 1 hangs without setting those breakpoints because it is waiting for connection 2 to respond and can't do anything until that completes.

This PR makes it not wait for the breakpoints to be set on a connection if the connection is waiting for code to execute.  So now connection 2 can finish initializing and run, no more deadlock.

Fixes xdebug#164
SidIcarus pushed a commit to towhidabsar/vscode-php-debug that referenced this pull request Oct 9, 2018
…ebug#294)

This introduces a new concept in the xDebug connection:  "execution commands": these are commands that result in PHP being executed.  The way xDebug works, if one of these commands is run, that connection is hung until the PHP code is done executing (until it gets to the next stopping point).  By keeping track of when such commands are still in progress, and making it something that the adapter can check, it allows the adapter to specifically code for when the connection is currently hung.  Which allows preventing the deadlock situation described in xdebug#164 

The adapter also now emits `before-execution-command` since it results in possibly going into a hung state for a while.  The adapter uses that event to send a `ContinueEvent`, this is just a side advantage that makes the interface more responsive.  Before when you click continue, if the PHP code is hung for whatever reason (it is slow, it has a lot to do, etc), it appears that it did not work as it appears to still be stopped on a breakpoint (but it is not, which you can tell by trying to view any of the details in the debug pane).  With the change, now when you click continue it immediately changes the process back to "running" state which is more truthful.

Since xdebug#164 has a lot of comments, here is the **tl;dr:** deadlock explanation: 
1. Connection 1 makes a second connection with curl.
2. It sends command to all connections to set breakpoints, and waits for response.
3. The second connection hangs as it is waiting for connection 1 to set breakpoints, connection 1 hangs without setting those breakpoints because it is waiting for connection 2 to respond and can't do anything until that completes.

This PR makes it not wait for the breakpoints to be set on a connection if the connection is waiting for code to execute.  So now connection 2 can finish initializing and run, no more deadlock.

Fixes xdebug#164
SidIcarus pushed a commit to towhidabsar/vscode-php-debug that referenced this pull request Oct 9, 2018
…ebug#294)

This introduces a new concept in the xDebug connection:  "execution commands": these are commands that result in PHP being executed.  The way xDebug works, if one of these commands is run, that connection is hung until the PHP code is done executing (until it gets to the next stopping point).  By keeping track of when such commands are still in progress, and making it something that the adapter can check, it allows the adapter to specifically code for when the connection is currently hung.  Which allows preventing the deadlock situation described in xdebug#164 

The adapter also now emits `before-execution-command` since it results in possibly going into a hung state for a while.  The adapter uses that event to send a `ContinueEvent`, this is just a side advantage that makes the interface more responsive.  Before when you click continue, if the PHP code is hung for whatever reason (it is slow, it has a lot to do, etc), it appears that it did not work as it appears to still be stopped on a breakpoint (but it is not, which you can tell by trying to view any of the details in the debug pane).  With the change, now when you click continue it immediately changes the process back to "running" state which is more truthful.

Since xdebug#164 has a lot of comments, here is the **tl;dr:** deadlock explanation: 
1. Connection 1 makes a second connection with curl.
2. It sends command to all connections to set breakpoints, and waits for response.
3. The second connection hangs as it is waiting for connection 1 to set breakpoints, connection 1 hangs without setting those breakpoints because it is waiting for connection 2 to respond and can't do anything until that completes.

This PR makes it not wait for the breakpoints to be set on a connection if the connection is waiting for code to execute.  So now connection 2 can finish initializing and run, no more deadlock.

Fixes xdebug#164
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.

4 participants