DYN-9707: Harden graph locking against corrupt lock files and improve Save As safety#17197
Conversation
There was a problem hiding this comment.
See the ticket for this pull request: https://jira.autodesk.com/browse/DYN-9707
There was a problem hiding this comment.
Pull request overview
This PR strengthens Dynamo’s graph-lock sidecar (.dynlock) behavior in DynamoCore and refines the WPF conflict “Save As” UX in DynamoCoreWpf, with accompanying regression tests and localized strings.
Changes:
- Treat unreadable/corrupt
.dynlockfiles as reclaimable and add heartbeat resilience (shorter interval + consecutive failure handling). - Harden the WPF “Save As” flow to prevent selecting the locked source file and restore overwrite confirmations.
- Add new unit tests covering corrupt lock handling, heartbeat mismatch behavior, and Save As validation; move new UI strings into resources.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/DynamoCoreWpfTests/WpfGraphLockUserPromptTests.cs | Adds WPF tests for Save As validation and overwrite confirmation behavior. |
| test/DynamoCoreTests/Graph/Workspaces/GraphLockManagerTests.cs | Adds tests for corrupt lock takeover and heartbeat/session-mismatch behavior. |
| src/DynamoCoreWpf/UI/CustomSaveFileDialog.cs | Extends IFileSaver / CustomSaveFileDialog with FileOk + OverwritePrompt for testability and validation hooks. |
| src/DynamoCoreWpf/Services/WpfGraphLockUserPrompt.cs | Implements Save As path blocking for the locked source file and custom overwrite confirmation. |
| src/DynamoCoreWpf/PublicAPI.Unshipped.txt | Declares newly exposed CustomSaveFileDialog members in the public API tracking file. |
| src/DynamoCoreWpf/Properties/Resources.resx | Adds localized strings for Save As validation and overwrite confirmation. |
| src/DynamoCoreWpf/Properties/Resources.en-US.resx | Adds en-US localized strings for Save As validation and overwrite confirmation. |
| src/DynamoCoreWpf/Properties/Resources.Designer.cs | Updates resource designer accessors for the new strings. |
| src/DynamoCore/Graph/Workspaces/Locking/GraphLockManager.cs | Updates lock acquisition/heartbeat logic (interval, reclaim policy, retry/drop behavior). |
Files not reviewed (1)
- src/DynamoCoreWpf/Properties/Resources.Designer.cs: Generated file
| if (!readable) | ||
| { | ||
| return null; | ||
| // The lock file exists but could not be parsed after all retry attempts. | ||
| // This indicates a corrupt file rather than a live owner. | ||
| // Treat it the same as a stale lock: overwrite and take ownership. | ||
| GraphLockFile.WriteHeartbeat(sidecarPath, info); | ||
| RegisterOwnedLock(normalizedPath, sidecarPath, info, workspace); | ||
| return GraphLockAcquireResult.Acquired(normalizedPath); | ||
| } |
There was a problem hiding this comment.
Fair point. This is now addressed with GraphLockReadResult
There was a problem hiding this comment.
I think the fix you applied is correct, but I think there is a missing case that I flagged in GraphLockManager.CreateAndOpenCopy() that you had applied in GraphLockManager.TryAcquireReadableLock() for the TransientFailure case.
| // OverwritePrompt is false so we must replicate the OS overwrite warning | ||
| // for any other existing file the user may have chosen. | ||
| if (File.Exists(chosenPath)) | ||
| { | ||
| var confirm = showMessageBox( | ||
| Path.GetFileName(chosenPath) + Resources.ConfirmReplaceFileMessage, | ||
| Resources.ConfirmReplaceFileTitle, | ||
| MessageBoxButtons.YesNo, | ||
| MessageBoxIcon.Warning); |
There was a problem hiding this comment.
... now addressed properly.
Looks updated here.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…ilename-is-modified
|
jasonstratton
left a comment
There was a problem hiding this comment.
Honestly this is a pretty complicated solution to a gnarly problem. SO it is great work, but I did find a couple of things that should be addressed, I think. ... Please feel free to push back if you don't agree. As I said, it is pretty complicated and you understand it much more intimately than I. Thanks.
| return GraphLockAcquireResult.Acquired(normalizedPath); | ||
| } | ||
|
|
||
| if (readResult == GraphLockReadResult.TransientFailure) |
There was a problem hiding this comment.
This fix addressing the TransierntFailure should probably be applied below as well, look at line 322
| ownsSaveAsLock = true; | ||
| } | ||
| else if (!readable || IsStale(saveAsLock) || IsDeadLocalProcess(saveAsLock)) | ||
| else if (saveAsReadResult == GraphLockReadResult.Corrupt || IsStale(saveAsLock) || IsDeadLocalProcess(saveAsLock)) |
There was a problem hiding this comment.
So here, you might want to check for the TransientFailure as well?
Or maybe guard against it before this if statement?
| if (!readable) | ||
| { | ||
| return null; | ||
| // The lock file exists but could not be parsed after all retry attempts. | ||
| // This indicates a corrupt file rather than a live owner. | ||
| // Treat it the same as a stale lock: overwrite and take ownership. | ||
| GraphLockFile.WriteHeartbeat(sidecarPath, info); | ||
| RegisterOwnedLock(normalizedPath, sidecarPath, info, workspace); | ||
| return GraphLockAcquireResult.Acquired(normalizedPath); | ||
| } |
There was a problem hiding this comment.
I think the fix you applied is correct, but I think there is a missing case that I flagged in GraphLockManager.CreateAndOpenCopy() that you had applied in GraphLockManager.TryAcquireReadableLock() for the TransientFailure case.
| GraphLockFile.WriteHeartbeat(sidecarPath, info); | ||
| ownsSaveAsLock = true; | ||
| } | ||
| else |
There was a problem hiding this comment.
So if TransientFailure is not handled above, it will be handled by this else clause, which cancels the lock and it gets stolen.
| GraphLockInfo current; | ||
| if (!GraphLockFile.TryRead(owned.SidecarPath, out current) || | ||
| current.SessionId != owned.Info.SessionId) | ||
| if (GraphLockFile.TryRead(owned.SidecarPath, out current) != GraphLockReadResult.Ok) |
There was a problem hiding this comment.
If TryRead() returns NotFound ... it's pretty definitive that it does not exist and the retries are unnecessary. It's probably best to break out of the retries right away as it might allow another session to claim the path.
There was a problem hiding this comment.
Similarly returning Corrupt would also retry, but the file would not suddenly uncorrupt itself ... Elsewhere in AcquireLockInternal() treats Corrupt as something that can be immediately reclaimed. This might be a window where two sessions claim the same path.
|
Closing this PR as agreed. I'll revert the changes from the other "graph lock" PRs and resubmit them after the 4.2 release. |



Purpose
This PR is related to DYN-9707 and addresses this post plus a comment from a recent catchup. The changes improve the behaviour of the graph locking mechanism that prevents two Dynamo instances from opening and editing the same
.dynor.dyffile at the same time.When Dynamo opens a graph file it writes a small sidecar file (
.dynlock) to signal that the file is in use. If another Dynamo instance detects that sidecar, it shows a prompt asking the user to either cancel or save a copy of the graph to work on instead. Several edge cases in that flow were not handled correctly and are addressed here.Improvements to previous logic:
Corrupt lock files no longer block the user:
A
.dynlockfile that cannot be read (e.g. left behind by a crash or a disk error) was previously treated the same as a live lock, triggering the conflict prompt with no useful owner information. It is now treated as abandoned and silently overwritten, so the graph opens normally.Transient read failures no longer drop a valid lock:
If an antivirus scan or a network hiccup briefly prevents Dynamo from reading its own
.dynlockduring a heartbeat cycle, the lock is now retried rather than immediately abandoned. The lock is only released after three consecutive failures — at which point the file would already appear stale to other instances anyway.Lock freshness check is faster:
The heartbeat interval was halved from 30 seconds to 15 seconds, reducing the time another instance has to wait before it can safely take over a lock left behind by a crashed Dynamo (from 90 seconds down to 45 seconds).
Save As dialog is safer:
When the user chooses to save a copy, the dialog now prevents them from accidentally picking the same file that is already locked. It also restores the standard "this file already exists, do you want to replace it?" warning for any other existing file they choose, which was inadvertently suppressed.
All user-facing strings are now localizable:
The messages shown in the conflict dialogs have been moved from hardcoded English text into resource files.
Regression tests cover the new behaviours: corrupt lock handling, heartbeat resilience, and the save-as dialog validation.
Declarations
Check these if you believe they are true
Release Notes
Fixed an issue where a corrupted graph lock file could incorrectly block a user from opening a graph.
Reviewers
@DynamoDS/eidos
@jasonstratton
@johnpierson
FYIs
@dnenov