void bytes_chunk_cache::clear_nolock()

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);
        }
    }
}