Skip to content

Conversation

@aswinshakil
Copy link
Member

What changes were proposed in this pull request?

While accessing snapshots, We use SnapshotCache to load and retrieve a snapshot's RocksDB instance. The function call is as follows get()->cleanup(), When multiple background process calls get() some threads can also be doing thecleanup() of pending eviction list.

There is a scenario, where Thread 1(KeyDeletingService) is executing get()->cleanup() method and Thread 2(SSTFilteringService) is executing get() (Hasn't reached cleanup() yet), The reference count of the snapshot is incremented by get()(Thread 2) but we still close the rocksDB instance because the cleanup()(Thread1) method assumes everything in the pending eviction list has a reference count of 0. This is not the case in the above-mentioned scenario, We need to recheck if the reference count is still 0 when closing the RocksDB instance.

What is the link to the Apache JIRA

https://issues.apache.org/jira/browse/HDDS-10076

How was this patch tested?

Manually tested with an existing test.

@aswinshakil aswinshakil added the snapshot https://issues.apache.org/jira/browse/HDDS-6517 label Jan 6, 2024
@aswinshakil aswinshakil self-assigned this Jan 6, 2024
Copy link
Contributor

@smengcl smengcl left a comment

Choose a reason for hiding this comment

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

Thanks @aswinshakil . The fix lgtm. Some nits inline.

Copy link
Contributor

@hemantk-12 hemantk-12 left a comment

Choose a reason for hiding this comment

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

Thanks @aswinshakil for the patch.

Overall looks good to me. Left a inline comment, please check that.

} catch (IOException ex) {
throw new IllegalStateException("Error while closing snapshot DB", ex);
}
dbMap.computeIfPresent(key, (k, v) -> {
Copy link
Contributor

Choose a reason for hiding this comment

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

I just realized that we should use compute and log/throw an error if dbMap doesn't contain the key. In case when key is missing from dbMap, it is possible that object was not closed properly or some other inconsistency issue.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for pointing out. Updated the patch.

Copy link
Contributor

@swamirishi swamirishi left a comment

Choose a reason for hiding this comment

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

@aswinshakil Thanks for the quick patch and for identifying the issue. I am not sure if this patch completely fixes the race condition, would like other reviewers as well to double check the same.

+ ", actual: " + v + " for key: " + key);

// Close the instance, which also closes its DB handle.
if (rcOmSnapshot.getTotalRefCount() == 0L) {
Copy link
Contributor

@swamirishi swamirishi Jan 9, 2024

Choose a reason for hiding this comment

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

This particular check is not exactly thread safe there would still be a race condition, this doesn't really take a ref count lock. We should probably put this function check inside ReferenceCounted class which would take a ref count lock when doing the check. Some function like
boolean isTotalRefCountEqual(long expectedValue) which would return if the value is equal by taking a ref count lock. You can refer incrementRefCount function for this.

Copy link
Contributor

Choose a reason for hiding this comment

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

GetKey increaments the ref count after the dbMap.compute() function so the lock is already out of scope. I don't see any change in the get function so this should be the way going forward for the cleanup.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't see a need to get refCountLock unless you are actually updating it. By that logic then we would also need to get a refCountLock everytime we do refCount.get(). @smengcl Do you see any problem with this? AFAIK I don't think this is problem.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah every time we do a refCount.get() if it is going to impact the cache yes we need to take a lock

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess separating out the referenceCounted and cacheLoader is not the right way to implement the cache. From what I know this has go hand in hand in a single lock. Increasing the reference count or decreasing the reference count or when evicting an instance from the cache which would check if there are any references. BTW even decrementRefCount has this problem. decrement is called with referenceCountLock, and it checks if the reference count is zero and adds it to the pending eviction list which checks the count is 0. But in case of a race condition a get call could have a closed rocksdb instance, cleaner thread could have closed the instance in the background.

Copy link
Contributor

Choose a reason for hiding this comment

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

refCount increment should happen when things are loaded in the cache and within the same lock. i.e. the same db.compute method

Copy link
Member Author

Choose a reason for hiding this comment

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

refCount increment cannot happen in the same db.compute() because it will cause a deadlock.

Copy link
Contributor

@smengcl smengcl Jan 10, 2024

Choose a reason for hiding this comment

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

How about a new atomic operation like rcOmSnapshot.closeObjSafely(). It only closes the internal obj if refcount == 0, and it returns true when the internal object is successfully closed?

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess it comes to the same point when we call rcOmSnapshot.closeObjSafely() it returns true but when actually closing it, object is referenced again.

@aswinshakil
Copy link
Member Author

Closing it as #5986 fixes this issue.

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

Labels

snapshot https://issues.apache.org/jira/browse/HDDS-6517

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants