mirror of
https://git.proxmox.com/git/mirror_zfs.git
synced 2026-05-22 02:27:36 +03:00
Fix snapshot automount expiry cancellation deadlock
A deadlock occurs when snapshot expiry tasks are cancelled while holding locks. The snapshot expiry task (snapentry_expire) spawns an umount process and waits for it to complete. Concurrently, ARC memory pressure triggers arc_prune which calls zfs_exit_fs(), attempting to cancel the expiry task while holding locks. The umount process spawned by the expiry task blocks trying to acquire locks held by arc_prune, which is blocked waiting for the expiry task to complete. This creates a circular dependency: expiry task waits for umount, umount waits for arc_prune, arc_prune waits for expiry task. Fix by adding non-blocking cancellation support to taskq_cancel_id(). The zfs_exit_fs() path calls zfsctl_snapshot_unmount_delay() to reschedule the unmount, which needs to cancel any existing expiry task. It now uses non-blocking cancellation to avoid waiting while holding locks, breaking the deadlock by returning immediately when the task is already running. The per-entry se_taskqid_lock has been removed, with all taskqid operations now protected by the global zfs_snapshot_lock held as WRITER. Additionally, an se_in_umount flag prevents recursive waits when zfsctl_destroy() is called during unmount. The taskqid is now only cleared by the caller on successful cancellation; running tasks clear their own taskqid upon completion. Reviewed-by: Alexander Motin <alexander.motin@TrueNAS.com> Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov> Signed-off-by: Ameer Hamza <ahamza@ixsystems.com> Closes #17941
This commit is contained in:
@@ -840,7 +840,7 @@ spl_kmem_cache_destroy(spl_kmem_cache_t *skc)
|
||||
id = skc->skc_taskqid;
|
||||
spin_unlock(&skc->skc_lock);
|
||||
|
||||
taskq_cancel_id(spl_kmem_cache_taskq, id);
|
||||
taskq_cancel_id(spl_kmem_cache_taskq, id, B_TRUE);
|
||||
|
||||
/*
|
||||
* Wait until all current callers complete, this is mainly
|
||||
|
||||
@@ -598,13 +598,22 @@ taskq_of_curthread(void)
|
||||
EXPORT_SYMBOL(taskq_of_curthread);
|
||||
|
||||
/*
|
||||
* Cancel an already dispatched task given the task id. Still pending tasks
|
||||
* will be immediately canceled, and if the task is active the function will
|
||||
* block until it completes. Preallocated tasks which are canceled must be
|
||||
* freed by the caller.
|
||||
* Cancel a dispatched task. Pending tasks are cancelled immediately.
|
||||
* If the task is running, behavior depends on wait parameter:
|
||||
* - wait=B_TRUE: Block until task completes
|
||||
* - wait=B_FALSE: Return EBUSY immediately
|
||||
*
|
||||
* Return values:
|
||||
* 0 - Cancelled before execution. Caller must release resources.
|
||||
* EBUSY - Task running (wait=B_FALSE only). Will self-cleanup.
|
||||
* ENOENT - Not found, or completed after waiting. Already cleaned up.
|
||||
*
|
||||
* Note: wait=B_TRUE returns ENOENT (not EBUSY) after waiting because
|
||||
* the task no longer exists. This distinguishes "cancelled before run"
|
||||
* from "completed naturally" for proper resource management.
|
||||
*/
|
||||
int
|
||||
taskq_cancel_id(taskq_t *tq, taskqid_t id)
|
||||
taskq_cancel_id(taskq_t *tq, taskqid_t id, boolean_t wait)
|
||||
{
|
||||
taskq_ent_t *t;
|
||||
int rc = ENOENT;
|
||||
@@ -667,8 +676,12 @@ taskq_cancel_id(taskq_t *tq, taskqid_t id)
|
||||
spin_unlock_irqrestore(&tq->tq_lock, flags);
|
||||
|
||||
if (t == ERR_PTR(-EBUSY)) {
|
||||
taskq_wait_id(tq, id);
|
||||
rc = EBUSY;
|
||||
if (wait) {
|
||||
taskq_wait_id(tq, id);
|
||||
rc = ENOENT; /* Completed, no longer exists */
|
||||
} else {
|
||||
rc = EBUSY; /* Still running */
|
||||
}
|
||||
}
|
||||
|
||||
return (rc);
|
||||
|
||||
Reference in New Issue
Block a user