-
Notifications
You must be signed in to change notification settings - Fork 360
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
Fix: Pyramid delete before open #4062
Conversation
N-o-Z
commented
Aug 31, 2022
- Closes Pyramid: Fix delete file before open #4061
pkg/pyramid/tier_fs.go
Outdated
rcb := tfs.fileLocker.lock(fileRef.fsRelativePath) | ||
defer rcb() // release callback |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rcb := tfs.fileLocker.lock(fileRef.fsRelativePath) | |
defer rcb() // release callback | |
releaseFileLock := tfs.fileLocker.lock(fileRef.fsRelativePath) | |
defer releaseFileLock() |
pkg/pyramid/file_locker.go
Outdated
// fileLocker allows file locking to avoid race conditions between cache rejection/eviction | ||
// and the file usage by TierFS | ||
type fileLocker struct { | ||
wgMap *sync.Map |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Consider using map+mutex (usually it will work faster)
- Using a map with mutex will remove the extra allocation of waitgroup in case there is already a waitgroup on the path
45ec8b9
to
61311b8
Compare
@itaiad200 @nopcoder Please see new code |
pkg/pyramid/file_tracker.go
Outdated
if val, ok := t.refMap[string(path)]; ok { | ||
ref = val + 1 | ||
t.refMap[string(path)] = ref | ||
} else { | ||
t.refMap[string(path)] = ref | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if val, ok := t.refMap[string(path)]; ok { | |
ref = val + 1 | |
t.refMap[string(path)] = ref | |
} else { | |
t.refMap[string(path)] = ref | |
} | |
if val, ok := t.refMap[string(path)]; ok { | |
ref += val | |
} | |
t.refMap[string(path)] = ref |
pkg/pyramid/file_tracker.go
Outdated
func newFileTracker() *fileTracker { | ||
return &fileTracker{ | ||
refMap: map[string]int{}, | ||
mu: sync.Mutex{}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not needed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can remove the mu initialization it is a value type no need initialize it with the same value
pkg/pyramid/file_tracker.go
Outdated
} | ||
} | ||
|
||
func (t *fileTracker) dec(path params.RelativePath, fn decCallback) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can remove the 'fn' argument here - if dec returns true/false or count - we can defer call to 'dec' and delete the file.
pkg/pyramid/tier_fs.go
Outdated
@@ -114,7 +115,12 @@ func (tfs *TierFS) removeFromLocal(rPath params.RelativePath, filesize int64) { | |||
// This will be called by the cache eviction mechanism during entry insert. | |||
// We don't want to wait while the file is being removed from the local disk. | |||
evictionHistograms.WithLabelValues(tfs.fsName).Observe(float64(filesize)) | |||
go tfs.removeFromLocalInternal(rPath) | |||
// Decrement the reference count on the file. When the ref count reaches -1, The file will be deleted | |||
tfs.fileTracker.dec(rPath, func(ref int) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need a 'delete' marker on the map - we like that for every time we called open we inc/dec in case the file wasn't found - it means that evict delete it.
if we like a better visibility I think the tracker should understand when to perform the callback or make sure that every time you call dec you also check and delete the file.
Something like:
tracker := NewTracker(deleteCallback) // callback to the implementation on how to delete file from local
closer := tracker.Open(path); // return a callback
defer closer() // closer will take the lock remove the ref count when 0 and remove from map will check if a delete flag is mark and call the callback
tracker.Delete(path) // mark the file for delete or delete by keeping the lock
pkg/pyramid/file_tracker.go
Outdated
if ref == -1 { | ||
delete(t.refMap, string(path)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Why is -1 the point of eviction and not 0?
- What is the meaning of
dec
an non existing path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
0 Doesn't mean the file was flagged for eviction - it just means that nobody is currently trying to open it.
When we trigger the eviction function it performs a decrement on the file without a prior increment - which will eventually after all the open requests completed will cause it to reach -1. This is the indication that the file was requested for deletion and only then the logic will trigger
pkg/pyramid/tier_fs.go
Outdated
if ref == -1 { | ||
tfs.removeFromLocalInternal(rPath) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if ref
returns as -1
and before the file is removed, it is acquired again, by another thread.
I think it will still be removed, as the mutex protects the ref-count, but nothing prevents ref increase, after the -1
is returned and removeFromLocalInternal
is fired
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This cannot happen because removeFromLocalInternal happens in the scope of dec where we have a mutex lock on the map.
What will happen is that on dec the entry will be deleted from the map, and the file deleted from storage. Only after that the lock will be released - the inc will happen and a new entry in the map will be created for the file
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please add some tests for this? Apart from helping in verifying this fix, it can also help in understanding what flows/use-cases are relevant, and by that to really understand the problem.
Another thing, I am not sure this fix really protect against deleting the file by one thread, from under the legs of another. Please see comment inside
493a979
to
dba5ab8
Compare
Please see revised code |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Blocking this PR until Sunday
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks better - comments related on how we handle delete
pkg/pyramid/file_tracker.go
Outdated
func newFileTracker() *fileTracker { | ||
return &fileTracker{ | ||
refMap: map[string]int{}, | ||
mu: sync.Mutex{}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can remove the mu initialization it is a value type no need initialize it with the same value
pkg/pyramid/file_tracker.go
Outdated
if val.ref == 0 && val.deleted { | ||
t.innerDelete(path) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- when val.ref == 0 - we need to delete it from the map
- when we delete it from the map and val.deleted true - we need to invoke the callback
pkg/pyramid/file_tracker.go
Outdated
val.deleted = true | ||
if val.ref == 0 { | ||
t.innerDelete(path) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On Delete - when path in map - mark it 'deleted' = true.
When path not in map - delete the file.
All while map is locked.
@@ -256,19 +261,12 @@ func (tfs *TierFS) openFile(ctx context.Context, fileRef localFileRef, fh *os.Fi | |||
} | |||
|
|||
if !tfs.eviction.Store(fileRef.fsRelativePath, stat.Size()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OnReject is called also in case method returns false?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both OnEvict and OnReject CB are configured to run the same OnEvict method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right - but for a rejected file you will call tfs.fileTracker.Delete(rPath)
twice. Here and in the OnReject
callback (which calls tfs.removeFromLocal
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I see onReject is called only in processItems (NewCache, Clear)
This flow is for Set
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now sure I understand. onReject
is called whenever a file isn't being stored (decided by their policy) - so here we would definitely call Delete
twice for a rejected file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ristrettoEviction Store calls cache Set which in turn calls SetWithTTL.
In the SetWithTTL flow there are several places where the function return false without calling onReject CB.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right - onReject
only gets called when the eviction policy decides to reject. All other rejections are not called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm!
|
||
var testFileMap map[string]bool | ||
|
||
func TestTracker(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome 🤩
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the recent changers. Excellent test
6d505b8
to
330efb3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment and question inside.
Also - how was this tested? I don't think our current tests cover cache being filled up and many concurrent calls to the same paths.
@@ -256,19 +261,12 @@ func (tfs *TierFS) openFile(ctx context.Context, fileRef localFileRef, fh *os.Fi | |||
} | |||
|
|||
if !tfs.eviction.Store(fileRef.fsRelativePath, stat.Size()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right - but for a rejected file you will call tfs.fileTracker.Delete(rPath)
twice. Here and in the OnReject
callback (which calls tfs.removeFromLocal
)
errorsTotal.WithLabelValues(tfs.fsName, "DirRemoval") | ||
} | ||
// Delete Dir async | ||
go func() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we changing it to async?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removeFromLocalInternal was called asynchronously to not delay the flow on the delete dir operation. Now that it is called out of context, I've modified only the call to delete dir to be perfomed async
330efb3
to
ae40f62
Compare
I added a unit test for the file tracker. It is true that we currently don't have test that perform concurrent operations but the unit test should cover this specific scenario |
@@ -256,19 +261,12 @@ func (tfs *TierFS) openFile(ctx context.Context, fileRef localFileRef, fh *os.Fi | |||
} | |||
|
|||
if !tfs.eviction.Store(fileRef.fsRelativePath, stat.Size()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now sure I understand. onReject
is called whenever a file isn't being stored (decided by their policy) - so here we would definitely call Delete
twice for a rejected file.
I don't think that it will. We have an issue when the cache gets filled up. If we don't have a unit test we need to test it manually to verify that we don't encounter bugs when the cache gets filled up and that the cache size stays within its limits (no untracked files) with some deviation as we're not guaranteed to be always within bounds (there are short periods where we might cross the limits). |
Co-authored-by: itai-david <90712874+itai-david@users.noreply.github.com>
c3bbb2c
to
a12a92b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tested together on a real scenario - works amazing!