{"record":{"id":"a80ddc46a4b11189","repo":"apache/superset","slug":"failed-to-remove-subscription-for-task-task-id","errorCode":null,"errorMessage":"Failed to remove subscription for task {task_id}, user {user_id}","messagePattern":"Failed to remove subscription for task (.+?), user (.+?)","errorType":"exception","errorClass":"DAODeleteFailedError","httpStatus":500,"severity":"error","filePath":"superset/daos/tasks.py","lineNumber":331,"sourceCode":"\n        if not subscription:\n            return None\n\n        try:\n            db.session.delete(subscription)\n            db.session.flush()\n            logger.info(\"Removed subscriber %s from task %s\", user_id, task_id)\n\n            # Return the updated task\n            task = cls.find_by_id(task_id, skip_base_filter=True)\n            if task:\n                db.session.refresh(task)  # Ensure subscribers list is fresh\n            return task\n\n        except DAODeleteFailedError:\n            raise\n        except Exception as ex:\n            raise DAODeleteFailedError(\n                f\"Failed to remove subscription for task {task_id}, user {user_id}\"\n            ) from ex\n\n    @classmethod\n    def set_properties_and_payload(\n        cls,\n        task_uuid: UUID,\n        properties: TaskProperties | None = None,\n        payload: dict[str, Any] | None = None,\n    ) -> bool:\n        \"\"\"\n        Perform a zero-read SQL UPDATE on properties and/or payload columns.\n\n        This method directly writes the provided values without reading first.\n        The caller (TaskContext) is responsible for maintaining the authoritative\n        cached state and passing complete values to write.\n\n        This method is designed for internal task updates (progress, is_abortable)","sourceCodeStart":313,"sourceCodeEnd":349,"githubUrl":"https://github.com/apache/superset/blob/f4587218dd19d046c3e4d00063e7d27f8a2ed354/superset/daos/tasks.py#L313-L349","documentation":"Raised by TaskDAO when removing a subscriber from an alert/report task fails. The DAO performs the delete + flush inside a try block; any unexpected exception (DB error, stale task row, session issue) is re-wrapped as DAODeleteFailedError with the message naming the task_id and user_id. Only the two expected not-found cases raise it directly; everything else lands in the generic except.","triggerScenarios":"DELETE call to remove a user's subscription from a report/alert (TaskDAO.remove_subscriber / the REST endpoint that calls it) where db.session.flush() or the preceding delete raises: task UUID deleted concurrently, metadata DB connection dropped mid-transaction, or a subscriber row already removed by a racing request.","commonSituations":"Two browser tabs unfollowing the same report simultaneously; metadata database (SQLAlchemy session) in a bad state after a long idle connection; operating on a task that was just deleted by an admin.","solutions":["Check the chained cause (ex.__cause__) in logs — the original exception says whether it is a connectivity issue, integrity error, or missing row.","Verify the task_id still exists (TaskDAO.find_by_id) and the user is currently in its subscribers list before retrying the removal.","If it is a connection issue, fix the metadata DB session/pool config (SQLALCHEMY_DATABASE_URI pool_pre_ping, pool_recycle) and retry the API call.","For race conditions (row already gone), treat the 4xx response as success — the end state (user not subscribed) is already achieved."],"exampleFix":"// before\ntask = TaskDAO.remove_subscriber(task_id, user_id)\n\n// after\ntask = TaskDAO.remove_subscriber(task_id, user_id)  # may raise DAODeleteFailedError\n# caller:\ntry:\n    TaskDAO.remove_subscriber(task_id, user_id)\nexcept DAODeleteFailedError:\n    current = TaskDAO.find_by_id(task_id, skip_base_filter=True)\n    if current and user_id in [u.id for u in current.owners]:\n        raise  # real failure\n    # else: already unsubscribed, treat as done","handlingStrategy":"try-catch","validationCode":"from superset.daos.task import TaskDAO\ntask = TaskDAO.find_by_id(task_id, skip_base_filter=True)\nif task and any(u.id == user_id for u in task.owners):\n    TaskDAO.remove_subscriber(task_id, user_id)","typeGuard":null,"tryCatchPattern":"from superset.daos.exceptions import DAODeleteFailedError\ntry:\n    TaskDAO.remove_subscriber(task_id, user_id)\nexcept DAODeleteFailedError as ex:\n    logger.warning(\"unsubscribe failed: %s\", ex, exc_info=ex.__cause__)\n    # idempotent recovery: verify end state\n    task = TaskDAO.find_by_id(task_id, skip_base_filter=True)\n    if task and any(u.id == user_id for u in task.owners):\n        raise","preventionTips":["Check the subscriber relationship exists before issuing the delete.","Make unsubscribe actions idempotent in the UI/API layer (404 or already-removed treated as success).","Keep metadata DB connections healthy (pool_pre_ping/pool_recycle) to avoid flush failures."],"tags":["database","tasks","subscriptions","dao"],"backgroundTag":null,"analyzedSha":"f4587218dd19d046c3e4d00063e7d27f8a2ed354","analyzedAt":"2026-08-14T22:39:27.425Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}