Skip to content

fix: Retry session directory cleanup on Windows while handles are released - #376

Open
leongdl wants to merge 6 commits into
OpenJobDescription:mainlinefrom
leongdl:fix/windows-cleanup-retry
Open

leongdl wants to merge 6 commits into
OpenJobDescription:mainlinefrom
leongdl:fix/windows-cleanup-retry

Conversation

@leongdl

@leongdl leongdl commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Fixes: no GitHub issue; reported against Windows workers running DCC applications.

What was the problem/requirement? (What/Why)

On Windows, session cleanup leaks the working directory when a process started by the job still has a file in it open. A child process can keep its handles after it exits or is terminated, and Windows will not delete a file while a handle to it is open. TempDir.cleanup ran rmtree once and raised:

Files within temporary directory C:\ProgramData\Amazon\OpenJD\bwmdrwqv could not be deleted.
C:\ProgramData\Amazon\OpenJD\bwmdrwqv\locked.bin: PermissionError(13, 'The process cannot access the file because it is being used by another process')

Nothing said which process held the file. On long-running workers these directories accumulate until the disk fills.

What was the solution? (How)

  • On Windows, TempDir.cleanup retries the deletion after 1, 2, 4, 8 and 16 s (_CLEANUP_RETRY_DELAYS_SECONDS, about 31 s in total). POSIX does not retry, because a failure there is a permission problem that waiting will not fix. A directory that was already missing still raises immediately, as before.
  • Each failed Windows attempt logs a warning to the session logger (Session.cleanup now passes it) naming the files that could not be deleted and the processes holding them open, found with the Restart Manager (RmGetList, new _win32/_file_locks.py), and the next delay or "Giving up.":
    Could not delete temporary directory C:\ProgramData\Amazon\OpenJD\t42wveld: The process cannot access the file because it is being used by another process. 1 file(s) could not be deleted, including locked.bin. Files are still locked open by: powershell.exe (pid 5928). Retrying in 4 s.
    
  • The final RuntimeError is unchanged.

What is the impact of this change?

On Windows, cleanup blocks for up to about 31 s longer when a file is still held open, and logs a warning per failed attempt. The common case, where the first attempt succeeds, is unchanged. No change on POSIX. TempDir.cleanup gains an optional logger argument.

How was this change tested?

The test job leaves a detached grandchild process holding locked.bin (opened with no sharing) in the session directory after the step exits. I ran it with openjd run from openjd-cli main on a Windows Server 2022 Deadline Cloud service-managed worker:

hold 0.14.0 this PR warnings
2.5 s dir leaked, error above deleted after 5.6 s 2
10 s dir leaked deleted after 17.5 s 4
45 s dir leaked dir leaked after 33.5 s, same error 6, the last "Giving up."

Every warning named powershell.exe and the grandchild's pid.

  • New tests in TestCleanupRetry and TestCleanupLockLogging cover the retry, the delays, the warning text with holders, a failed lookup, no lookup when only directories failed, nothing extra logged on POSIX, and the default logger. Windows-only tests hold a real file open, and one asserts the warning names this test's pid via the Restart Manager.
  • Linux: sessions_v0 960 passed. Windows: the tempdir tests pass; the full sessions_v0 suite has the same 28 failures on 0.14.0 and on this branch (wrap/resolved-symtab session-state tests, run as SYSTEM).
  • ruff, black --check and mypy are clean.

Was this change documented?

Yes, in the docstrings on TempDir.cleanup and _CLEANUP_RETRY_DELAYS_SECONDS.

Is this a breaking change?

No.

Does this change impact security?

No. It deletes only the same paths as before. The Restart Manager query is read-only, and the log lines name files inside the session directory and the processes holding them.

Cross-port to openjd-rs


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

…eased

Signed-off-by: David Leong <leongdl@amazon.com>
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Claude review · advisory

Reviewed 719a9ca (changes since 45a4f72): 0 new, 0 resolved.

Open: 0 blocking · 0 should-fix · 0 nit. ✅ Nothing blocking.

Fix or reply to each thread; the next revision's review re-checks open threads and resolves those it agrees are handled. Resolving a thread yourself also closes it. Later revisions review only what changed.

…les open

Signed-off-by: David Leong <leongdl@amazon.com>
Comment thread src/openjd/sessions/_tempdir.py Outdated
def _find_lock_holders(files: list[str]) -> tuple[list[tuple[int, str]], Optional[str]]:
"""The (pid, name) of each process holding any of ``files`` open, and the
reason the lookup failed if it did. Windows only."""
from ._win32._file_locks import processes_holding # type: ignore

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should fix: This lookup only feeds a log message, but a failure in it can abort cleanup. The import sits outside the try, and only OSError is caught. Importing _win32._file_locks runs ctypes.WinDLL("rstrtmgr") at module load, so if that DLL fails to load, an OSError escapes cleanup() before any retry. A ctypes.ArgumentError or anything else non-OSError from the Restart Manager calls escapes the same way. Session.cleanup only catches RuntimeError, so the remaining retries are skipped, _runner.shutdown() never runs, and the session never reaches ENDED. Move the import inside the try and catch Exception:

try:
    from ._win32._file_locks import processes_holding
    return [...], None
except Exception as err:
    return [], str(err)

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.

Fixed in efb2a73. The import is inside the try and it catches Exception, so a missing rstrtmgr.dll or a ctypes error becomes the "could not be determined: : " clause of the warning and the retries continue. New test test_a_lookup_that_cannot_even_import_does_not_abort_cleanup blocks the module import and checks that cleanup still retries and succeeds. It fails against e82c7d0.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Addressed: The import now sits inside the try and the handler catches Exception, so a failed import or ctypes error becomes the reason string and cleanup retries continue. The new test covers this case.

@crowecawcaw
crowecawcaw marked this pull request as ready for review November 6, 2026 20:32

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants