Skip to content
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

Merged
merged 7 commits into from
Sep 6, 2022
Merged

Conversation

N-o-Z
Copy link
Member

@N-o-Z N-o-Z commented Aug 31, 2022

@N-o-Z N-o-Z added bug Something isn't working exclude-changelog PR description should not be included in next release changelog team/versioning-engine Team versioning engine labels Aug 31, 2022
@N-o-Z N-o-Z self-assigned this Aug 31, 2022
Comment on lines 290 to 291
rcb := tfs.fileLocker.lock(fileRef.fsRelativePath)
defer rcb() // release callback
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
rcb := tfs.fileLocker.lock(fileRef.fsRelativePath)
defer rcb() // release callback
releaseFileLock := tfs.fileLocker.lock(fileRef.fsRelativePath)
defer releaseFileLock()

// fileLocker allows file locking to avoid race conditions between cache rejection/eviction
// and the file usage by TierFS
type fileLocker struct {
wgMap *sync.Map
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. Consider using map+mutex (usually it will work faster)
  2. Using a map with mutex will remove the extra allocation of waitgroup in case there is already a waitgroup on the path

@N-o-Z N-o-Z force-pushed the fix/tier-fs-evict-before-open-4061 branch from 45ec8b9 to 61311b8 Compare September 1, 2022 15:05
@N-o-Z N-o-Z requested review from itaiad200 and nopcoder September 1, 2022 15:06
@N-o-Z
Copy link
Member Author

N-o-Z commented Sep 1, 2022

@itaiad200 @nopcoder Please see new code

Comment on lines 29 to 42
if val, ok := t.refMap[string(path)]; ok {
ref = val + 1
t.refMap[string(path)] = ref
} else {
t.refMap[string(path)] = ref
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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

func newFileTracker() *fileTracker {
return &fileTracker{
refMap: map[string]int{},
mu: sync.Mutex{},
Copy link
Contributor

Choose a reason for hiding this comment

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

not needed

Copy link
Contributor

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

}
}

func (t *fileTracker) dec(path params.RelativePath, fn decCallback) {
Copy link
Contributor

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.

@@ -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) {
Copy link
Contributor

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

Comment on lines 46 to 66
if ref == -1 {
delete(t.refMap, string(path))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. Why is -1 the point of eviction and not 0?
  2. What is the meaning of dec an non existing path?

Copy link
Member Author

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

Comment on lines 120 to 121
if ref == -1 {
tfs.removeFromLocalInternal(rPath)
Copy link
Contributor

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

Copy link
Member Author

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

Copy link
Contributor

@itaidavid itaidavid left a 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

@N-o-Z N-o-Z force-pushed the fix/tier-fs-evict-before-open-4061 branch from 493a979 to dba5ab8 Compare September 2, 2022 10:58
@N-o-Z
Copy link
Member Author

N-o-Z commented Sep 2, 2022

Please see revised code

@N-o-Z N-o-Z requested review from nopcoder and itaidavid September 2, 2022 10:58
Copy link
Contributor

@itaiad200 itaiad200 left a 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

Copy link
Contributor

@nopcoder nopcoder left a 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

func newFileTracker() *fileTracker {
return &fileTracker{
refMap: map[string]int{},
mu: sync.Mutex{},
Copy link
Contributor

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

Comment on lines 53 to 55
if val.ref == 0 && val.deleted {
t.innerDelete(path)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. when val.ref == 0 - we need to delete it from the map
  2. when we delete it from the map and val.deleted true - we need to invoke the callback

val.deleted = true
if val.ref == 0 {
t.innerDelete(path)
}
Copy link
Contributor

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()) {
Copy link
Contributor

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?

Copy link
Member Author

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

Copy link
Contributor

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)

Copy link
Member Author

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

Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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.

@N-o-Z N-o-Z requested a review from nopcoder September 2, 2022 12:01
Copy link
Contributor

@nopcoder nopcoder left a 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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Awesome 🤩

Copy link
Contributor

@itaidavid itaidavid left a 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

@N-o-Z N-o-Z force-pushed the fix/tier-fs-evict-before-open-4061 branch from 6d505b8 to 330efb3 Compare September 4, 2022 08:44
@N-o-Z N-o-Z requested a review from itaiad200 September 5, 2022 09:52
Copy link
Contributor

@itaiad200 itaiad200 left a 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()) {
Copy link
Contributor

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() {
Copy link
Contributor

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?

Copy link
Member Author

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

@N-o-Z N-o-Z force-pushed the fix/tier-fs-evict-before-open-4061 branch from 330efb3 to ae40f62 Compare September 5, 2022 11:47
@N-o-Z
Copy link
Member Author

N-o-Z commented Sep 5, 2022

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.

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

@N-o-Z N-o-Z requested a review from itaiad200 September 5, 2022 11:56
@@ -256,19 +261,12 @@ func (tfs *TierFS) openFile(ctx context.Context, fileRef localFileRef, fh *os.Fi
}

if !tfs.eviction.Store(fileRef.fsRelativePath, stat.Size()) {
Copy link
Contributor

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.

@itaiad200
Copy link
Contributor

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.

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

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).

@N-o-Z N-o-Z force-pushed the fix/tier-fs-evict-before-open-4061 branch from c3bbb2c to a12a92b Compare September 6, 2022 08:00
Copy link
Contributor

@itaiad200 itaiad200 left a 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!

@N-o-Z N-o-Z added include-changelog PR description should be included in next release changelog and removed exclude-changelog PR description should not be included in next release changelog labels Sep 6, 2022
@N-o-Z N-o-Z merged commit d1873a0 into master Sep 6, 2022
@N-o-Z N-o-Z deleted the fix/tier-fs-evict-before-open-4061 branch September 6, 2022 09:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working include-changelog PR description should be included in next release changelog team/versioning-engine Team versioning engine
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Pyramid: Fix delete file before open
4 participants