diff --git a/docs/data-management/delete.md b/docs/data-management/delete.md
index cf571566edec..799e8b4b8b91 100644
--- a/docs/data-management/delete.md
+++ b/docs/data-management/delete.md
@@ -112,9 +112,13 @@ Some of the parameters used in the task payload are further explained below:
| `limit` | null (no limit) | Maximum number of segments for the kill task to delete.|
| `maxUsedStatusLastUpdatedTime` | null (no cutoff) | Maximum timestamp used as a cutoff to include unused segments. The kill task only considers segments which lie in the specified `interval` and were marked as unused no later than this time. The default behavior is to kill all unused segments in the `interval` regardless of when they where marked as unused.|
-
-**WARNING:** The `kill` task permanently removes all information about the affected segments from the metadata store and
+:::warning
+- The `kill` task permanently removes all information about the affected segments from the metadata store and
deep storage. This operation cannot be undone.
+- When using [concurrent locks](../ingestion/concurrent-append-replace.md) to run a `kill` task, ensure to keep a large
+enough buffer period before killing segments after they have been marked as unused. Otherwise, there may be a potential
+data loss if a concurrent append job upgrades one of the segments that are being killed.
+:::
### Auto-kill data using Coordinator duties
diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/common/task/KillUnusedSegmentsTask.java b/indexing-service/src/main/java/org/apache/druid/indexing/common/task/KillUnusedSegmentsTask.java
index fe58c264ca8b..21b9e3f6a799 100644
--- a/indexing-service/src/main/java/org/apache/druid/indexing/common/task/KillUnusedSegmentsTask.java
+++ b/indexing-service/src/main/java/org/apache/druid/indexing/common/task/KillUnusedSegmentsTask.java
@@ -48,10 +48,10 @@
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.java.util.common.logger.Logger;
import org.apache.druid.server.coordination.BroadcastDatasourceLoadingSpec;
+import org.apache.druid.server.http.DataSegmentPlus;
import org.apache.druid.server.lookup.cache.LookupLoadingSpec;
import org.apache.druid.server.security.ResourceAction;
import org.apache.druid.timeline.DataSegment;
-import org.apache.druid.timeline.SegmentId;
import org.apache.druid.utils.CollectionUtils;
import org.joda.time.DateTime;
import org.joda.time.Interval;
@@ -67,10 +67,10 @@
import java.util.NavigableMap;
import java.util.Set;
import java.util.TreeMap;
+import java.util.function.Function;
import java.util.stream.Collectors;
/**
- *
* The client representation of this task is {@link ClientKillUnusedSegmentsTaskQuery}.
* JSON serialization fields of this class must correspond to those of {@link
* ClientKillUnusedSegmentsTaskQuery}, except for {@link #id} and {@link #context} fields.
@@ -87,6 +87,10 @@
*
Filter the set of unreferenced segments using load specs from the set of used segments.
*
Kill the filtered set of segments from deep storage.
*
+ * Note: When {@link Tasks#USE_CONCURRENT_LOCKS} is true, keep a large buffer
+ * period before killing segments after they have been marked as unused.
+ * Otherwise, there may be a potential data loss if a concurrent APPEND job
+ * upgrades one of the segments that are being killed.
*/
public class KillUnusedSegmentsTask extends AbstractFixedIntervalTask
{
@@ -211,7 +215,7 @@ public TaskStatus runTask(TaskToolbox toolbox) throws Exception
// List unused segments
int nextBatchSize = computeNextBatchSize(numSegmentsKilled);
@Nullable Integer numTotalBatches = getNumTotalBatches();
- List unusedSegments;
+ List unusedSegmentsPlus;
logInfo(
"Starting kill for datasource[%s] in interval[%s] and versions[%s] with batchSize[%d], up to limit[%d]"
+ " segments before maxUsedStatusLastUpdatedTime[%s] will be deleted%s",
@@ -236,12 +240,25 @@ public TaskStatus runTask(TaskToolbox toolbox) throws Exception
break;
}
- unusedSegments = fetchNextBatchOfUnusedSegments(toolbox, nextBatchSize);
+ unusedSegmentsPlus = fetchNextBatchOfUnusedSegments(toolbox, nextBatchSize);
+ if (unusedSegmentsPlus.isEmpty()) {
+ // No more segments eligible for kill, do not proceed further
+ break;
+ }
// Fetch locks each time as a revokal could have occurred in between batches
final NavigableMap> taskLockMap
= getNonRevokedTaskLockMap(toolbox.getTaskActionClient());
+ final Set unusedSegments = unusedSegmentsPlus.stream()
+ .map(DataSegmentPlus::getDataSegment)
+ .collect(Collectors.toSet());
+ final Map unusedIdToSegmentPlus = CollectionUtils.toMap(
+ unusedSegmentsPlus,
+ segment -> segment.getDataSegment().getId().toString(),
+ Function.identity()
+ );
+
if (!TaskLocks.isLockCoversSegments(taskLockMap, unusedSegments)) {
throw new ISE(
"Locks[%s] for task[%s] can't cover segments[%s]",
@@ -251,62 +268,46 @@ public TaskStatus runTask(TaskToolbox toolbox) throws Exception
);
}
- // Kill segments. Order is important here:
- // Retrieve the segment upgrade infos for the batch _before_ the segments are nuked
- // We then want the nuke action to clean up the metadata records _before_ the segments are removed from storage.
- // This helps maintain that we will always have a storage segment if the metadata segment is present.
- // Determine the subset of segments to be killed from deep storage based on loadspecs.
- // If the segment nuke throws an exception, then the segment cleanup is abandoned.
-
- // Determine upgraded segment ids before nuking
- final Set segmentIds = unusedSegments.stream()
- .map(DataSegment::getId)
- .map(SegmentId::toString)
- .collect(Collectors.toSet());
- final Map upgradedFromSegmentIds = new HashMap<>();
- try {
- upgradedFromSegmentIds.putAll(
- taskActionClient.submit(
- new RetrieveUpgradedFromSegmentIdsAction(getDataSource(), segmentIds)
- ).getUpgradedFromSegmentIds()
- );
- }
- catch (Exception e) {
- LOG.warn(
- e,
- "Could not retrieve parent segment ids using task action[retrieveUpgradedFromSegmentIds]."
- + " Overlord may be on an older version."
- );
- }
+ // Kill segments - order of steps 1, 2, 3, 4 must remain the same
- // Nuke Segments
- taskActionClient.submit(new SegmentNukeAction(new HashSet<>(unusedSegments)));
- emitMetric(toolbox.getEmitter(), TaskMetrics.SEGMENTS_DELETED_FROM_METADATA_STORE, unusedSegments.size());
+ // 1. Determine parent segment ids of killable unused segments
+ final Map upgradedFromSegmentIds
+ = fetchParentIdsForSegments(toolbox, unusedIdToSegmentPlus);
- // Determine segments to be killed
- final List segmentsToBeKilled
- = getKillableSegments(unusedSegments, upgradedFromSegmentIds, usedSegmentLoadSpecs, taskActionClient);
+ // 2. Identify killable segments whose load specs are not shared with any other segment
+ final List segmentsToKillFromDeepStore = getKillableSegments(
+ unusedIdToSegmentPlus,
+ upgradedFromSegmentIds,
+ usedSegmentLoadSpecs,
+ taskActionClient
+ );
+ // 2a. Track segments that cannot be removed from deep store yet
final Set segmentsNotKilled = new HashSet<>(unusedSegments);
- segmentsToBeKilled.forEach(segmentsNotKilled::remove);
-
+ segmentsToKillFromDeepStore.forEach(segmentsNotKilled::remove);
if (!segmentsNotKilled.isEmpty()) {
LOG.warn(
- "Skipping kill of [%d] segments from deep storage as their load specs are used by other segments.",
- segmentsNotKilled.size()
+ "Skipping kill of [%d] segments of datasource[%s] from deep storage"
+ + " as their load specs are shared by other segments.",
+ segmentsNotKilled.size(), getDataSource()
);
}
- toolbox.getDataSegmentKiller().kill(segmentsToBeKilled);
- emitMetric(toolbox.getEmitter(), TaskMetrics.SEGMENTS_DELETED_FROM_DEEPSTORE, segmentsToBeKilled.size());
+ // 3. Nuke all eligible unused segments
+ taskActionClient.submit(new SegmentNukeAction(unusedSegments));
+ emitMetric(toolbox.getEmitter(), TaskMetrics.SEGMENTS_DELETED_FROM_METADATA_STORE, unusedIdToSegmentPlus.size());
+
+ // 4. Delete deep store files only for segments which do not share load specs with other segments
+ toolbox.getDataSegmentKiller().kill(segmentsToKillFromDeepStore);
+ emitMetric(toolbox.getEmitter(), TaskMetrics.SEGMENTS_DELETED_FROM_DEEPSTORE, segmentsToKillFromDeepStore.size());
numBatchesProcessed++;
- numSegmentsKilled += segmentsToBeKilled.size();
+ numSegmentsKilled += segmentsToKillFromDeepStore.size();
logInfo("Processed [%d] batches for kill task[%s].", numBatchesProcessed, getId());
nextBatchSize = computeNextBatchSize(numSegmentsKilled);
- } while (!unusedSegments.isEmpty() && (null == numTotalBatches || numBatchesProcessed < numTotalBatches));
+ } while (!unusedSegmentsPlus.isEmpty() && (null == numTotalBatches || numBatchesProcessed < numTotalBatches));
final String taskId = getId();
logInfo(
@@ -342,7 +343,7 @@ int computeNextBatchSize(int numSegmentsKilled)
/**
* Fetches the next batch of unused segments that are eligible for kill.
*/
- protected List fetchNextBatchOfUnusedSegments(TaskToolbox toolbox, int nextBatchSize) throws IOException
+ protected List fetchNextBatchOfUnusedSegments(TaskToolbox toolbox, int nextBatchSize) throws IOException
{
return toolbox.getTaskActionClient().submit(
new RetrieveUnusedSegmentsAction(
@@ -352,7 +353,44 @@ protected List fetchNextBatchOfUnusedSegments(TaskToolbox toolbox,
nextBatchSize,
maxUsedStatusLastUpdatedTime
)
- );
+ )
+ .stream()
+ .map(segment -> new DataSegmentPlus(segment, null, null, null, null, null, null, null))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Fetches the parent IDs (if any) for the given unused segments.
+ *
+ * @param unusedIdToSegmentPlus Map containing unused segments whose parent IDs
+ * need to be fetched
+ * @return Map from segment ID to the segment ID from which
+ * it was upgraded. If an input segment was not upgraded from any other segment,
+ * it does not have an entry in the map.
+ */
+ protected Map fetchParentIdsForSegments(
+ TaskToolbox toolbox,
+ Map unusedIdToSegmentPlus
+ )
+ {
+ try {
+ return toolbox.getTaskActionClient().submit(
+ new RetrieveUpgradedFromSegmentIdsAction(getDataSource(), unusedIdToSegmentPlus.keySet())
+ ).getUpgradedFromSegmentIds();
+ }
+ catch (Exception e) {
+ // Do not proceed with killing these segments as we cannot be sure if their
+ // load spec is shared by any other segment or not. If load spec is shared,
+ // segment files cannot be deleted from deep store. If load spec is not
+ // shared, segments cannot be deleted from metadata store as that would
+ // leave deep store files orphaned, and they would never be cleaned up.
+ throw new ISE(
+ e,
+ "Could not retrieve parent segment ids using task action[retrieveUpgradedFromSegmentIds]."
+ + " Stopping kill task to avoid data loss in case the segment files"
+ + " are shared by other segments."
+ );
+ }
}
/**
@@ -387,21 +425,20 @@ private NavigableMap> getNonRevokedTaskLockMap(TaskActi
* @return list of segments to kill from deep storage
*/
private List getKillableSegments(
- List unusedSegments,
+ Map unusedSegments,
Map upgradedFromSegmentIds,
Set