fix: clean up upload directories and session state on TO2 failure - #214
fix: clean up upload directories and session state on TO2 failure#214sarmahaj wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements a cleanup mechanism for temporary upload directories and orphaned session states. It introduces tracking for GUID-based directories and ensures their removal upon session failure or invalidation. Additionally, the database logic is updated to prune related session tables when a token is invalidated. Feedback focuses on the performance impact of sweeping stale states on every cleanup, the omission of the DeviceInfo table from the cleanup list, and the need for database transactions to ensure atomicity during session invalidation.
|
|
b37f210 to
798efd4
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a mechanism to clean up GUID-based upload directories and orphaned session states. It updates the state machine to track created directories and ensures they are deleted if a session does not complete successfully. Furthermore, it enhances the database's token invalidation process by transactionally removing related session data across multiple tables. Feedback focuses on a critical race condition and performance issues in the state sweeping logic, as well as a potential logic error where the completion status fails to distinguish between successful execution and error-induced termination.
| func (s moduleStateMachines) sweepStaleStates() { | ||
| for token, module := range s.states { | ||
| if !s.DB.SessionExists(token) { | ||
| module.Stop() | ||
| s.cleanupUploadDirs(module) | ||
| delete(s.states, token) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
This function introduces two significant issues:
-
Race Condition: It iterates over and modifies the
s.statesmap without any synchronization. SincemoduleStateMachinesis used within concurrent HTTP handlers, this will lead to race conditions and potential panics. Thestatesmap should be protected by async.RWMutex(and methods should use pointer receivers). -
Performance Bottleneck: It performs a full iteration of the map and a database query (
SessionExists) for every entry, every time a session is cleaned up. This results in$O(N^2)$ complexity relative to the number of active sessions.
Consider moving the stale state sweeping to a background goroutine with a periodic ticker instead of triggering it on every session cleanup.
| if !valid { | ||
| module.Completed = true | ||
| } |
There was a problem hiding this comment.
The Completed flag is set to true whenever the iterator finishes, but it does not distinguish between a successful completion and an early exit due to an error (e.g., a database failure inside ownerModules). This results in errors being swallowed and may prevent proper cleanup of upload directories on failure. Consider capturing errors from the iterator and only marking the session as completed if it finished successfully.
|
Hi @sarmahaj - I'm playing with this fix and I've found a weird behavior. It may be due to a path not being covered by the cleanup path. Still investigating, but I think you may be able to reproduce this. I wanted to see what happens if TO2 fails due to a problem uploading a file. So I modified the test/ci/test-fsim-upload.sh test to try to upload a file that doesn't exist. Here I added a file to upload that does not exist: Then ran the test: I'm seeing TO2 fail: as expected. This repeats... as expected. What I found was that new GUID subdirectories are being created in the test upload directory The code looks like it should be cleaning that up, but as best as I can tell the CleanupModule state machine callback is never being called by the go-fdo library. I don't think this failure to call CleanupModule is related to this change at all. Probably an issue elsewhere. I'll poke some more... let you know what I see. But related: maybe there's a good FSIM CI test we can add to test this. Maybe an FSIM configuration that uploads a file, then runs an "fdo.command" that fails the first time it runs TO2, then passes the next time TO2 runs (saves some state to the filesystem to track this?). |
|
And here you go: fido-device-onboard/go-fdo#231 That's preventing the owner from invoking the cleanup module path. Here's a proposed fix: |
|
@kgiusti Thanks for digging into this ! In your test scenario, sweepStaleStates didn't help because it only runs inside CleanupModules, which was never called for any session due to the go-fdo bug. I am looking into adding an alternative trigger for the sweep (e.g., on new session creation) so it can catch orphans even when CleanupModules is never invoked. (also until the fix lands on go-fdo) Agree on adding a CI test for this/similar failure path. |
ffcf013 to
b6fbc0a
Compare
Track GUID-based upload dirs created during fdo.upload and remove them if TO2 does not complete successfully. Clean up orphaned session state rows (to2_sessions, to1_sessions, etc.) on token invalidation. Signed-off-by: Sarita Mahajan <sarmahaj@redhat.com>
Move sweepStaleStates from CleanupModules to NextModule so orphaned sessions are cleaned up when a new TO2 attempt starts, even if CleanupModules was never called due to a client-side error. Throttled to run at most once per minute. Add E2E test for upload dir cleanup on TO2 failure and retry. Signed-off-by: Sarita Mahajan <sarmahaj@redhat.com>
b6fbc0a to
0ecf5a3
Compare
NoteThe two new CI tests (test-upload-cleanup and test-upload-cleanup-missing-file) build both the client and server against fido-device-onboard/go-fdo#232 to validate the full cleanup flow end to end. Once that fix lands in go-fdo and we bump the dependency, these tests will build against the released version instead. |
kgiusti
left a comment
There was a problem hiding this comment.
The tests you added look great - thanks!
One question and one potential issue:
Q: Are the state.go changes related to the cleanup? I'm having trouble understanding why those changes are part of this patch.
Issue: I haven't looked too closely at this code before but I think there's a potential race here over the moduleStateMachines struct. This is a single-instance that is shared across all active TO2 sessions. The http server spawns a separate goroutine for each client connection, which means they are all touching the same moduleStateMachines instance.
I think a lock needs to be added to this structure and taken when the states map and lastSweep flags are being referenced.
|
Ah - and if I paid attention to the gemini review comments I would've seen that it already raised the race issue... ouch. |
djach7
left a comment
There was a problem hiding this comment.
Two relatively small changes, nothing major
| for _, dir := range module.UploadDirs { | ||
| slog.Info("fdo.upload: cleaning up upload directory after TO2 failure", "dir", dir) | ||
| if err := os.RemoveAll(dir); err != nil { | ||
| slog.Error("fdo.upload: failed to clean up upload directory", "dir", dir, "err", err) |
There was a problem hiding this comment.
Would it make sense to leave a comment here that this is a best effort cleanup? Given that if the cleanup fails and this error is passed the directory still remains.
I suppose the other way to do it would be to track it for retry or something like that, but I think a comment as a disclaimer suffices.
There was a problem hiding this comment.
Good point — updated the doc comment to clarify this is best-effort cleanup. If os.RemoveAll fails, the directory is left in place and the error is logged. There's no retry path since the entry is removed from the in-memory states map regardless of outcome, so we lose track of it. A startup sweep for orphaned GUID subdirs could address that, but seems better as a follow-up.
| if !ok { | ||
| // Sweep orphaned states from previous sessions before creating a | ||
| // new one. Throttled to avoid excessive DB queries under load. | ||
| if time.Since(s.lastSweep) >= sweepInterval { |
There was a problem hiding this comment.
I think this might result in a similar race issue to the one gemini documented elsewhere. The mutex fix gemini suggested for the states race should be able to fix this one too.
There was a problem hiding this comment.
thanks for pointing that ,added a mutex lock now
@kgiusti
you're right. The states map was already shared across goroutines, but this PR adds lastSweep and sweepStaleStates() which iterates the map and deletes stale entries, increasing the exposure. I'll add a sync.Mutex to the struct and lock in Module(), NextModule(), and CleanupModules() to address this. |
Protect the states map and lastSweep field with a sync.Mutex since moduleStateMachines is a single instance shared across all TO2 session goroutines. Also clarify that upload dir cleanup is best-effort. Signed-off-by: Sarita Mahajan <sarmahaj@redhat.com>
fdo.uploadin TO2 and clean them up if the session fails or the device disconnects, preventing orphaned partial uploads from accumulating on disk.InvalidateToken, which previously left them orphaned on every failed session.