github-actions[bot] commented on code in PR #68090:
URL: https://github.com/apache/doris/pull/68090#discussion_r4026755619


##########
be/src/cloud/cloud_warm_up_manager.cpp:
##########
@@ -297,7 +297,7 @@ void CloudWarmUpManager::handle_jobs() {
                     continue;
                 }
 
-                int64_t expiration_time = tablet_meta->ttl_seconds();
+                int64_t expiration_time = 
tablet_meta->file_cache_ttl_expiration_time();

Review Comment:
   [P1] Preserve the tablet id on the synchronous warm-up path. These warm-ups 
are forced through `CachedRemoteFileReader::_read_from_indirect_cache()`, which 
constructs `CacheContext(io_ctx)` but never copies `_tablet_id` (unlike the 
async and remote-only paths). The resulting blocks are stored with tablet_id 0, 
so `BlockFileCacheTtlMgr` cannot find or register them and a block warmed 
before the deadline can remain TTL indefinitely. Set the tablet id before 
`get_or_set()` and cover a warm-up that starts live and is observed after 
expiry.



##########
be/src/storage/tablet/tablet_reader.cpp:
##########
@@ -191,7 +191,7 @@ Status TabletReader::_capture_rs_readers(const 
ReaderParams& read_params) {
     _reader_context.output_columns = &read_params.output_columns;
     _reader_context.extra_columns = read_params.extra_columns;
     _reader_context.push_down_agg_type_opt = 
read_params.push_down_agg_type_opt;
-    _reader_context.ttl_seconds = _tablet->ttl_seconds();
+    _reader_context.file_cache_expiration_time = 
_tablet->file_cache_ttl_expiration_time();

Review Comment:
   [P2] Update the parallel score-runtime read context too. 
`OlapScanner::_prepare_impl()` still calls 
`build_score_runtime_collection_io_context(..., tablet->ttl_seconds(), ...)`; 
the helper copies that argument directly to `IOContext::expiration_time`, and 
`CollectionStatistics` carries it through `IndexFileReader` into cached remote 
reads. Scoring cache misses therefore still persist `3600` instead of 
`creation_time + 3600` and can recreate TTL blocks for an expired tablet. Pass 
the absolute tablet deadline here as well and update the olap-scanner tests.



##########
be/src/cloud/cloud_internal_service.cpp:
##########
@@ -1270,7 +1270,7 @@ void 
CloudInternalServiceImpl::warm_up_rowset(google::protobuf::RpcController* c
                       << " us, tablet_id: " << rs_meta.tablet_id()
                       << ", rowset_id: " << rowset_id.to_string();
         }
-        int64_t expiration_time = tablet_meta->ttl_seconds();
+        int64_t expiration_time = 
tablet_meta->file_cache_ttl_expiration_time();

Review Comment:
   [P2] Carry the absolute deadline through peer-server pull-through fills too. 
The detached peer race calls `fetch_blocks()` without its IO context, and the 
request carries tablet/resource identity but no expiration. On the fill server, 
`handle_peer_file_cache_block_request()` therefore performs the first 
`get_or_set()` with a default context, creating a NORMAL block owned by tablet 
0 before the downloader runs; the later synchronous read only finds that 
existing cell and cannot replace its metadata. Active-TTL tablets filled 
through the designated fill compute group consequently bypass TTL protection. 
Propagate both fields into the first admission and capability-gate or fail 
closed when an old server cannot honor TTL-aware fill; then test the stored 
metadata and field-absent fallback, not only returned bytes.



##########
be/src/io/fs/file_writer.h:
##########
@@ -47,7 +47,7 @@ struct FileWriterOptions {
     bool allow_adaptive_file_cache_write = true;
     bool is_cold_data = false;
     bool sync_file_data = true;              // Whether flush data into 
storage system
-    uint64_t file_cache_expiration_time = 0; // Relative time
+    uint64_t file_cache_expiration_time = 0; // Absolute time, 0 means no TTL

Review Comment:
   [P1] Migrate and durably normalize existing expiration metadata. This field 
already exists on disk and across BE warm-up RPCs, but older blocks contain 
durations such as `3600`. Startup restores them before `BlockFileCacheTtlMgr` 
exists, `add_cell()` turns every nonzero value into TTL, and the restored 
tablet ids are never registered. Even blocks demoted by the manager keep their 
nonzero expiration, so a subsequent DB-driven restore can put them back in TTL. 
Please migrate/validate restored and peer-provided values against the owning 
tablet, register restored tablet ids, and rewrite expiration together with 
queue type so convergence survives a restart.



##########
be/src/storage/tablet/tablet_meta.cpp:
##########
@@ -716,6 +716,25 @@ Status TabletMeta::save_meta(DataDir* data_dir) {
     return _save_meta(data_dir);
 }
 
+int64_t TabletMeta::file_cache_ttl_expiration_time() const {
+    int64_t ttl = ttl_seconds();
+    int64_t ctime = creation_time();
+    if (ttl <= 0 || ctime <= 0) {
+        return 0;
+    }
+    // FE caps file_cache_ttl_seconds at Long.MAX_VALUE / 2, so this cannot 
wrap, but a tablet
+    // meta that reached us from anywhere else still must not turn a huge ttl 
into a past
+    // deadline that silently downgrades the tablet to normal cache.
+    if (ctime > std::numeric_limits<int64_t>::max() - ttl) {
+        return std::numeric_limits<int64_t>::max();
+    }
+    int64_t expiration_time = ctime + ttl;
+    // Already past the deadline: report no TTL at all, so callers stamp the 
blocks they
+    // create as NORMAL right away instead of putting them in the TTL queue for
+    // BlockFileCacheTtlMgr to take straight back out again.
+    return expiration_time > UnixSeconds() ? expiration_time : 0;

Review Comment:
   [P1] Re-evaluate the deadline at final cache admission. This helper is 
sampled once and every caller stores the result, but `CacheContext`, 
`FileCacheAllocatorBuilder`, and packed-file admission treat any nonzero value 
as TTL without comparing it to the current time. A load or queued warm-up 
started just before expiry therefore keeps creating TTL blocks after the 
deadline, recreating the sweep churn; a context that captured zero can likewise 
keep creating NORMAL blocks after an ALTER extension, after the manager's 
edge-triggered promotion has already finished. Normalize/reload the current 
tablet deadline when each block is admitted and add deterministic 
cross-expiry/ALTER tests.



##########
regression-test/suites/cloud_p0/cache/ttl/test_ttl_expired_tablet.groovy:
##########
@@ -0,0 +1,193 @@
+// 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.
+
+import org.codehaus.groovy.runtime.IOGroovyMethods
+
+// The file cache TTL deadline is the tablet creation time plus 
file_cache_ttl_seconds.
+// Once a tablet is past that deadline, the load, query and warm up paths must 
all stamp the
+// blocks they create as NORMAL right away. Before the deadline was defined in 
one place,
+// those paths passed the raw ttl_seconds instead, so every block went into 
the TTL queue and
+// the background sweep pulled it straight back out, over and over, for the 
rest of the
+// tablet's life. This test pins that down: past the deadline, nothing reaches 
the TTL queue.
+suite("test_ttl_expired_tablet") {
+    def custoBeConfig = [
+        enable_evict_file_cache_in_advance : false,
+        file_cache_enter_disk_resource_limit_mode_percent : 99,
+        file_cache_background_ttl_gc_interval_ms : 1000,
+        file_cache_background_ttl_info_update_interval_ms : 1000,
+        file_cache_background_tablet_id_flush_interval_ms : 1000
+    ]
+
+    setBeConfigTemporary(custoBeConfig) {
+    sql "set global enable_auto_analyze = false"
+    sql "set global enable_audit_plugin = false"
+    def clusters = sql " SHOW CLUSTERS; "
+    assertTrue(!clusters.isEmpty())
+    def validCluster = clusters[0][0]
+    sql """use @${validCluster};""";
+
+    def ttlSeconds = 30
+    def ttlProperties = """ 
PROPERTIES("file_cache_ttl_seconds"="${ttlSeconds}") """
+    String[][] backends = sql """ show backends """
+    String backendId;
+    def backendIdToBackendIP = [:]
+    def backendIdToBackendHttpPort = [:]
+    def backendIdToBackendBrpcPort = [:]
+    for (String[] backend in backends) {
+        if (backend[9].equals("true") && 
backend[19].contains("regression_cluster_name1")) {
+            backendIdToBackendIP.put(backend[0], backend[1])
+            backendIdToBackendHttpPort.put(backend[0], backend[4])
+            backendIdToBackendBrpcPort.put(backend[0], backend[5])
+        }
+    }
+    assertEquals(backendIdToBackendIP.size(), 1)
+
+    backendId = backendIdToBackendIP.keySet()[0]
+    def url = backendIdToBackendIP.get(backendId) + ":" + 
backendIdToBackendHttpPort.get(backendId) + 
"""/api/file_cache?op=clear&sync=true"""
+    def clearFileCache = { check_func ->
+        httpTest {
+            endpoint ""
+            uri url
+            op "get"
+            body ""
+            check check_func
+        }
+    }
+
+    def getMetricsMethod = { check_func ->
+        httpTest {
+            endpoint backendIdToBackendIP.get(backendId) + ":" + 
backendIdToBackendBrpcPort.get(backendId)
+            uri "/brpc_metrics"
+            op "get"
+            check check_func
+        }
+    }
+
+    def getTtlCacheSize = {
+        long ttlCacheSize = -1
+        getMetricsMethod.call() {
+            respCode, body ->
+                assertEquals("${respCode}".toString(), "200")
+                String out = "${body}".toString()
+                for (String line in out.split('\n')) {
+                    if (line.startsWith("#")) {
+                        continue
+                    }
+                    if (line.contains("ttl_cache_size")) {
+                        def i = line.indexOf(' ')
+                        ttlCacheSize = line.substring(i).toLong()
+                        break
+                    }
+                }
+        }
+        assertTrue(ttlCacheSize >= 0, "ttl_cache_size metric not found")
+        return ttlCacheSize
+    }
+
+    def getTabletIds = { String tableName ->
+        def tablets = sql "show tablets from ${tableName}"
+        assertTrue(tablets.size() > 0, "No tablets found for table 
${tableName}")
+        tablets.collect { it[0] as Long }
+    }
+
+    def waitForFileCacheType = { List<Long> tabletIds, String expectedType, 
long timeoutMs = 60000L, long intervalMs = 1000L ->
+        long start = System.currentTimeMillis()
+        while (System.currentTimeMillis() - start < timeoutMs) {
+            boolean allMatch = true
+            for (Long tabletId in tabletIds) {
+                def rows = sql "select type from 
information_schema.file_cache_info where tablet_id = ${tabletId}"
+                if (rows.isEmpty()) {
+                    allMatch = false
+                    break
+                }
+                def mismatch = rows.find { row -> 
!row[0]?.toString()?.equalsIgnoreCase(expectedType) }
+                if (mismatch) {
+                    logger.info("tablet ${tabletId} has cache types 
${rows.collect { it[0] }} while waiting for ${expectedType}")
+                    allMatch = false
+                    break
+                }
+            }
+            if (allMatch) {
+                return
+            }
+            sleep(intervalMs)
+        }
+        assertTrue(false, "Timeout waiting for file_cache_info type 
${expectedType} for tablets ${tabletIds}")
+    }
+
+    def loadCustomerRows = { String table ->
+        def totalRows = 200
+        def batchSize = 100
+        def commentSuffix = ' ' + ('X' * 50)
+        for (int offset = 0; offset < totalRows; offset += batchSize) {
+            def sb = new StringBuilder()
+            int batchEnd = Math.min(totalRows, offset + batchSize)
+            for (int idx = offset; idx < batchEnd; idx++) {
+                def customerId = 10001 + idx
+                def customerName = String.format('Customer#%09d', customerId)
+                sb.append("""INSERT INTO ${table} VALUES (
+                    ${customerId},
+                    '${customerName}',
+                    'Address Line 1',
+                    15,
+                    '123-456-7890',
+                    12345.67,
+                    'AUTOMOBILE',
+                    'This is a test comment for the customer.${commentSuffix}'
+                    );
+                    """)
+            }
+            sql sb.toString()
+        }
+    }
+
+    sql new 
File("""${context.file.parent}/../ddl/customer_ttl_delete.sql""").text
+    clearFileCache.call() {
+        respCode, body -> {}
+    }
+    sleep(10000)
+    assertEquals(0L, getTtlCacheSize.call())
+
+    // Create the table, then let its TTL deadline pass before writing a 
single row.
+    sql (new File("""${context.file.parent}/../ddl/customer_ttl.sql""").text + 
ttlProperties)
+    sql """ alter table customer_ttl set ("disable_auto_compaction" = "true") 
"""
+    sleep((ttlSeconds + 15) * 1000L)
+
+    loadCustomerRows("customer_ttl")
+    def tabletIds = getTabletIds.call("customer_ttl")
+
+    // The tablet is past its deadline, so the load path must have written 
every block into

Review Comment:
   [P2] Make direct cache admission observable here. With the old code the 
expired tablet first admits these blocks as TTL, but the one-second background 
reconciler can demote them during the 60-second wait, so the load assertions 
still pass after the production fix is reverted. The subsequent SELECT is also 
a cache hit and never exercises the changed reader context. Please prevent or 
pause reconciliation while checking the load insertion, then clear and verify 
the cache is empty before SELECT and assert that the read recreates NORMAL 
blocks; add an actual warm-up case for the warm-up changes.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to