fix(dataset): delete child chunks the index processor leaves behind - #41261
fix(dataset): delete child chunks the index processor leaves behind#41261shaba wants to merge 1 commit into
Conversation
Nothing in clean_dataset_task removes rows from child_chunks. The only
dataset-wide delete lives in ParentChildIndexProcessor.clean(), and the whole
block that performs it sits inside
if dataset.indexing_technique == IndexTechniqueType.HIGH_QUALITY:
so a dataset whose indexing_technique is anything else keeps every child chunk
after it is deleted. The paragraph and QA processors never touch the table at
all.
The same rows are also stranded on the failure path the task already tolerates
on purpose: when index_processor.clean() raises, the task logs it and continues
with document and segment deletion by design. Documents, segments, attachments
and metadata are then removed, and the child chunks that referenced them are
not.
Both leaks are silent - the dataset disappears from the UI while its child
chunks stay in the table forever, with no owner left to trigger a retry.
Delete them explicitly as part of the task. The predicate matches both
tenant_id and dataset_id so it can use the existing compound index
child_chunk_dataset_id_idx on (tenant_id, dataset_id, document_id, segment_id,
index_node_id). Running after the index processor keeps it a no-op for the
high-quality parent-child datasets that are already handled.
Extracted from langgenius#38519 so it can be reviewed on its own; it is independent of
the transaction restructuring proposed there.
Refs langgenius#38518.
Pyrefly Type Coverage
|
Pyrefly Diffbase → PR--- /tmp/pyrefly_base.txt 2026-08-25 14:30:11.220967229 +0000
+++ /tmp/pyrefly_pr.txt 2026-08-25 14:29:56.941871092 +0000
@@ -9143,7 +9143,7 @@
ERROR Argument `SimpleNamespace` is not assignable to parameter `account` with type `Account` in function `services.workflow_restore.apply_published_workflow_snapshot_to_draft` [bad-argument-type]
--> tests/unit_tests/services/workflow/test_workflow_restore.py:70:17
ERROR Argument `list[FromClause]` is not assignable to parameter `tables` with type `Sequence[Table] | None` in function `sqlalchemy.sql.schema.MetaData.create_all` [bad-argument-type]
- --> tests/unit_tests/tasks/test_clean_dataset_task.py:91:56
+ --> tests/unit_tests/tasks/test_clean_dataset_task.py:93:56
ERROR Object of class `FunctionType` has no attribute `return_value` [missing-attribute]
--> tests/unit_tests/tasks/test_dataset_indexing_task.py:51:5
ERROR Object of class `FunctionType` has no attribute `return_value` [missing-attribute]
|
|
The Web Full-Stack E2E failure here is unrelated to this change. This PR touches two files, both Python: For comparison, #38519 changes the same task far more extensively and its run of the same E2E job passed a few minutes later, so the suite was exercising that scenario successfully at the time. API Unit Tests and API Integration Tests both pass here. I do not have permission to re-run the job; a maintainer re-run should clear it. |
|
Correction to my previous comment: I cited #38519 as having passed this same E2E job. That is no longer true — its next run failed the same scenario, so the comparison I drew there does not hold, and I withdraw it. The conclusion is unchanged, but here is the actual evidence. The same scenario also passed and then failed on an unchanged branch here, which is the signature of a flaky test rather than a regression. This PR adds one |
|
Filed the E2E flake as #41263, with the sampling across recent CI runs and an analysis of where the race appears to be. |
Extracted from #38519 so it can be reviewed independently. Refs #38518.
The leak
Nothing in
clean_dataset_taskdeletes rows fromchild_chunks. The only dataset-wide delete lives inParentChildIndexProcessor.clean(), and the entire block that performs it sits inside:A dataset whose
indexing_techniqueis anything other than high quality therefore keeps every child chunk after it is deleted.ParagraphIndexProcessor.clean()andQAIndexProcessor.clean()never touch the table at all.The same rows are stranded on a second path, one the task tolerates on purpose.
clean_dataset_taskwraps the index cleanup in its owntry/exceptand continues deliberately:Documents, segments, attachments and metadata are then deleted, and the child chunks that referenced them are not. So whenever the vector store is unreachable at delete time — the case that
exceptexists for — the child chunks are orphaned even for a high-quality parent-child dataset.Both leaks are silent. The dataset disappears from the UI, the rows stay in the table indefinitely, and because the dataset row is already gone there is nothing left to retry the cleanup.
The change
One
deletein the task, after the index processor has had its turn:The predicate matches both columns so it can use the existing compound index
child_chunk_dataset_id_idxon(tenant_id, dataset_id, document_id, segment_id, index_node_id)— the same access path the parent-child processor already relies on for its own delete.Placing it after the index cleanup makes it a no-op for the high-quality parent-child datasets that are already handled: those rows are gone by the time it runs.
Tests
Two cases in
TestChildChunkCleanup, using the suite's existing SQLite session factory and fixtures:maintests/unit_tests/tasks/test_clean_dataset_task.pypasses: 10 tests.ruff checkandruff format --checkare clean.