Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 12 commits into from
Aug 28, 2018

Conversation

jonyo
Copy link
Contributor

@jonyo jonyo commented Aug 15, 2018

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.

@@ -545,6 +545,8 @@ interface Command {
resolveFn: (response: XMLDocument) => any
/** 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
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
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.

@@ -603,6 +607,7 @@ export class Connection extends DbgpConnection {
const command = this._pendingCommands.get(transactionId)!
this._pendingCommands.delete(transactionId)
command.resolveFn(response)
this._pendingExecuteCommand = false;
Copy link
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?

src/phpDebug.ts Outdated
@@ -438,6 +442,7 @@ class PhpDebugSession extends vscode.DebugSession {
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
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
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.

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
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
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
Copy link
Contributor Author

jonyo commented Aug 15, 2018

@felixfbecker Changes in, thanks!

@codecov
Copy link

codecov bot commented Aug 15, 2018

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
Copy link

Mancy commented Aug 16, 2018

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

@macbookandrew
Copy link

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

try {
await connection.sendBreakpointSetCommand(breakpoint)
// only capture each breakpoint once
if (connectionIndex === 0) {
Copy link
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
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
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
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