This is an automated email from the ASF dual-hosted git repository.

kfaraz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new bb9924e59fd perf: speed-up coordinator segment metadata cache delta 
syncs (#19672)
bb9924e59fd is described below

commit bb9924e59fd477cb1cd21712e9339a3b1029d95c
Author: jtuglu1 <[email protected]>
AuthorDate: Tue Jul 14 00:49:52 2026 -0700

    perf: speed-up coordinator segment metadata cache delta syncs (#19672)
    
    Changes:
    - Update `DataSourcesSnapshot` for only the datasources that have been 
updated
    - Maintain a `hasNewWrites` flag in `HeapMemoryDatasourceSegmentCache` to 
track
    new writes since the last snapshot creation
    - Add new index on `druid_segments` metadata table to speed up cache syncs
    - Add docs
---
 docs/operations/metrics.md                         |  11 ++
 .../storage/sqlserver/SQLServerConnector.java      |   7 ++
 .../metadata/storage/mysql/MySQLConnector.java     |   7 ++
 .../apache/druid/client/DataSourcesSnapshot.java   |  79 +++++++++++++
 .../druid/metadata/SQLMetadataConnector.java       |  79 ++++++++++++-
 .../cache/HeapMemoryDatasourceSegmentCache.java    |  50 +++++++++
 .../cache/HeapMemorySegmentMetadataCache.java      |  84 ++++++++++++--
 .../druid/client/DataSourcesSnapshotTest.java      | 125 +++++++++++++++++++++
 .../druid/metadata/SQLMetadataConnectorTest.java   |  24 ++--
 .../cache/HeapMemorySegmentMetadataCacheTest.java  |  30 +++++
 10 files changed, 473 insertions(+), 23 deletions(-)

diff --git a/docs/operations/metrics.md b/docs/operations/metrics.md
index 81f8f21b917..ece838c8a93 100644
--- a/docs/operations/metrics.md
+++ b/docs/operations/metrics.md
@@ -383,11 +383,19 @@ The following metrics are emitted only when [segment 
metadata caching](../config
 |`segment/pending/count`|Number of pending segments currently present in the 
metadata store.|`dataSource`|
 |`segment/metadataCache/interval/count`|Total number of intervals present in 
the cache for a single datasource.|`dataSource`|
 |`segment/metadataCache/used/count`|Total number of used segments present in 
the cache for a single datasource.|`dataSource`|
+|`segment/metadataCache/unused/count`|Total number of unused segments present 
in the cache for a single datasource.|`dataSource`|
 |`segment/metadataCache/pending/count`|Total number of pending segments 
present in the cache for a single datasource.|`dataSource`|
 |`segment/metadataCache/transactions/readOnly`|Number of read-only 
transactions performed on the cache for a single datasource.|`dataSource`|
 |`segment/metadataCache/transactions/readWrite`|Number of read-write 
transactions performed on the cache for a single datasource.|`dataSource`|
 |`segment/metadataCache/transactions/writeOnly`|Number of write-only 
transactions performed on the cache for a single datasource. These transactions 
happen only if the cache is operating in mode `ifSynced` and the first sync on 
the leader Overlord is not complete yet.|`dataSource`|
 |`segment/metadataCache/sync/time`|Number of milliseconds taken for the cache 
to sync with the metadata store.||
+|`segment/metadataCache/fetchIds/time`|Time taken in a sync to fetch the IDs 
of used segments from the metadata store.||
+|`segment/metadataCache/fetchPayloads/time`|Time taken in a sync to fetch the 
payloads of used segments that have changed since the last sync.||
+|`segment/metadataCache/fetchPending/time`|Time taken in a sync to fetch 
pending segments from the metadata store.||
+|`segment/metadataCache/fetchSchemas/time`|Time taken in a sync to fetch 
segment schemas from the metadata store. Emitted only when centralized 
datasource schema is enabled.||
+|`segment/metadataCache/fetchIndexingStates/time`|Time taken in a sync to 
fetch segment indexing states from the metadata store.||
+|`segment/metadataCache/updateIds/time`|Time taken in a sync to reconcile the 
cached segment IDs of each datasource with the metadata store.||
+|`segment/metadataCache/updateSnapshot/time`|Time taken in a sync to rebuild 
the datasource snapshot from the cache.||
 |`segment/metadataCache/dataSource/deleted`|Indicates that a datasource has no 
used or pending segments anymore and has been removed from the 
cache.|`dataSource`|
 |`segment/metadataCache/deleted`|Total number of segments deleted from the 
cache during the latest sync.||
 |`segment/metadataCache/skipped`|Total number of unparseable segment records 
that were skipped in the latest sync.||
@@ -396,6 +404,9 @@ The following metrics are emitted only when [segment 
metadata caching](../config
 |`segment/metadataCache/pending/deleted`|Number of pending segments deleted 
from the cache during the latest sync.|`dataSource`|
 |`segment/metadataCache/pending/updated`|Number of pending segments updated in 
the cache during the latest sync.|`dataSource`|
 |`segment/metadataCache/pending/skipped`|Number of unparseable pending segment 
records that were skipped in the latest sync.|`dataSource`|
+|`segment/metadataCache/schema/skipped`|Number of unparseable segment schema 
records that were skipped in the latest sync. Emitted only when centralized 
datasource schema is enabled.||
+|`segment/metadataCache/indexingState/added`|Number of segment indexing states 
added to the cache during the latest sync.||
+|`segment/metadataCache/indexingState/deleted`|Number of segment indexing 
states deleted from the cache during the latest sync.||
 
 ### Auto-kill unused segments
 
diff --git 
a/extensions-contrib/sqlserver-metadata-storage/src/main/java/org/apache/druid/metadata/storage/sqlserver/SQLServerConnector.java
 
b/extensions-contrib/sqlserver-metadata-storage/src/main/java/org/apache/druid/metadata/storage/sqlserver/SQLServerConnector.java
index be7147cc5d2..ad76cf8a0b5 100644
--- 
a/extensions-contrib/sqlserver-metadata-storage/src/main/java/org/apache/druid/metadata/storage/sqlserver/SQLServerConnector.java
+++ 
b/extensions-contrib/sqlserver-metadata-storage/src/main/java/org/apache/druid/metadata/storage/sqlserver/SQLServerConnector.java
@@ -224,6 +224,13 @@ public class SQLServerConnector extends 
SQLMetadataConnector
     return String.format(Locale.ENGLISH, "FETCH NEXT %d ROWS ONLY", limit);
   }
 
+  @Override
+  protected String getDropIndexStatement(String indexName, String tableName)
+  {
+    // SQL Server requires the target table in a DROP INDEX statement.
+    return StringUtils.format("DROP INDEX %s ON %s", indexName, tableName);
+  }
+
   /**
    *
    * {@inheritDoc}
diff --git 
a/extensions-core/mysql-metadata-storage/src/main/java/org/apache/druid/metadata/storage/mysql/MySQLConnector.java
 
b/extensions-core/mysql-metadata-storage/src/main/java/org/apache/druid/metadata/storage/mysql/MySQLConnector.java
index 8c4f928be76..c9599eb0fd7 100644
--- 
a/extensions-core/mysql-metadata-storage/src/main/java/org/apache/druid/metadata/storage/mysql/MySQLConnector.java
+++ 
b/extensions-core/mysql-metadata-storage/src/main/java/org/apache/druid/metadata/storage/mysql/MySQLConnector.java
@@ -191,6 +191,13 @@ public class MySQLConnector extends SQLMetadataConnector
     return String.format(Locale.ENGLISH, "LIMIT %d", limit);
   }
 
+  @Override
+  protected String getDropIndexStatement(String indexName, String tableName)
+  {
+    // MySQL requires the target table in a DROP INDEX statement.
+    return StringUtils.format("DROP INDEX %s ON %s", indexName, tableName);
+  }
+
   @Override
   public boolean tableExists(Handle handle, String tableName)
   {
diff --git 
a/server/src/main/java/org/apache/druid/client/DataSourcesSnapshot.java 
b/server/src/main/java/org/apache/druid/client/DataSourcesSnapshot.java
index 574e1ec76ac..7a6b2666caa 100644
--- a/server/src/main/java/org/apache/druid/client/DataSourcesSnapshot.java
+++ b/server/src/main/java/org/apache/druid/client/DataSourcesSnapshot.java
@@ -35,6 +35,7 @@ import javax.annotation.Nullable;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -91,6 +92,67 @@ public class DataSourcesSnapshot
     );
   }
 
+  /**
+   * Builds a new snapshot from this one by recomputing only the datasources 
that
+   * changed, reusing this snapshot's {@link ImmutableDruidDataSource}, 
timeline,
+   * and overshadowed-segment computation for every unchanged datasource.
+   * <p>
+   * Note that a changed datasource has its entire timeline rebuilt even if 
only a
+   * single one of its segments changed; the saving is that untouched 
datasources
+   * are reused as is. Overshadowing is computed per-datasource, so reusing the
+   * prior overshadowed segments for unchanged datasources is correct. The 
result
+   * is identical to a full {@link #fromUsedSegments} rebuild of the same final
+   * state, at a cost proportional to the changed datasources rather than all 
of them.
+   *
+   * @param datasourcesToRefresh Datasource → its complete current set of used
+   *                             segments, for each datasource that changed. 
Sets
+   *                             must be non-empty; removals are conveyed via
+   *                             {@code removedDataSources}.
+   * @param removedDataSources   Datasources whose caches are now empty/gone.
+   * @param snapshotTime         Time of this snapshot (poll start time).
+   */
+  public DataSourcesSnapshot updateSnapshotForDataSources(
+      Map<String, Set<DataSegment>> datasourcesToRefresh,
+      Set<String> removedDataSources,
+      DateTime snapshotTime
+  )
+  {
+    final Map<String, String> properties = Map.of("created", 
snapshotTime.toString());
+    final Map<String, ImmutableDruidDataSource> dataSources = new 
HashMap<>(dataSourcesWithAllUsedSegments);
+    final Map<String, SegmentTimeline> timelines = new 
HashMap<>(usedSegmentsTimelinesPerDataSource);
+
+    final Set<String> changedDataSources = new HashSet<>(removedDataSources);
+    changedDataSources.addAll(datasourcesToRefresh.keySet());
+
+    removedDataSources.forEach(ds -> {
+      dataSources.remove(ds);
+      timelines.remove(ds);
+    });
+    datasourcesToRefresh.forEach((ds, segments) -> {
+      dataSources.put(ds, new ImmutableDruidDataSource(ds, properties, 
segments));
+      timelines.put(ds, SegmentTimeline.forSegments(segments));
+    });
+
+    // Reuse prior overshadowed segments for unchanged datasources; recompute 
only the changed ones.
+    final List<DataSegment> overshadowed = new ArrayList<>();
+    for (DataSegment segment : overshadowedSegments) {
+      if (!changedDataSources.contains(segment.getDataSource())) {
+        overshadowed.add(segment);
+      }
+    }
+    for (String ds : datasourcesToRefresh.keySet()) {
+      final ImmutableDruidDataSource dataSource = dataSources.get(ds);
+      final SegmentTimeline timeline = timelines.get(ds);
+      for (DataSegment segment : dataSource.getSegments()) {
+        if (timeline.isOvershadowed(segment)) {
+          overshadowed.add(segment);
+        }
+      }
+    }
+
+    return new DataSourcesSnapshot(snapshotTime, dataSources, timelines, 
overshadowed);
+  }
+
   private final DateTime snapshotTime;
   private final Map<String, ImmutableDruidDataSource> 
dataSourcesWithAllUsedSegments;
   private final Map<String, SegmentTimeline> 
usedSegmentsTimelinesPerDataSource;
@@ -110,6 +172,23 @@ public class DataSourcesSnapshot
     this.overshadowedSegments = 
ImmutableSet.copyOf(determineOvershadowedSegments());
   }
 
+  /**
+   * Constructor used by {@link #updateSnapshotForDataSources} where timelines 
and
+   * overshadowed segments have already been computed incrementally.
+   */
+  private DataSourcesSnapshot(
+      DateTime snapshotTime,
+      Map<String, ImmutableDruidDataSource> dataSourcesWithAllUsedSegments,
+      Map<String, SegmentTimeline> usedSegmentsTimelinesPerDataSource,
+      Collection<DataSegment> overshadowedSegments
+  )
+  {
+    this.snapshotTime = snapshotTime;
+    this.dataSourcesWithAllUsedSegments = dataSourcesWithAllUsedSegments;
+    this.usedSegmentsTimelinesPerDataSource = 
usedSegmentsTimelinesPerDataSource;
+    this.overshadowedSegments = ImmutableSet.copyOf(overshadowedSegments);
+  }
+
   /**
    * Time when this snapshot was taken. Since polling segments from the 
database
    * may be a slow operation, this represents the poll start time.
diff --git 
a/server/src/main/java/org/apache/druid/metadata/SQLMetadataConnector.java 
b/server/src/main/java/org/apache/druid/metadata/SQLMetadataConnector.java
index 74829cddbfb..2be87f57b01 100644
--- a/server/src/main/java/org/apache/druid/metadata/SQLMetadataConnector.java
+++ b/server/src/main/java/org/apache/druid/metadata/SQLMetadataConnector.java
@@ -389,11 +389,6 @@ public abstract class SQLMetadataConnector implements 
MetadataStorageConnector
         )
     );
 
-    createIndex(
-        tableName,
-        "IDX_%S_USED",
-        List.of("used")
-    );
     createIndex(
         tableName,
         "IDX_%S_DATASOURCE_USED_END_START",
@@ -404,6 +399,17 @@ public abstract class SQLMetadataConnector implements 
MetadataStorageConnector
             "start"
         )
     );
+    // Covering index for the used-segment ID scan performed on every metadata
+    // cache sync (SELECT id, dataSource, used_status_last_updated WHERE 
used=true).
+    // Includes id explicitly so the scan is index-only on all backends (not 
only
+    // engines like InnoDB that append the primary key to secondary indexes).
+    // Its leading 'used' column also serves plain 'WHERE used = ?' lookups, 
so a
+    // separate IDX_%S_USED index is not created.
+    createIndex(
+        tableName,
+        "IDX_%S_USED_USLU_DATASOURCE",
+        List.of("used", "used_status_last_updated", "dataSource", "id")
+    );
   }
 
   private void createUpgradeSegmentsTable(final String tableName)
@@ -663,6 +669,16 @@ public abstract class SQLMetadataConnector implements 
MetadataStorageConnector
         "IDX_%S_DATASOURCE_UPGRADED_FROM_SEGMENT_ID",
         List.of("dataSource", "upgraded_from_segment_id")
     );
+    // Migration for existing tables: covering index backing the used-segment 
ID
+    // scan on every cache sync (see createSegmentTable).
+    createIndex(
+        tableName,
+        "IDX_%S_USED_USLU_DATASOURCE",
+        List.of("used", "used_status_last_updated", "dataSource", "id")
+    );
+    // The covering index above leads with 'used', so it supersedes the 
single-column
+    // IDX_%S_USED. Drop the now-redundant index on existing tables.
+    dropIndex(tableName, "IDX_%S_USED", List.of("used"));
   }
 
   @Override
@@ -1256,6 +1272,59 @@ public abstract class SQLMetadataConnector implements 
MetadataStorageConnector
     }
   }
 
+  /**
+   * Drops an index on {@code tableName} if it exists, under either the full or
+   * short naming convention (see {@link #createIndex}). No-op if absent, so 
it is
+   * safe to call on every startup.
+   *
+   * @param tableName           Name of the table the index is on
+   * @param fullIndexNameFormat Same format string that was passed to {@link 
#createIndex}
+   * @param indexCols           Same columns that were passed to {@link 
#createIndex}
+   */
+  public void dropIndex(
+      final String tableName,
+      final String fullIndexNameFormat,
+      final List<String> indexCols
+  )
+  {
+    final Set<String> createdIndexSet = getIndexOnTable(tableName);
+    final String shortIndexName = generateShortIndexName(tableName, indexCols);
+    final String fullIndexName = 
StringUtils.toUpperCase(StringUtils.format(fullIndexNameFormat, tableName));
+
+    final String indexName;
+    if (createdIndexSet.contains(fullIndexName)) {
+      indexName = fullIndexName;
+    } else if (createdIndexSet.contains(shortIndexName)) {
+      indexName = shortIndexName;
+    } else {
+      log.info("Index[%s] on table[%s] does not exist, skipping drop.", 
fullIndexName, tableName);
+      return;
+    }
+
+    try {
+      retryWithHandle(
+          (HandleCallback<Void>) handle -> {
+            final String dropSQL = getDropIndexStatement(indexName, tableName);
+            log.info("Dropping index[%s] on table[%s] using SQL[%s].", 
indexName, tableName, dropSQL);
+            handle.execute(dropSQL);
+            return null;
+          }
+      );
+    }
+    catch (Exception e) {
+      log.warn(e, "Could not drop index[%s] on table[%s]", indexName, 
tableName);
+    }
+  }
+
+  /**
+   * SQL to drop an index. Standard SQL / Derby / PostgreSQL use {@code DROP 
INDEX <name>};
+   * MySQL requires the {@code ON <table>} clause (see {@code MySQLConnector}).
+   */
+  protected String getDropIndexStatement(String indexName, String tableName)
+  {
+    return StringUtils.format("DROP INDEX %s", indexName);
+  }
+
   /**
    * Checks table metadata to determine if the given column exists in the 
table.
    *
diff --git 
a/server/src/main/java/org/apache/druid/metadata/segment/cache/HeapMemoryDatasourceSegmentCache.java
 
b/server/src/main/java/org/apache/druid/metadata/segment/cache/HeapMemoryDatasourceSegmentCache.java
index 3a091a6c0b4..1b697d1f06f 100644
--- 
a/server/src/main/java/org/apache/druid/metadata/segment/cache/HeapMemoryDatasourceSegmentCache.java
+++ 
b/server/src/main/java/org/apache/druid/metadata/segment/cache/HeapMemoryDatasourceSegmentCache.java
@@ -38,6 +38,7 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
@@ -68,6 +69,15 @@ class HeapMemoryDatasourceSegmentCache extends 
ReadWriteCache implements AutoClo
    */
   private final AtomicInteger references = new AtomicInteger(0);
 
+  /**
+   * Set when a write-through transaction mutates this cache directly (via
+   * {@link HeapMemorySegmentMetadataCache#writeCacheForDataSource}), so that 
the
+   * incremental snapshot rebuild in the next sync refreshes this datasource 
even
+   * though the metadata store and cache already agree (the DB diff would 
report
+   * no change). Cleared while the snapshot is being rebuilt.
+   */
+  private final AtomicBoolean hasNewWrites = new AtomicBoolean(false);
+
   HeapMemoryDatasourceSegmentCache(String dataSource)
   {
     super(true);
@@ -116,6 +126,46 @@ class HeapMemoryDatasourceSegmentCache extends 
ReadWriteCache implements AutoClo
     return references.get() > 0;
   }
 
+  /**
+   * Marks that a write-through transaction has mutated this cache directly, so
+   * the next snapshot rebuild must refresh this datasource. Called under the
+   * write lock held by {@link 
HeapMemorySegmentMetadataCache#writeCacheForDataSource}.
+   */
+  void markHasNewWrites()
+  {
+    hasNewWrites.set(true);
+  }
+
+  /**
+   * If a write-through mutation occurred since the last snapshot rebuild, 
returns
+   * all used segments and clears the flag (atomically under the write lock);
+   * otherwise returns null. Used to catch datasources whose cache diverged 
from
+   * the last published snapshot without a corresponding metadata-store diff.
+   */
+  @Nullable
+  Set<DataSegment> getUsedSegmentsIfHasNewWrites()
+  {
+    return withWriteLock(() -> {
+      if (!hasNewWrites.getAndSet(false)) {
+        return null;
+      }
+      return findUsedSegmentsOverlappingAnyOf(List.of());
+    });
+  }
+
+  /**
+   * Returns all used segments and clears the new-writes flag, atomically under
+   * the write lock. Used for datasources already being refreshed from the
+   * metadata-store diff, so a concurrent write-through is not lost.
+   */
+  Set<DataSegment> getUsedSegmentsAndClearNewWrites()
+  {
+    return withWriteLock(() -> {
+      hasNewWrites.set(false);
+      return findUsedSegmentsOverlappingAnyOf(List.of());
+    });
+  }
+
   /**
    * Checks if a record in the cache needs to be updated.
    *
diff --git 
a/server/src/main/java/org/apache/druid/metadata/segment/cache/HeapMemorySegmentMetadataCache.java
 
b/server/src/main/java/org/apache/druid/metadata/segment/cache/HeapMemorySegmentMetadataCache.java
index f2accfe5541..d9d2cfce159 100644
--- 
a/server/src/main/java/org/apache/druid/metadata/segment/cache/HeapMemorySegmentMetadataCache.java
+++ 
b/server/src/main/java/org/apache/druid/metadata/segment/cache/HeapMemorySegmentMetadataCache.java
@@ -336,6 +336,10 @@ public class HeapMemorySegmentMetadataCache implements 
SegmentMetadataCache
     try (final HeapMemoryDatasourceSegmentCache datasourceCache = 
getCacheWithReference(dataSource)) {
       return datasourceCache.withWriteLock(
           () -> {
+            // Flag the write-through before performing it, so that even a 
write
+            // which throws after partially mutating the cache still causes 
the next sync
+            // to refresh this datasource's snapshot.
+            datasourceCache.markHasNewWrites();
             try {
               return writeAction.perform(datasourceCache);
             }
@@ -570,12 +574,16 @@ public class HeapMemorySegmentMetadataCache implements 
SegmentMetadataCache
 
     final Map<String, DatasourceSegmentSummary> datasourceToSummary = new 
HashMap<>();
 
+    // Datasources whose used-segment set changed this sync. Null forces a full
+    // snapshot rebuild (the first sync, which has no previous snapshot to 
reuse).
+    Set<String> datasourcesToRefresh = null;
+
     // Fetch all used segments if this is the first sync
     if (syncFinishTime.get() == null) {
       retrieveAllUsedSegments(datasourceToSummary);
     } else {
       retrieveUsedSegmentIds(datasourceToSummary);
-      updateSegmentIdsInCache(datasourceToSummary, 
syncStartTime.minus(SYNC_BUFFER_DURATION));
+      datasourcesToRefresh = updateSegmentIdsInCache(datasourceToSummary, 
syncStartTime.minus(SYNC_BUFFER_DURATION));
       retrieveUsedSegmentPayloads(datasourceToSummary);
     }
 
@@ -591,21 +599,41 @@ public class HeapMemorySegmentMetadataCache implements 
SegmentMetadataCache
       retrieveAndResetUsedIndexingStates();
     }
 
-    markCacheSynced(syncStartTime);
+    markCacheSynced(syncStartTime, datasourcesToRefresh);
 
     syncFinishTime.set(DateTimes.nowUtc());
     return totalSyncDuration.millisElapsed();
   }
 
   /**
-   * Marks the cache for all datasources as synced and emit total stats.
+   * Marks the cache for all datasources as synced and emits stats, then 
rebuilds
+   * the {@link DataSourcesSnapshot}.
+   * <p>
+   * When {@code datasourcesToRefresh} is non-null and a previous snapshot 
exists,
+   * only the changed datasources are rebuilt into the snapshot (reusing the 
prior
+   * snapshot's per-datasource timelines/overshadow for the rest); the result 
is
+   * identical to a full rebuild. A datasource is rebuilt if the metadata-store
+   * diff changed it ({@code datasourcesToRefresh}) or a write-through 
transaction
+   * mutated its cache since the last sync ({@code hasNewWrites}). Otherwise 
the
+   * whole snapshot is rebuilt (e.g. first sync, when there is no previous 
snapshot).
+   *
+   * @param datasourcesToRefresh Datasources changed by the metadata-store 
diff this
+   *                             sync, or null to force a full rebuild.
    */
-  private void markCacheSynced(DateTime syncStartTime)
+  private void markCacheSynced(DateTime syncStartTime, @Nullable Set<String> 
datasourcesToRefresh)
   {
     final Stopwatch updateDuration = Stopwatch.createStarted();
 
+    final DataSourcesSnapshot previousSnapshot = datasourcesSnapshot.get();
+    // updateSegmentIdsInCache never returns null, so datasourcesToRefresh is 
null
+    // only on the first sync which also has no previous snapshot to build on.
+    final boolean incremental = previousSnapshot != null;
+
     final Set<String> cachedDatasources = 
Set.copyOf(datasourceToSegmentCache.keySet());
+    // In incremental mode this holds only the datasources being rebuilt; in 
full
+    // mode it holds every non-empty datasource.
     final Map<String, Set<DataSegment>> datasourceToUsedSegments = new 
HashMap<>();
+    final Set<String> removedDatasources = new HashSet<>();
 
     for (String dataSource : cachedDatasources) {
       final HeapMemoryDatasourceSegmentCache cache = 
datasourceToSegmentCache.getOrDefault(
@@ -627,19 +655,43 @@ public class HeapMemorySegmentMetadataCache implements 
SegmentMetadataCache
               }
             }
         );
+        removedDatasources.add(dataSource);
       } else {
         emitMetric(dataSource, Metric.CACHED_INTERVALS, 
stats.getNumIntervals());
         emitMetric(dataSource, Metric.CACHED_USED_SEGMENTS, 
stats.getNumUsedSegments());
         emitMetric(dataSource, Metric.CACHED_UNUSED_SEGMENTS, 
stats.getNumUnusedSegments());
         emitMetric(dataSource, Metric.CACHED_PENDING_SEGMENTS, 
stats.getNumPendingSegments());
 
-        datasourceToUsedSegments.put(dataSource, 
cache.findUsedSegmentsOverlappingAnyOf(List.of()));
+        if (!incremental) {
+          datasourceToUsedSegments.put(dataSource, 
cache.findUsedSegmentsOverlappingAnyOf(List.of()));
+        } else if (datasourcesToRefresh.contains(dataSource)) {
+          // Changed by the metadata-store diff; materialize and clear the
+          // write-through flag atomically so a concurrent write is not lost.
+          datasourceToUsedSegments.put(dataSource, 
cache.getUsedSegmentsAndClearNewWrites());
+        } else {
+          // Not changed in the store, but a write-through transaction may have
+          // mutated the cache directly since the last snapshot; catch that 
here.
+          final Set<DataSegment> segmentsFromWrites = 
cache.getUsedSegmentsIfHasNewWrites();
+          if (segmentsFromWrites != null) {
+            datasourceToUsedSegments.put(dataSource, segmentsFromWrites);
+          }
+        }
       }
     }
 
-    datasourcesSnapshot.set(
-        DataSourcesSnapshot.fromUsedSegments(datasourceToUsedSegments, 
syncStartTime)
-    );
+    if (incremental) {
+      datasourcesSnapshot.set(
+          previousSnapshot.updateSnapshotForDataSources(
+              datasourceToUsedSegments,
+              removedDatasources,
+              syncStartTime
+          )
+      );
+    } else {
+      datasourcesSnapshot.set(
+          DataSourcesSnapshot.fromUsedSegments(datasourceToUsedSegments, 
syncStartTime)
+      );
+    }
     emitMetric(Metric.UPDATE_SNAPSHOT_DURATION_MILLIS, 
updateDuration.millisElapsed());
   }
 
@@ -742,14 +794,21 @@ public class HeapMemorySegmentMetadataCache implements 
SegmentMetadataCache
    * metadata store in {@link #retrieveUsedSegmentIds}. The update done on each
    * datasource cache is atomic. Also identifies the segment IDs which have 
been
    * updated in the metadata store and need to be refreshed in the cache.
+   *
+   * @return Set of datasource names whose used segment set has changed in this
+   * sync and need to be refreshed in the snapshot.
    */
-  private void updateSegmentIdsInCache(
+  private Set<String> updateSegmentIdsInCache(
       Map<String, DatasourceSegmentSummary> datasourceToSummary,
       DateTime syncStartTime
   )
   {
     final Stopwatch updateDuration = Stopwatch.createStarted();
 
+    // Datasources whose used-segment set actually changed this sync (used to
+    // rebuild only the affected part of the snapshot).
+    final Set<String> datasourcesToRefresh = new HashSet<>();
+
     // Sync segments for datasources which were retrieved in the latest poll
     datasourceToSummary.forEach((dataSource, summary) -> {
       final HeapMemoryDatasourceSegmentCache cache = 
getCacheForDatasource(dataSource);
@@ -759,6 +818,9 @@ public class HeapMemorySegmentMetadataCache implements 
SegmentMetadataCache
       emitNonZeroMetric(dataSource, Metric.DELETED_SEGMENTS, 
result.getDeleted());
 
       summary.usedSegmentIdsToRefresh.addAll(result.getExpiredIds());
+      if (result.getDeleted() > 0 || !result.getExpiredIds().isEmpty()) {
+        datasourcesToRefresh.add(dataSource);
+      }
     });
 
     // Update cache for datasources which returned no segments in the latest 
poll
@@ -766,10 +828,14 @@ public class HeapMemorySegmentMetadataCache implements 
SegmentMetadataCache
       if (!datasourceToSummary.containsKey(dataSource)) {
         final SegmentSyncResult result = cache.syncSegmentIds(List.of(), 
syncStartTime);
         emitNonZeroMetric(dataSource, Metric.DELETED_SEGMENTS, 
result.getDeleted());
+        if (result.getDeleted() > 0) {
+          datasourcesToRefresh.add(dataSource);
+        }
       }
     });
 
     emitMetric(Metric.UPDATE_IDS_DURATION_MILLIS, 
updateDuration.millisElapsed());
+    return datasourcesToRefresh;
   }
 
   /**
diff --git 
a/server/src/test/java/org/apache/druid/client/DataSourcesSnapshotTest.java 
b/server/src/test/java/org/apache/druid/client/DataSourcesSnapshotTest.java
new file mode 100644
index 00000000000..7df7148a0d9
--- /dev/null
+++ b/server/src/test/java/org/apache/druid/client/DataSourcesSnapshotTest.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.client;
+
+import org.apache.druid.java.util.common.DateTimes;
+import org.apache.druid.java.util.common.granularity.Granularities;
+import org.apache.druid.server.coordinator.CreateDataSegments;
+import org.apache.druid.timeline.DataSegment;
+import org.joda.time.DateTime;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class DataSourcesSnapshotTest
+{
+  private static final String DS1 = "ds1";
+  private static final String DS2 = "ds2";
+  private static final DateTime SNAPSHOT_TIME = DateTimes.of("2024-01-01");
+
+  private static Set<DataSegment> segmentsOf(String dataSource, String start, 
int numIntervals)
+  {
+    return CreateDataSegments.ofDatasource(dataSource)
+                             .forIntervals(numIntervals, Granularities.DAY)
+                             .startingAt(start)
+                             .withNumPartitions(1)
+                             .eachOfSizeInMb(500)
+                             .stream()
+                             .collect(Collectors.toSet());
+  }
+
+  private static Set<String> idsOf(DataSourcesSnapshot snapshot, String 
dataSource)
+  {
+    return snapshot.getDataSource(dataSource).getSegments().stream()
+                   .map(s -> s.getId().toString())
+                   .collect(Collectors.toSet());
+  }
+
+  private static DataSourcesSnapshot baseSnapshot()
+  {
+    final Map<String, Set<DataSegment>> base = new HashMap<>();
+    base.put(DS1, segmentsOf(DS1, "2024-01-01", 2));
+    base.put(DS2, segmentsOf(DS2, "2024-01-01", 2));
+    return DataSourcesSnapshot.fromUsedSegments(base, SNAPSHOT_TIME);
+  }
+
+  @Test
+  public void testUpdateSnapshot_reusesUnchangedDatasourceByReference()
+  {
+    final DataSourcesSnapshot previous = baseSnapshot();
+
+    final Set<DataSegment> newDs1 = segmentsOf(DS1, "2024-02-01", 3);
+    final DataSourcesSnapshot updated = previous.updateSnapshotForDataSources(
+        Map.of(DS1, newDs1),
+        Set.of(),
+        SNAPSHOT_TIME
+    );
+
+    Assert.assertSame(previous.getDataSource(DS2), updated.getDataSource(DS2));
+    Assert.assertNotSame(previous.getDataSource(DS1), 
updated.getDataSource(DS1));
+    Assert.assertEquals(
+        newDs1.stream().map(s -> 
s.getId().toString()).collect(Collectors.toSet()),
+        idsOf(updated, DS1)
+    );
+  }
+
+  @Test
+  public void testUpdateSnapshot_equalsFullRebuild()
+  {
+    final DataSourcesSnapshot previous = baseSnapshot();
+    final Set<DataSegment> baseDs2 = previous.getDataSource(DS2).getSegments()
+                                             
.stream().collect(Collectors.toSet());
+
+    final Set<DataSegment> newDs1 = segmentsOf(DS1, "2024-02-01", 3);
+    final DataSourcesSnapshot incremental = 
previous.updateSnapshotForDataSources(
+        Map.of(DS1, newDs1),
+        Set.of(),
+        SNAPSHOT_TIME
+    );
+
+    final Map<String, Set<DataSegment>> finalState = new HashMap<>();
+    finalState.put(DS1, newDs1);
+    finalState.put(DS2, baseDs2);
+    final DataSourcesSnapshot fullRebuild = 
DataSourcesSnapshot.fromUsedSegments(finalState, SNAPSHOT_TIME);
+
+    Assert.assertEquals(idsOf(fullRebuild, DS1), idsOf(incremental, DS1));
+    Assert.assertEquals(idsOf(fullRebuild, DS2), idsOf(incremental, DS2));
+    Assert.assertEquals(fullRebuild.getOvershadowedSegments(), 
incremental.getOvershadowedSegments());
+  }
+
+  @Test
+  public void testUpdateSnapshot_removesDatasource()
+  {
+    final DataSourcesSnapshot previous = baseSnapshot();
+
+    final DataSourcesSnapshot updated = previous.updateSnapshotForDataSources(
+        Map.of(),
+        Set.of(DS2),
+        SNAPSHOT_TIME
+    );
+
+    Assert.assertNull(updated.getDataSource(DS2));
+    Assert.assertNotNull(updated.getDataSource(DS1));
+  }
+}
diff --git 
a/server/src/test/java/org/apache/druid/metadata/SQLMetadataConnectorTest.java 
b/server/src/test/java/org/apache/druid/metadata/SQLMetadataConnectorTest.java
index 3fef28a963a..51bd3dd8390 100644
--- 
a/server/src/test/java/org/apache/druid/metadata/SQLMetadataConnectorTest.java
+++ 
b/server/src/test/java/org/apache/druid/metadata/SQLMetadataConnectorTest.java
@@ -337,8 +337,11 @@ public class SQLMetadataConnectorTest
     connector.createSegmentTable(segmentsTable);
     connector.alterSegmentTable();
     connector.getDBI().withHandle(handle -> {
-      handle.execute("DROP INDEX 
IDX_8FE3D20EC8C9CA932EA3FF6AC497D1A9E75ADDA0");
-      handle.execute("CREATE INDEX IDX_DRUIDTEST_SEGMENTS_USED ON 
druidTest_segments(used)");
+      handle.execute("DROP INDEX 
IDX_93A18EE829B37C5F38FC6DAFB070261D88503835");
+      handle.execute(
+          "CREATE INDEX IDX_DRUIDTEST_SEGMENTS_USED_USLU_DATASOURCE"
+          + " ON 
druidTest_segments(used,used_status_last_updated,dataSource,id)"
+      );
       return null;
     });
 
@@ -346,7 +349,7 @@ public class SQLMetadataConnectorTest
     connector.alterSegmentTable();
 
     final Set<String> expectedIndices = Sets.newHashSet(
-        "IDX_DRUIDTEST_SEGMENTS_USED",
+        "IDX_DRUIDTEST_SEGMENTS_USED_USLU_DATASOURCE",
         "IDX_D011BD6ED76268701273CE512704C5AFA060D672",
         "IDX_6381EF2DB4824C35C0E72EF9E166626ADB2B21A3"
     );
@@ -372,8 +375,11 @@ public class SQLMetadataConnectorTest
     connector.createSegmentTable(segmentsTable);
     connector.alterSegmentTable();
     connector.getDBI().withHandle(handle -> {
-      handle.execute("DROP INDEX IDX_DRUIDTEST_SEGMENTS_USED");
-      handle.execute("CREATE INDEX 
IDX_8FE3D20EC8C9CA932EA3FF6AC497D1A9E75ADDA0 ON druidTest_segments(used)");
+      handle.execute("DROP INDEX IDX_DRUIDTEST_SEGMENTS_USED_USLU_DATASOURCE");
+      handle.execute(
+          "CREATE INDEX IDX_93A18EE829B37C5F38FC6DAFB070261D88503835"
+          + " ON 
druidTest_segments(used,used_status_last_updated,dataSource,id)"
+      );
       return null;
     });
 
@@ -381,7 +387,7 @@ public class SQLMetadataConnectorTest
     connector.alterSegmentTable();
 
     final Set<String> expectedIndices = Sets.newHashSet(
-        "IDX_8FE3D20EC8C9CA932EA3FF6AC497D1A9E75ADDA0",
+        "IDX_93A18EE829B37C5F38FC6DAFB070261D88503835",
         "IDX_DRUIDTEST_SEGMENTS_DATASOURCE_USED_END_START",
         "IDX_DRUIDTEST_SEGMENTS_DATASOURCE_UPGRADED_FROM_SEGMENT_ID"
     );
@@ -405,7 +411,7 @@ public class SQLMetadataConnectorTest
     final String segmentsTable = tablesConfig.getSegmentsTable();
 
     final Set<String> expectedIndices = Sets.newHashSet(
-        "IDX_8FE3D20EC8C9CA932EA3FF6AC497D1A9E75ADDA0",
+        "IDX_93A18EE829B37C5F38FC6DAFB070261D88503835",
         "IDX_D011BD6ED76268701273CE512704C5AFA060D672",
         "IDX_6381EF2DB4824C35C0E72EF9E166626ADB2B21A3"
     );
@@ -431,9 +437,9 @@ public class SQLMetadataConnectorTest
     final String segmentsTable = tablesConfig.getSegmentsTable();
 
     final Set<String> expectedIndices = Sets.newHashSet(
-        "IDX_DRUIDTEST_SEGMENTS_USED",
         "IDX_DRUIDTEST_SEGMENTS_DATASOURCE_USED_END_START",
-        "IDX_DRUIDTEST_SEGMENTS_DATASOURCE_UPGRADED_FROM_SEGMENT_ID"
+        "IDX_DRUIDTEST_SEGMENTS_DATASOURCE_UPGRADED_FROM_SEGMENT_ID",
+        "IDX_DRUIDTEST_SEGMENTS_USED_USLU_DATASOURCE"
     );
 
     connector.createSegmentTable(segmentsTable);
diff --git 
a/server/src/test/java/org/apache/druid/metadata/segment/cache/HeapMemorySegmentMetadataCacheTest.java
 
b/server/src/test/java/org/apache/druid/metadata/segment/cache/HeapMemorySegmentMetadataCacheTest.java
index e739744480e..95a8a344bc5 100644
--- 
a/server/src/test/java/org/apache/druid/metadata/segment/cache/HeapMemorySegmentMetadataCacheTest.java
+++ 
b/server/src/test/java/org/apache/druid/metadata/segment/cache/HeapMemorySegmentMetadataCacheTest.java
@@ -851,6 +851,36 @@ public class HeapMemorySegmentMetadataCacheTest
     );
   }
 
+  @Test
+  public void testSync_reflectsWriteThroughInsertInSnapshot()
+  {
+    setupAndSyncCache();
+
+    final DataSegmentPlus segment =
+        
CreateDataSegments.ofDatasource(TestDataSource.WIKI).updatedNow().markUsed().asPlus();
+
+    // Simulate a write-through transaction: the segment is written to BOTH the
+    // metadata store and the datasource cache, so the next sync's store diff 
finds
+    // nothing to refresh for this datasource (DB and cache already agree).
+    insertSegmentsInMetadataStore(Set.of(segment));
+    cache.writeCacheForDataSource(
+        TestDataSource.WIKI,
+        wikiCache -> wikiCache.insertSegments(Set.of(segment))
+    );
+
+    syncCache();
+
+    // The published snapshot must still include the write-through segment, 
even
+    // though the incremental rebuild saw no metadata-store change for WIKI.
+    
Assert.assertNotNull(cache.getDataSourcesSnapshot().getDataSource(TestDataSource.WIKI));
+    Assert.assertTrue(
+        cache.getDataSourcesSnapshot()
+             .getDataSource(TestDataSource.WIKI)
+             .getSegments()
+             .contains(segment.getDataSegment())
+    );
+  }
+
   private void insertSegmentsInMetadataStore(Set<DataSegmentPlus> segments)
   {
     IndexerSqlMetadataStorageCoordinatorTestBase


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to