Self Checks
Dify version
v1.15.0
Cloud or Self Hosted
Self Hosted (Docker)
Steps to reproduce
-
Deploy Dify with VECTOR_STORE=qdrant.
-
Use a dataset whose datasets.doc_form and/or datasets.indexing_technique column is empty/null. This is common for knowledge bases created in older Dify versions and carried forward via upgrade. Index some documents so a Qdrant collection such as Vector_index_<uuid>_Node exists on the vector store.
-
Delete the dataset from the UI (or via DELETE /datasets/{id}).
-
Inspect Qdrant afterwards:
The Vector_index_<uuid>_Node collection is still present. Checking the Qdrant HTTP access log shows that no DELETE /collections/... request was ever issued during the deletion.
-
The orphaned collection persists permanently. Repeated deletes keep accumulating more orphans; they are never reconciled.
✔️ Expected Behavior
When a dataset is deleted, the vector-store collection that holds its embeddings must be reliably deleted (or scheduled for deletion that is guaranteed to run), so that no orphaned collection is left behind — regardless of whether doc_form / indexing_technique are populated.
❌ Actual Behavior
The datasets row is removed, but the corresponding Qdrant collection is never deleted and becomes an orphan. The deletion handler short-circuits before dispatching the cleanup task, and there is no retry/reconcile path, so the orphan is permanent.
Root cause (code-level)
The signal handler that owns vector cleanup guards too aggressively and bails out for exactly the case the cleanup task is prepared to handle.
api/events/event_handlers/clean_when_dataset_deleted.py:
@dataset_was_deleted.connect
def handle(sender: Dataset, **kwargs):
dataset = sender
if not dataset.doc_form or not dataset.indexing_technique:
return # ← skips vector cleanup entirely
clean_dataset_task.delay(
dataset.id,
dataset.tenant_id,
dataset.indexing_technique,
dataset.index_struct,
dataset.collection_binding_id,
dataset.doc_form,
dataset.pipeline_id,
)
The early return short-circuits cleanup for any dataset whose doc_form or indexing_technique is empty. Datasets created in older Dify versions (and migrated forward) frequently have one of these fields empty, so deleting them never dispatches clean_dataset_task, and the Qdrant collection is never removed.
This is also internally inconsistent. The very task it guards already defends against an empty doc_form:
# api/tasks/clean_dataset_task.py
if doc_form is None or (isinstance(doc_form, str) and not doc_form.strip()):
# Use default paragraph index type for empty/invalid datasets
# to enable vector database cleanup
doc_form = IndexStructureType.PARAGRAPH_INDEX
So the handler's guard is stricter than the task it protects — it blocks the precise case the task already handles. indexing_technique is likewise not required by the cleanup logic (it is only stored on the reconstructed Dataset object and not used to select the cleanup path), so guarding on it serves no purpose other than skipping cleanup.
A second, related weakness compounds the problem. Inside clean_dataset_task, the vector cleanup is wrapped in try/except; on failure it only logs and then proceeds to commit the rest of the DB cleanup:
# api/tasks/clean_dataset_task.py
try:
index_processor = IndexProcessorFactory(doc_form).init_index_processor()
index_processor.clean(dataset, None, with_keywords=True, delete_child_chunks=True)
except Exception:
logger.exception(...)
# Continue with document and segment deletion even if vector cleanup fails
Because delete_dataset has already committed the deletion of the datasets row (db.session.delete(dataset) + commit) before the async task runs, a failed cleanup can never be retried — the orphan is permanent and silent.
Impact
Orphans accumulate silently and invisibly (the API never reports them). On a qdrant container restart/recreate, all collections — including orphans — are loaded at once, spiking open file handles. If the container's nofile ulimit is modest, this triggers RocksDB IO error: Too many open files → Qdrant panic → restart: always loop → all knowledge-base retrieval fails. From the API side this surfaces as:
InactiveRpcError ... StatusCode.UNAVAILABLE
details: "DNS resolution failed for qdrant:6334: ... Domain name not found"
(The DNS error is a symptom: the qdrant container is crash-looping and intermittently unresolvable, not an actual DNS problem.)
We hit exactly this in production after a version upgrade that recreated the qdrant container: the container restart-looped ~100 times and knowledge-base retrieval was down.
Self Checks
Dify version
v1.15.0
Cloud or Self Hosted
Self Hosted (Docker)
Steps to reproduce
Deploy Dify with
VECTOR_STORE=qdrant.Use a dataset whose
datasets.doc_formand/ordatasets.indexing_techniquecolumn is empty/null. This is common for knowledge bases created in older Dify versions and carried forward via upgrade. Index some documents so a Qdrant collection such asVector_index_<uuid>_Nodeexists on the vector store.Delete the dataset from the UI (or via
DELETE /datasets/{id}).Inspect Qdrant afterwards:
The
Vector_index_<uuid>_Nodecollection is still present. Checking the Qdrant HTTP access log shows that noDELETE /collections/...request was ever issued during the deletion.The orphaned collection persists permanently. Repeated deletes keep accumulating more orphans; they are never reconciled.
✔️ Expected Behavior
When a dataset is deleted, the vector-store collection that holds its embeddings must be reliably deleted (or scheduled for deletion that is guaranteed to run), so that no orphaned collection is left behind — regardless of whether
doc_form/indexing_techniqueare populated.❌ Actual Behavior
The
datasetsrow is removed, but the corresponding Qdrant collection is never deleted and becomes an orphan. The deletion handler short-circuits before dispatching the cleanup task, and there is no retry/reconcile path, so the orphan is permanent.Root cause (code-level)
The signal handler that owns vector cleanup guards too aggressively and bails out for exactly the case the cleanup task is prepared to handle.
api/events/event_handlers/clean_when_dataset_deleted.py:The early
returnshort-circuits cleanup for any dataset whosedoc_formorindexing_techniqueis empty. Datasets created in older Dify versions (and migrated forward) frequently have one of these fields empty, so deleting them never dispatchesclean_dataset_task, and the Qdrant collection is never removed.This is also internally inconsistent. The very task it guards already defends against an empty
doc_form:So the handler's guard is stricter than the task it protects — it blocks the precise case the task already handles.
indexing_techniqueis likewise not required by the cleanup logic (it is only stored on the reconstructedDatasetobject and not used to select the cleanup path), so guarding on it serves no purpose other than skipping cleanup.A second, related weakness compounds the problem. Inside
clean_dataset_task, the vector cleanup is wrapped intry/except; on failure it only logs and then proceeds to commit the rest of the DB cleanup:Because
delete_datasethas already committed the deletion of thedatasetsrow (db.session.delete(dataset)+commit) before the async task runs, a failed cleanup can never be retried — the orphan is permanent and silent.Impact
Orphans accumulate silently and invisibly (the API never reports them). On a qdrant container restart/recreate, all collections — including orphans — are loaded at once, spiking open file handles. If the container's
nofileulimit is modest, this triggers RocksDBIO error: Too many open files→ Qdrant panic →restart: alwaysloop → all knowledge-base retrieval fails. From the API side this surfaces as:(The DNS error is a symptom: the qdrant container is crash-looping and intermittently unresolvable, not an actual DNS problem.)
We hit exactly this in production after a version upgrade that recreated the qdrant container: the container restart-looped ~100 times and knowledge-base retrieval was down.