in turbonfs/src/file_cache.cpp [2666:2922]
void bytes_chunk_cache::clear_nolock(bool shutdown)
{
AZLogDebug("[{}] Cache purge(shutdown={}): chunkmap.size()={}, "
"backing_file_name={}",
CACHE_TAG, shutdown, chunkmap.size(), backing_file_name);
assert(bytes_allocated <= bytes_allocated_g);
assert(bytes_cached <= bytes_cached_g);
assert((bytes_dirty + bytes_commit_pending) <= bytes_allocated);
/*
* All the data sitting in the cache is either dirty pending commit
* We can't release any of this data so return early.
*/
if (!shutdown && ((bytes_dirty + bytes_commit_pending) == bytes_allocated)) {
AZLogDebug("[{}] Cache purge: backing_file_name={} has no purgeable "
"data",
CACHE_TAG, backing_file_name);
return;
}
/*
* We go over all the bytes_chunk to see if they can be freed. Following
* bytes_chunk cannot be freed (when shutdown is false):
* 1. If it's marked dirty, i.e., it has data which needs to be sync'ed to
* the Blob. This is application data which need to be written to the
* Blob and freeing the bytes_chunk w/o that will cause data consistency
* issues as we have already completed these writes to the application.
* 2. If it's locked, i.e., it currently has some IO ongoing. If the
* ongoing IO is reading data from Blob into the cache, we actually
* do not care, but if the lock is held for writing application data
* into the membuf then we cannot free it.
* 3. If it's marked commit_pending, i.e., it has data which needs to be
* committed to the Blob. This is application data which need to be committed
* to the Blob (in case of commit fails, we may need to resend them) and freeing
* the bytes_chunk w/o that will cause data consistency issues.
*
* Since bytes_chunk_cache::get() increases the inuse count of all membufs
* returned, and it does that while holding the bytes_chunk_cache::lock, we
* can safely remove from chunkmap iff inuse/dirty/locked are not set.
*
* When shutdown is true we don't expect any of the above membuf types to
* be present, so we assert.
*/
const uint64_t start_size = chunkmap.size();
for (auto it = chunkmap.cbegin(), next_it = it;
it != chunkmap.cend();
it = next_it) {
++next_it;
const struct bytes_chunk *bc = &(it->second);
const struct membuf *mb = bc->get_membuf();
/*
* Possibly under IO.
* It could be writer writing application data into the membuf, or
* reader reading Blob data into the membuf. For the read case we don't
* really care but we cannot distinguish between the two.
*
* TODO: Currently this means we also don't invalidate membufs which
* may be fetched for read. Technically these shouldn't be
* skipped.
*/
if (!shutdown) {
if (mb->is_inuse()) {
AZLogDebug("[{}] Cache purge: skipping inuse membuf(offset={}, "
"length={}) (inuse count={}, dirty={})",
CACHE_TAG, mb->offset.load(), mb->length.load(),
mb->get_inuse(), mb->is_dirty());
continue;
}
} else {
if (mb->is_inuse()) {
AZLogError("[{}] Cache purge: Got inuse membuf(offset={}, "
"length={}) (inuse count={}, dirty={}) when shutting "
"down cache",
CACHE_TAG, mb->offset.load(), mb->length.load(),
mb->get_inuse(), mb->is_dirty());
// No membufs should be in use when file is closed.
assert(0);
}
}
/*
* Usually inuse count is dropped after the lock so if inuse count
* is zero membuf must not be locked, but users who may want to
* release() some chunk while holding the lock may drop their inuse
* count to allow release() to release the bytes_chunk.
*/
if (!shutdown) {
if (mb->is_locked()) {
AZLogDebug("[{}] Cache purge: skipping locked membuf(offset={}, "
"length={}) (inuse count={}, dirty={})",
CACHE_TAG, mb->offset.load(), mb->length.load(),
mb->get_inuse(), mb->is_dirty());
continue;
}
} else {
if (mb->is_locked()) {
AZLogError("[{}] Cache purge: Got locked membuf(offset={}, "
"length={}) (inuse count={}, dirty={}) when shutting "
"down cache",
CACHE_TAG, mb->offset.load(), mb->length.load(),
mb->get_inuse(), mb->is_dirty());
// No membufs should be locked when file is closed.
assert(0);
}
}
/*
* Has data to be written to Blob.
* Cannot safely drop this from the cache.
*/
if (!shutdown) {
if (mb->is_dirty()) {
AZLogDebug("[{}] Cache purge: skipping dirty membuf(offset={}, "
"length={})",
CACHE_TAG, mb->offset.load(), mb->length.load());
continue;
}
} else {
/*
* This can happen f.e., when we have dirty membufs due to write
* failures, log and proceed with freeing.
*/
if (mb->is_dirty()) {
AZLogWarn("[{}] Cache purge: Got dirty membuf(offset={}, "
"length={}) when shutting down cache, freeing it. "
"THIS MAY CAUSE FILE DATA TO BE INCONSISTENT!",
CACHE_TAG, mb->offset.load(), mb->length.load());
}
}
/*
* Has data not yet committed.
* Cannot safely drop this from the cache.
*/
if (!shutdown) {
if (mb->is_commit_pending()) {
AZLogDebug("[{}] Cache purge: skipping commit_pending "
"membuf(offset={}, length={})",
CACHE_TAG, mb->offset.load(), mb->length.load());
continue;
}
} else {
/*
* This can happen f.e., when we have uncommitted membufs due to
* write failures, log and proceed with freeing.
*/
if (mb->is_commit_pending()) {
AZLogWarn("[{}] Cache purge: Got commit_pending "
"membuf(offset={}, length={}) when shutting down "
"cache, freeing it. "
"THIS MAY CAUSE FILE DATA TO BE INCONSISTENT!",
CACHE_TAG, mb->offset.load(), mb->length.load());
}
}
AZLogDebug("[{}] Cache purge: deleting membuf(offset={}, length={}), "
"use_count={}, deleted {} of {}",
CACHE_TAG, mb->offset.load(), mb->length.load(),
bc->get_membuf_usecount(),
start_size - chunkmap.size(), start_size);
// Make sure the compound check also passes.
assert(bc->safe_to_release() || shutdown);
/*
* Release the chunk.
* This will release the membuf (munmap() it in case of file-backed
* cache and delete it for heap backed cache). At this point the membuf
* is guaranteed to be not in use since we checked the inuse count
* above.
*/
assert(num_chunks > 0);
num_chunks--;
assert(num_chunks_g > 0);
num_chunks_g--;
assert(bytes_cached >= bc->length);
assert(bytes_cached_g >= bc->length);
bytes_cached -= bc->length;
bytes_cached_g -= bc->length;
chunkmap.erase(it);
}
if (!chunkmap.empty()) {
AZLogDebug("[{}] Cache purge: Skipping delete for backing_file_name={}, "
"as chunkmap not empty (still present {} of {})",
CACHE_TAG, backing_file_name,
chunkmap.size(), start_size);
// On file close, we should free all chunks.
assert(!shutdown);
assert(bytes_allocated > 0);
return;
}
/*
* Entire cache is purged, bytes_cached and bytes_allocated must drop to 0.
*
* Note: If some caller is still holding a bytes_chunk reference, the
* membuf will not be freed and hence bytes_allocated won't drop to 0.
* But, since we allow clear() only when inuse is 0, technically we
* shouldn't have any such user.
*
* XXX Even though we allow clear() only when inuse is 0, it's
* possible that the caller has dropped the inuse ref but is
* still holding on to the bytes_chunk/membuf, which will cause
* bytes_chunk to be removed from the chunkmap but the membuf
* will still not be freed, causing bytes_allocated to not drop
* to 0. f.e., rpc_task::bc_vec holds bytes_chunk references but
* we may drop inuse when read completes.
*/
assert(bytes_cached == 0);
if (bytes_allocated != 0) {
AZLogWarnNR("[{}] Cache purge: bytes_allocated is still {}, some user "
"is still holding on to the bytes_chunk/membuf even after "
"dropping the inuse count: backing_file_name={}",
CACHE_TAG, bytes_allocated.load(), backing_file_name);
#if 0
assert(0);
#endif
}
/*
* If all chunks are released, delete the backing file in case of
* file-backed caches.
*/
if (backing_file_fd != -1) {
const int ret = ::close(backing_file_fd);
if (ret != 0) {
AZLogError("Cache purge: close(fd={}) failed: {}",
backing_file_fd, strerror(errno));
assert(0);
} else {
AZLogDebug("Cache purge: Backing file {} closed, fd={}",
backing_file_name, backing_file_fd);
}
backing_file_fd = -1;
backing_file_len = 0;
}
assert(backing_file_len == 0);
if (!backing_file_name.empty()) {
const int ret = ::unlink(backing_file_name.c_str());
if ((ret != 0) && (errno != ENOENT)) {
AZLogError("Cache purge: unlink({}) failed: {}",
backing_file_name, strerror(errno));
assert(0);
} else {
AZLogDebug("Backing file {} deleted", backing_file_name);
}
}
}