fix(blocks): resolve race conditions in connectionStoreLock with retry mechanism#5014
fix(blocks): resolve race conditions in connectionStoreLock with retry mechanism#5014WillyEverGreen wants to merge 4 commits intosugarlabs:masterfrom
Conversation
|
❌ Some Jest tests failed. Please check the logs and fix the issues before merging. Failed Tests: |
There was a problem hiding this comment.
Pull request overview
This PR attempts to fix race conditions in the duplicate block functionality by introducing a retry-based locking mechanism for connectionStoreLock in both FlowBlocks.js and IntervalsBlocks.js. However, the refactoring is incomplete and introduces critical bugs.
Key Changes:
- Wrapped connection setup and restoration logic in helper functions with async retry mechanisms
- Replaced ineffective lock checks with
setTimeout-based retry loops - Removed FIXME comments about race conditions
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| js/blocks/IntervalsBlocks.js | Refactored duplicate block connection handling with retry-based locking, but contains critical bugs with duplicate code and undefined variable references |
| js/blocks/FlowBlocks.js | Refactored duplicate block connection handling with retry-based locking, but contains critical bugs with duplicate code and undefined variable references |
Critical Issues Found:
Both files contain the same critical bug where the original synchronous code (that should have been removed) remains after the setupConnections() call, causing:
- ReferenceError due to undefined
otherTurtlevariable - Duplicate execution of connection logic
- Race conditions that the PR was intended to fix
Additionally, the retry mechanisms lack timeout protection, which could lead to infinite loops if the lock gets stuck.
Comments suppressed due to low confidence (2)
js/blocks/FlowBlocks.js:334
- This entire block of code (lines 265-334) appears to be leftover code that should have been removed. This code duplicates the logic that has been moved inside the setupConnections function (lines 243-260). The code block operates on an undefined variable 'otherTurtle' (declared inside setupConnections at line 244 but not available in this scope), which will cause a ReferenceError at runtime.
if (otherTurtle != null) {
// Copy the connections and queue the blocks
logo.connectionStore[turtle][blk] = [];
for (let i = logo.connectionStore[otherTurtle][blk].length; i > 0; i--) {
const obj = [
logo.connectionStore[otherTurtle][blk][i - 1][0],
logo.connectionStore[otherTurtle][blk][i - 1][1],
logo.connectionStore[otherTurtle][blk][i - 1][2]
];
logo.connectionStore[turtle][blk].push(obj);
let child = obj[0];
if (activity.blocks.blockList[child].name === "hidden") {
child = activity.blocks.blockList[child].connections[0];
}
const queueBlock = new Queue(child, factor, blk, receivedArg);
tur.parentFlowQueue.push(blk);
tur.queue.push(queueBlock);
}
} else {
let child = activity.blocks.findBottomBlock(args[1]);
while (child != blk) {
if (activity.blocks.blockList[child].name !== "hidden") {
const queueBlock = new Queue(child, factor, blk, receivedArg);
tur.parentFlowQueue.push(blk);
tur.queue.push(queueBlock);
}
child = activity.blocks.blockList[child].connections[0];
}
// Break the connections between blocks in the clamp so
// that when we run the queues, only the individual blocks
// run
logo.connectionStore[turtle][blk] = [];
child = args[1];
while (child != null) {
const lastConnection =
activity.blocks.blockList[child].connections.length - 1;
const nextBlk =
activity.blocks.blockList[child].connections[lastConnection];
// Don't disconnect a hidden block from its parent
if (
nextBlk != null &&
activity.blocks.blockList[nextBlk].name === "hidden"
) {
logo.connectionStore[turtle][blk].push([
nextBlk,
1,
activity.blocks.blockList[nextBlk].connections[1]
]);
child = activity.blocks.blockList[nextBlk].connections[1];
activity.blocks.blockList[nextBlk].connections[1] = null;
} else {
logo.connectionStore[turtle][blk].push([
child,
lastConnection,
nextBlk
]);
activity.blocks.blockList[child].connections[lastConnection] = null;
child = nextBlk;
}
if (child != null) {
activity.blocks.blockList[child].connections[0] = null;
}
}
}
logo.connectionStoreLock = false;
js/blocks/IntervalsBlocks.js:943
- This entire block of code (lines 885-943) appears to be leftover code that should have been removed. This code duplicates the logic that has been moved inside the setupConnections function (lines 862-879). The code block operates on an undefined variable 'otherTurtle' (declared inside setupConnections at line 863 but not available in this scope), which will cause a ReferenceError at runtime.
// Check to see if another turtle has already disconnected these blocks
const otherTurtle = __lookForOtherTurtles(blk, turtle);
if (otherTurtle != null) {
// Copy the connections and queue the blocks.
logo.connectionStore[turtle][blk] = [];
for (let i = logo.connectionStore[otherTurtle][blk].length; i > 0; i--) {
const obj = [
logo.connectionStore[otherTurtle][blk][i - 1][0],
logo.connectionStore[otherTurtle][blk][i - 1][1],
logo.connectionStore[otherTurtle][blk][i - 1][2]
];
logo.connectionStore[turtle][blk].push(obj);
let child = obj[0];
if (activity.blocks.blockList[child].name === "hidden") {
child = activity.blocks.blockList[child].connections[0];
}
const queueBlock = new Queue(child, factor, blk, receivedArg);
tur.parentFlowQueue.push(blk);
tur.queue.push(queueBlock);
}
} else {
let child = activity.blocks.findBottomBlock(args[1]);
while (child != blk) {
if (activity.blocks.blockList[child].name !== "hidden") {
const queueBlock = new Queue(child, factor, blk, receivedArg);
tur.parentFlowQueue.push(blk);
tur.queue.push(queueBlock);
}
child = activity.blocks.blockList[child].connections[0];
}
// Break the connections between blocks in the clamp so
// that when we run the queues, only the individual blocks,
// each inserted into a semitoneinterval block, run.
logo.connectionStore[turtle][blk] = [];
child = args[1];
while (child != null) {
const lastConnection = activity.blocks.blockList[child].connections.length - 1;
const nextBlk = activity.blocks.blockList[child].connections[lastConnection];
// Don't disconnect a hidden block from its parent.
if (nextBlk != null && activity.blocks.blockList[nextBlk].name === "hidden") {
logo.connectionStore[turtle][blk].push([
nextBlk,
1,
activity.blocks.blockList[nextBlk].connections[1]
]);
child = activity.blocks.blockList[nextBlk].connections[1];
activity.blocks.blockList[nextBlk].connections[1] = null;
} else {
logo.connectionStore[turtle][blk].push([child, lastConnection, nextBlk]);
activity.blocks.blockList[child].connections[lastConnection] = null;
child = nextBlk;
}
if (child != null) {
activity.blocks.blockList[child].connections[0] = null;
}
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
✅ All Jest tests passed! This PR is ready to merge. |
|
✅ All Jest tests passed! This PR is ready to merge. |
|
Note: The retry is intentionally bounded so we don’t end up waiting forever if |
|
Did you have a test for this? How do we trigger the race condition? |
|
This issue is timing dependent, so it doesn’t always show up reliably in tests. That said, the problem is clear from the code, the lock was checked, but execution continued even when it was already held, allowing concurrent changes to connectionStore. This change makes the lock actually effective, instead of logging and moving on, the code now waits for the lock to clear before proceeding. If you’d prefer more deterministic coverage, I can add a test that simulates concurrent access by briefly holding the lock and then releasing it, so the retry logic is exercised in a controlled way. Happy to adjust based on the preferred approach here. |
|
I am seeing this error:
|
|
✅ All Jest tests passed! This PR is ready to merge. |
Fixed! The error was caused by calling logo.disconnectBlock(blk) which doesn't exist on the logo object it seems. I've removed these invalid calls, the block disconnection logic is already handled by the connection manipulation code that follows. |
|
How did you test this? |
Tested via:
All tests pass locally. Happy to add more explicit deterministic tests if you'd like. |
|
Duplicate doesn't seem to work for me with this fix. |
…ue mechanism This fix properly handles race conditions in DuplicateBlock (FlowBlocks.js) and ArpeggioBlock (IntervalsBlocks.js) by using the existing turtle queue mechanism to defer execution when connectionStoreLock is held. Key changes: - When lock is held, requeue the block using turtle.queue and tur.doWait(0.01) - Acquire lock at the start of block execution (synchronous) - Use setTimeout with doWait() in listeners to wait for lock release - Add retry limit (50) in listeners to prevent infinite waiting/starvation - Add null checks to prevent errors when connectionStore entries are missing - Preserve all original block queueing logic (findBottomBlock, etc.) The previous implementation had issues: 1. Used setTimeout-based async retry mechanism that could bypass the event loop 2. Missing block queueing logic (findBottomBlock loop) in the 'else' branch 3. Made the listener async which changed execution order 4. Lacked bounded retries, leading to potential infinite loops This approach uses the native Music Blocks queue/wait mechanism which properly integrates with the event loop and turtle scheduling. Signed-off-by: WillyEverGreen <kushang.shah05@gmail.com>
8c7a536 to
9292508
Compare
|
✅ All Jest tests passed! This PR is ready to merge. |
|
@walterbender thanks for flagging this. Now I've pushed a complete refactor that addresses the regression with the "Duplicate" block and properly handles the race condition. What was wrongA previous change unintentionally removed the block queuing logic (the The FixI've rewritten the logic in both DuplicateBlock (
Verification
This should be robust, safe, and fully backward compatible. Thanks for catching the issue! |
|
I think I may have merged a different solution already. Please check. |
Thanks for the heads-up, Walter. I’ll check the latest master and see what was merged. |
|
Closing this PR since the race condition was resolved by #5046, which was merged earlier today. Thanks for the review and feedback! |
fix(blocks): resolve race conditions in connectionStoreLock with retry mechanism
Description
This PR fixes race conditions in
FlowBlocks.jsandIntervalsBlocks.jswherelogo.connectionStoreLockwas checked but not respected, allowing concurrentmutations of
connectionStore.Changes
(
maxRetries = 1000) to safely wait for lock releaseto prevent duplicate execution and reference errors
FIXMEcomments and debug loggingVerification
Related
FIXMEs inFlowBlocks.jsandIntervalsBlocks.js