}
 
 /*
- * Check if an inode was logged in the current transaction. This may often
- * return some false positives, because logged_trans is an in memory only field,
- * not persisted anywhere. This is meant to be used in contexts where a false
- * positive has no functional consequences.
+ * Check if an inode was logged in the current transaction. This correctly deals
+ * with the case where the inode was logged but has a logged_trans of 0, which
+ * happens if the inode is evicted and loaded again, as logged_trans is an in
+ * memory only field (not persisted).
+ *
+ * Returns 1 if the inode was logged before in the transaction, 0 if it was not,
+ * and < 0 on error.
  */
-static bool inode_logged(struct btrfs_trans_handle *trans,
-                        struct btrfs_inode *inode)
+static int inode_logged(struct btrfs_trans_handle *trans,
+                       struct btrfs_inode *inode,
+                       struct btrfs_path *path_in)
 {
+       struct btrfs_path *path = path_in;
+       struct btrfs_key key;
+       int ret;
+
        if (inode->logged_trans == trans->transid)
-               return true;
+               return 1;
 
-       if (!test_bit(BTRFS_ROOT_HAS_LOG_TREE, &inode->root->state))
-               return false;
+       /*
+        * If logged_trans is not 0, then we know the inode logged was not logged
+        * in this transaction, so we can return false right away.
+        */
+       if (inode->logged_trans > 0)
+               return 0;
+
+       /*
+        * If no log tree was created for this root in this transaction, then
+        * the inode can not have been logged in this transaction. In that case
+        * set logged_trans to anything greater than 0 and less than the current
+        * transaction's ID, to avoid the search below in a future call in case
+        * a log tree gets created after this.
+        */
+       if (!test_bit(BTRFS_ROOT_HAS_LOG_TREE, &inode->root->state)) {
+               inode->logged_trans = trans->transid - 1;
+               return 0;
+       }
+
+       /*
+        * We have a log tree and the inode's logged_trans is 0. We can't tell
+        * for sure if the inode was logged before in this transaction by looking
+        * only at logged_trans. We could be pessimistic and assume it was, but
+        * that can lead to unnecessarily logging an inode during rename and link
+        * operations, and then further updating the log in followup rename and
+        * link operations, specially if it's a directory, which adds latency
+        * visible to applications doing a series of rename or link operations.
+        *
+        * A logged_trans of 0 here can mean several things:
+        *
+        * 1) The inode was never logged since the filesystem was mounted, and may
+        *    or may have not been evicted and loaded again;
+        *
+        * 2) The inode was logged in a previous transaction, then evicted and
+        *    then loaded again;
+        *
+        * 3) The inode was logged in the current transaction, then evicted and
+        *    then loaded again.
+        *
+        * For cases 1) and 2) we don't want to return true, but we need to detect
+        * case 3) and return true. So we do a search in the log root for the inode
+        * item.
+        */
+       key.objectid = btrfs_ino(inode);
+       key.type = BTRFS_INODE_ITEM_KEY;
+       key.offset = 0;
+
+       if (!path) {
+               path = btrfs_alloc_path();
+               if (!path)
+                       return -ENOMEM;
+       }
+
+       ret = btrfs_search_slot(NULL, inode->root->log_root, &key, path, 0, 0);
+
+       if (path_in)
+               btrfs_release_path(path);
+       else
+               btrfs_free_path(path);
 
        /*
-        * The inode's logged_trans is always 0 when we load it (because it is
-        * not persisted in the inode item or elsewhere). So if it is 0, the
-        * inode was last modified in the current transaction then the inode may
-        * have been logged before in the current transaction, then evicted and
-        * loaded again in the current transaction - or may have never been logged
-        * in the current transaction, but since we can not be sure, we have to
-        * assume it was, otherwise our callers can leave an inconsistent log.
+        * Logging an inode always results in logging its inode item. So if we
+        * did not find the item we know the inode was not logged for sure.
         */
-       if (inode->logged_trans == 0 &&
-           inode->last_trans == trans->transid &&
-           !test_bit(BTRFS_FS_LOG_RECOVERING, &trans->fs_info->flags))
-               return true;
+       if (ret < 0) {
+               return ret;
+       } else if (ret > 0) {
+               /*
+                * Set logged_trans to a value greater than 0 and less then the
+                * current transaction to avoid doing the search in future calls.
+                */
+               inode->logged_trans = trans->transid - 1;
+               return 0;
+       }
+
+       /*
+        * The inode was previously logged and then evicted, set logged_trans to
+        * the current transacion's ID, to avoid future tree searches as long as
+        * the inode is not evicted again.
+        */
+       inode->logged_trans = trans->transid;
+
+       /*
+        * If it's a directory, then we must set last_dir_index_offset to the
+        * maximum possible value, so that the next attempt to log the inode does
+        * not skip checking if dir index keys found in modified subvolume tree
+        * leaves have been logged before, otherwise it would result in attempts
+        * to insert duplicate dir index keys in the log tree. This must be done
+        * because last_dir_index_offset is an in-memory only field, not persisted
+        * in the inode item or any other on-disk structure, so its value is lost
+        * once the inode is evicted.
+        */
+       if (S_ISDIR(inode->vfs_inode.i_mode))
+               inode->last_dir_index_offset = (u64)-1;
 
-       return false;
+       return 1;
 }
 
 /*
        struct btrfs_path *path;
        int ret;
 
-       if (!inode_logged(trans, dir))
+       ret = inode_logged(trans, dir, NULL);
+       if (ret == 0)
+               return;
+       else if (ret < 0) {
+               btrfs_set_log_full_commit(trans);
                return;
+       }
 
        ret = join_running_log_trans(root);
        if (ret)
        u64 index;
        int ret;
 
-       if (!inode_logged(trans, inode))
+       ret = inode_logged(trans, inode, NULL);
+       if (ret == 0)
                return;
+       else if (ret < 0) {
+               btrfs_set_log_full_commit(trans);
+               return;
+       }
 
        ret = join_running_log_trans(root);
        if (ret)
        struct extent_buffer *src = path->nodes[0];
        const int nritems = btrfs_header_nritems(src);
        const u64 ino = btrfs_ino(inode);
-       const bool inode_logged_before = inode_logged(trans, inode);
        bool last_found = false;
        int batch_start = 0;
        int batch_size = 0;
                                ctx->log_new_dentries = true;
                }
 
-               if (!inode_logged_before)
+               if (!ctx->logged_before)
                        goto add_to_batch;
 
                /*
                 * If we were logged before and have logged dir items, we can skip
                 * checking if any item with a key offset larger than the last one
                 * we logged is in the log tree, saving time and avoiding adding
-                * contention on the log tree.
+                * contention on the log tree. We can only rely on the value of
+                * last_dir_index_offset when we know for sure that the inode was
+                * previously logged in the current transaction.
                 */
                if (key.offset > inode->last_dir_index_offset)
                        goto add_to_batch;
        u64 max_key;
        int ret;
 
-       /*
-        * If this is the first time we are being logged in the current
-        * transaction, or we were logged before but the inode was evicted and
-        * reloaded later, in which case its logged_trans is 0, reset the value
-        * of the last logged key offset. Note that we don't use the helper
-        * function inode_logged() here - that is because the function returns
-        * true after an inode eviction, assuming the worst case as it can not
-        * know for sure if the inode was logged before. So we can not skip key
-        * searches in the case the inode was evicted, because it may not have
-        * been logged in this transaction and may have been logged in a past
-        * transaction, so we need to reset the last dir index offset to (u64)-1.
-        */
-       if (inode->logged_trans != trans->transid)
-               inode->last_dir_index_offset = (u64)-1;
-
        min_key = BTRFS_DIR_START_INDEX;
        max_key = 0;
        ctx->last_dir_item_offset = inode->last_dir_index_offset;
        struct btrfs_key found_key;
        int start_slot;
 
-       if (!inode_logged(trans, inode))
-               return 0;
-
        key.objectid = btrfs_ino(inode);
        key.type = max_key_type;
        key.offset = (u64)-1;
         * are small, with a root at level 2 or 3 at most, due to their short
         * life span.
         */
-       if (inode_logged(trans, inode)) {
+       if (ctx->logged_before) {
                drop_args.path = path;
                drop_args.start = em->start;
                drop_args.end = em->start + em->len;
        bool xattrs_logged = false;
        bool recursive_logging = false;
        bool inode_item_dropped = true;
+       const bool orig_logged_before = ctx->logged_before;
 
        path = btrfs_alloc_path();
        if (!path)
                mutex_lock(&inode->log_mutex);
        }
 
+       /*
+        * Before logging the inode item, cache the value returned by
+        * inode_logged(), because after that we have the need to figure out if
+        * the inode was previously logged in this transaction.
+        */
+       ret = inode_logged(trans, inode, path);
+       if (ret < 0) {
+               err = ret;
+               goto out_unlock;
+       }
+       ctx->logged_before = (ret == 1);
+
        /*
         * This is for cases where logging a directory could result in losing a
         * a file after replaying the log. For example, if we move a file from a
                clear_bit(BTRFS_INODE_COPY_EVERYTHING, &inode->runtime_flags);
                if (inode_only == LOG_INODE_EXISTS)
                        max_key_type = BTRFS_XATTR_ITEM_KEY;
-               ret = drop_inode_items(trans, log, path, inode, max_key_type);
+               if (ctx->logged_before)
+                       ret = drop_inode_items(trans, log, path, inode,
+                                              max_key_type);
        } else {
-               if (inode_only == LOG_INODE_EXISTS && inode_logged(trans, inode)) {
+               if (inode_only == LOG_INODE_EXISTS && ctx->logged_before) {
                        /*
                         * Make sure the new inode item we write to the log has
                         * the same isize as the current one (if it exists).
                             &inode->runtime_flags)) {
                        if (inode_only == LOG_INODE_EXISTS) {
                                max_key.type = BTRFS_XATTR_ITEM_KEY;
-                               ret = drop_inode_items(trans, log, path, inode,
-                                                      max_key.type);
+                               if (ctx->logged_before)
+                                       ret = drop_inode_items(trans, log, path,
+                                                              inode, max_key.type);
                        } else {
                                clear_bit(BTRFS_INODE_NEEDS_FULL_SYNC,
                                          &inode->runtime_flags);
                                clear_bit(BTRFS_INODE_COPY_EVERYTHING,
                                          &inode->runtime_flags);
-                               if (inode_logged(trans, inode))
+                               if (ctx->logged_before)
                                        ret = truncate_inode_items(trans, log,
                                                                   inode, 0, 0);
                        }
                        if (inode_only == LOG_INODE_ALL)
                                fast_search = true;
                        max_key.type = BTRFS_XATTR_ITEM_KEY;
-                       ret = drop_inode_items(trans, log, path, inode,
-                                              max_key.type);
+                       if (ctx->logged_before)
+                               ret = drop_inode_items(trans, log, path, inode,
+                                                      max_key.type);
                } else {
                        if (inode_only == LOG_INODE_ALL)
                                fast_search = true;
 out:
        btrfs_free_path(path);
        btrfs_free_path(dst_path);
+
+       if (recursive_logging)
+               ctx->logged_before = orig_logged_before;
+
        return err;
 }
 
        struct btrfs_root *root = inode->root;
        struct btrfs_log_ctx ctx;
        bool log_pinned = false;
-       int ret = 0;
+       int ret;
 
        /*
         * this will force the logging code to walk the dentry chain
         * if this inode hasn't been logged and directory we're renaming it
         * from hasn't been logged, we don't need to log it
         */
-       if (!inode_logged(trans, inode) &&
-           (!old_dir || !inode_logged(trans, old_dir)))
-               return;
+       ret = inode_logged(trans, inode, NULL);
+       if (ret < 0) {
+               goto out;
+       } else if (ret == 0) {
+               if (!old_dir)
+                       return;
+               /*
+                * If the inode was not logged and we are doing a rename (old_dir is not
+                * NULL), check if old_dir was logged - if it was not we can return and
+                * do nothing.
+                */
+               ret = inode_logged(trans, old_dir, NULL);
+               if (ret < 0)
+                       goto out;
+               else if (ret == 0)
+                       return;
+       }
+       ret = 0;
 
        /*
         * If we are doing a rename (old_dir is not NULL) from a directory that
         */
        btrfs_log_inode_parent(trans, inode, parent, LOG_INODE_EXISTS, &ctx);
 out:
-       if (log_pinned) {
-               /*
-                * If an error happened mark the log for a full commit because
-                * it's not consistent and up to date. Do it before unpinning the
-                * log, to avoid any races with someone else trying to commit it.
-                */
-               if (ret < 0)
-                       btrfs_set_log_full_commit(trans);
+       /*
+        * If an error happened mark the log for a full commit because it's not
+        * consistent and up to date or we couldn't find out if one of the
+        * inodes was logged before in this transaction. Do it before unpinning
+        * the log, to avoid any races with someone else trying to commit it.
+        */
+       if (ret < 0)
+               btrfs_set_log_full_commit(trans);
+       if (log_pinned)
                btrfs_end_log_trans(root);
-       }
 }