diff --git a/docs/09-store-handler.md b/docs/09-store-handler.md index b20c5ef4..d4b2253b 100644 --- a/docs/09-store-handler.md +++ b/docs/09-store-handler.md @@ -124,6 +124,7 @@ For `ELOQDSS_ROCKSDB_CLOUD_S3`, a `FileCacheSyncWorker` periodically sends the p ## 6. TTL and Purge Mechanisms - **Record TTL (data plane).** `BatchWriteRecords` items carry a `ttl` (ms epoch). On read, `FetchRecordCallback` treats an expired EloqKV record as `RecordStatus::Deleted` even if the store still has it. Physical reclamation is compaction-driven: `TTLCompactionFilter` (both `rocksdb_handler.h` and DSS `rocksdb_data_store_common.h`) drops expired entries during RocksDB compaction; the DSS variant flags TTL presence in the version-ts MSB (`MSB`/`MSB_MASK`). +- **Diagnostic TTL bypass (EloqDSS RocksDB variants).** `--ignore_redis_ttl=true` (or `[store] ignore_redis_ttl=true`) bypasses the EloqKV point-read, bucket, and scanner expiry checks and makes `TTLCompactionFilter` retain expired records. EloqKV separately makes its TTL object variants report no TTL while the mode is active. Neither the version-ts MSB, the outer expiration timestamp, nor the Redis object's embedded TTL is rewritten, so restarting every EloqKV and DSS process with the option disabled restores normal expiration from the stored absolute timestamp. All participating processes must use the same setting; the option is intended for isolated diagnostic stores, and it does not make ordinary writes read-only. - **Scan-iterator TTL (`TTLWrapperCache`, `data_store_service.h/.cpp`).** Open scan sessions cache their RocksDB iterator (`RocksDBIteratorTTLWrapper`) keyed by session id. A per-shard `dss_ttl` worker wakes every 3 s and erases not-in-use wrappers idle longer than the interval; `Borrow`/`Return` mark in-use; shard close force-erases. - **Cloud SST purger (`purger_event_listener.h`, `purger_sliding_window.h`).** rocksdb-cloud's background purger deletes obsolete S3 files. `PurgerEventListener` tracks live file numbers across flush/compaction; a time-based `SlidingWindow` publishes the smallest in-use file number to S3 (`S3FileNumberUpdater`) so the purger never deletes a file a lagging reader (standby syncing the file cache) may still need, and can temporarily block the purger. diff --git a/store_handler/data_store_service_client_closure.cpp b/store_handler/data_store_service_client_closure.cpp index 738f1798..574e9e39 100644 --- a/store_handler/data_store_service_client_closure.cpp +++ b/store_handler/data_store_service_client_closure.cpp @@ -28,6 +28,7 @@ #include #include "cc_req_misc.h" +#include "eloq_data_store_service/ignore_redis_ttl.h" #include "error_messages.h" #include "store_util.h" // host_to_big_endian #include "tx_service/include/cc/cc_request.h" @@ -172,7 +173,7 @@ void FetchRecordCallback(void *data, { // Hash partition const uint64_t rec_ttl = read_closure->Ttl(); - if (rec_ttl > 0 && + if (!IgnoreRedisTTL() && rec_ttl > 0 && rec_ttl < txservice::LocalCcShards::ClockTsInMillseconds()) { // expired record @@ -277,7 +278,7 @@ void FetchBucketDataCallback(void *data, { scan_next_closure->GetItem(item_idx, key_str, value_str, ts, ttl); last_scanned_key = key_str; - if (ttl > 0 && ttl < now) + if (!IgnoreRedisTTL() && ttl > 0 && ttl < now) { // fetch_bucket_data_cc->AddDataItem(std::move(tx_key), "", 1, // true); diff --git a/store_handler/data_store_service_scanner.cpp b/store_handler/data_store_service_scanner.cpp index 8322981e..f8e05fb0 100644 --- a/store_handler/data_store_service_scanner.cpp +++ b/store_handler/data_store_service_scanner.cpp @@ -30,6 +30,7 @@ #include #include "data_store_service_client_closure.h" +#include "eloq_data_store_service/ignore_redis_ttl.h" #include "eloq_data_store_service/object_pool.h" #include "tx_service/include/tx_key.h" @@ -154,7 +155,7 @@ void SinglePartitionScanner::ProcessScanNextResult( sp_scanner->last_key_ = key; } - if (ttl > 0 && ttl < now) + if (!IgnoreRedisTTL() && ttl > 0 && ttl < now) { // TTL expired record DLOG(INFO) << "TTL expired record, key: " << key << ", ttl: " << ttl diff --git a/store_handler/eloq_data_store_service/ignore_redis_ttl.h b/store_handler/eloq_data_store_service/ignore_redis_ttl.h new file mode 100644 index 00000000..a1e9ab5e --- /dev/null +++ b/store_handler/eloq_data_store_service/ignore_redis_ttl.h @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2026 EloqData Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under either of the following two licenses: + * 1. GNU Affero General Public License, version 3, as published by the Free + * Software Foundation. + * 2. GNU General Public License as published by the Free Software + * Foundation; version 2 of the License. + */ + +#pragma once + +#include + +#if defined(DATA_STORE_TYPE_ELOQDSS_ROCKSDB) || \ + defined(DATA_STORE_TYPE_ELOQDSS_ROCKSDB_CLOUD_S3) || \ + defined(DATA_STORE_TYPE_ELOQDSS_ROCKSDB_CLOUD_GCS) +DECLARE_bool(ignore_redis_ttl); +#endif + +namespace EloqDS +{ +/** + * Returns whether RocksDB-backed EloqDSS should preserve and expose expired + * EloqKV records for diagnostics. Non-RocksDB builds always return false. + */ +inline bool IgnoreRedisTTL() +{ +#if defined(DATA_STORE_TYPE_ELOQDSS_ROCKSDB) || \ + defined(DATA_STORE_TYPE_ELOQDSS_ROCKSDB_CLOUD_S3) || \ + defined(DATA_STORE_TYPE_ELOQDSS_ROCKSDB_CLOUD_GCS) + return FLAGS_ignore_redis_ttl; +#else + return false; +#endif +} +} // namespace EloqDS diff --git a/store_handler/eloq_data_store_service/rocksdb_config.cpp b/store_handler/eloq_data_store_service/rocksdb_config.cpp index 493afbc5..28149acc 100644 --- a/store_handler/eloq_data_store_service/rocksdb_config.cpp +++ b/store_handler/eloq_data_store_service/rocksdb_config.cpp @@ -32,6 +32,10 @@ #include "glog/logging.h" DEFINE_string(rocksdb_info_log_level, "INFO", "RocksDB store info log level"); +DEFINE_bool(ignore_redis_ttl, + false, + "Expose persisted EloqKV records without enforcing their TTL. " + "Intended only for diagnostic clusters"); DEFINE_bool(rocksdb_enable_stats, false, "RocksDB store enable stats"); DEFINE_uint32(rocksdb_stats_dump_period_sec, 600, @@ -334,6 +338,12 @@ bool CheckCommandLineFlagIsDefault(const char *name) RocksDBConfig::RocksDBConfig(const INIReader &config, const std::string &eloq_data_path) { + if (CheckCommandLineFlagIsDefault("ignore_redis_ttl")) + { + FLAGS_ignore_redis_ttl = + config.GetBoolean("store", "ignore_redis_ttl", false); + } + info_log_level_ = !CheckCommandLineFlagIsDefault("rocksdb_info_log_level") ? FLAGS_rocksdb_info_log_level : config.GetString("store", diff --git a/store_handler/eloq_data_store_service/rocksdb_data_store_common.cpp b/store_handler/eloq_data_store_service/rocksdb_data_store_common.cpp index 8d01030d..ef282321 100644 --- a/store_handler/eloq_data_store_service/rocksdb_data_store_common.cpp +++ b/store_handler/eloq_data_store_service/rocksdb_data_store_common.cpp @@ -3,6 +3,7 @@ #include #include +#include "ignore_redis_ttl.h" #include "internal_request.h" namespace EloqDS @@ -52,6 +53,14 @@ bool TTLCompactionFilter::Filter(int level, std::string *new_value, bool *value_changed) const { + // Diagnostic mode must not physically reclaim expired records. The value + // remains byte-for-byte unchanged, so normal compaction behavior resumes + // after the mode is disabled and the service is restarted. + if (IgnoreRedisTTL()) + { + return false; + } + const DecodedValueHeader header = DecodeValueHeader(existing_value.data(), existing_value.size()); if (!header.has_ttl) diff --git a/tx_service/tests/TTLCompactionFilter-Test.cpp b/tx_service/tests/TTLCompactionFilter-Test.cpp index 266e7085..d2714aa0 100644 --- a/tx_service/tests/TTLCompactionFilter-Test.cpp +++ b/tx_service/tests/TTLCompactionFilter-Test.cpp @@ -23,6 +23,7 @@ #include // clang-format on +#include "eloq_data_store_service/ignore_redis_ttl.h" #include "eloq_data_store_service/rocksdb_data_store_common.h" namespace @@ -55,10 +56,19 @@ TEST_CASE( { SECTION("expired TTL value is removed") { + FLAGS_ignore_redis_ttl = false; const std::string value = MakeValue(EloqDS::MSB | 42, 1); REQUIRE(ShouldFilter(value, kCompactionTimestamp)); } + SECTION("expired TTL value is retained in diagnostic mode") + { + FLAGS_ignore_redis_ttl = true; + const std::string value = MakeValue(EloqDS::MSB | 42, 1); + REQUIRE_FALSE(ShouldFilter(value, kCompactionTimestamp)); + FLAGS_ignore_redis_ttl = false; + } + SECTION("unexpired TTL value is retained") { const std::string value =