Skip to main content

distributed_cache/
config.rs

1//
2// Copyright 2018-2026 Accenture Technology
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15//
16
17//! Cache tunables from application configuration, plus the shared
18//! [`RedisConfig`] connection parameters resolved from the plain `redis.*`
19//! namespace ([`BASE_PREFIX`]) — Rust port of the Java `CacheConfig`. The
20//! cache and sync-over-async (`soa.redis.*`) therefore never collide, while a
21//! deployment can still point both at one Redis by setting only `redis.*`.
22//!
23//! ```properties
24//! redis.cache.enabled=true            # opt-in master switch (the two functions register only when true)
25//! redis.cache.instances=20            # worker instances (function concurrency), NOT connections -
26//!                                     #   every worker shares the runtime's one multiplexed connection
27//! redis.cache.default.ttl=1h          # default TTL applied when a PUT/MPUT/LIST_PUSH omits one
28//! redis.cache.key.prefix=             # optional namespace prepended to every key (isolate apps sharing one Redis)
29//! ```
30
31use platform_core::AppConfigReader;
32use redis_connection::{duration_seconds, RedisConfig, BASE_PREFIX};
33
34/// `redis.cache.enabled` — the opt-in master switch.
35pub const CACHE_ENABLED_KEY: &str = "redis.cache.enabled";
36/// `redis.cache.instances` — worker instances of `v1.cache.redis`.
37pub const CACHE_INSTANCES_KEY: &str = "redis.cache.instances";
38/// `redis.cache.key.prefix` — the application namespace prepended to every key.
39pub const KEY_PREFIX_KEY: &str = "redis.cache.key.prefix";
40/// `redis.cache.default.ttl` — the TTL a write uses when it omits `ttl`.
41pub const DEFAULT_TTL_KEY: &str = "redis.cache.default.ttl";
42
43const DEFAULT_TTL: &str = "1h";
44const DEFAULT_TTL_SECONDS: u64 = 3600;
45
46/// The resolved cache configuration (Java `CacheConfig` record).
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct CacheConfig {
49    redis: RedisConfig,
50    key_prefix: String,
51    default_ttl_seconds: u64,
52}
53
54impl CacheConfig {
55    /// Read the tunables and the plain-`redis.*` connection parameters from
56    /// application configuration (Java `CacheConfig.from`). A missing or
57    /// unparseable `redis.cache.default.ttl` degrades to the built-in `1h`
58    /// rather than to a zero TTL the server would reject (a port-side
59    /// tightening of the Java `getDurationInSeconds` → 0 behaviour).
60    pub fn from_config() -> Self {
61        let config = AppConfigReader::get_instance();
62        let ttl_text = config.get_property_or(DEFAULT_TTL_KEY, DEFAULT_TTL);
63        CacheConfig {
64            redis: RedisConfig::from_prefix(BASE_PREFIX),
65            key_prefix: config.get_property(KEY_PREFIX_KEY).unwrap_or_default(),
66            default_ttl_seconds: duration_seconds(&ttl_text)
67                .filter(|seconds| *seconds > 0)
68                .unwrap_or(DEFAULT_TTL_SECONDS),
69        }
70    }
71
72    /// Explicit constructor for tests and embedders.
73    pub fn new(
74        redis: RedisConfig,
75        key_prefix: impl Into<String>,
76        default_ttl_seconds: u64,
77    ) -> Self {
78        CacheConfig {
79            redis,
80            key_prefix: key_prefix.into(),
81            default_ttl_seconds,
82        }
83    }
84
85    /// The shared connection parameters (host/port/auth/ssl/cluster) from `redis.*`.
86    pub fn redis(&self) -> &RedisConfig {
87        &self.redis
88    }
89
90    /// Prepended to every cache key; blank = no prefix.
91    pub fn key_prefix(&self) -> &str {
92        &self.key_prefix
93    }
94
95    /// Default TTL in seconds for writes that omit one.
96    pub fn default_ttl_seconds(&self) -> u64 {
97        self.default_ttl_seconds
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use platform_core::overrides;
105
106    fn serial() -> std::sync::MutexGuard<'static, ()> {
107        static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
108        LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
109    }
110
111    const KEYS: &[&str] = &[
112        "redis.cache.key.prefix",
113        "redis.cache.default.ttl",
114        "redis.host",
115        "redis.port",
116        "soa.redis.host",
117    ];
118
119    fn clear() {
120        for key in KEYS {
121            overrides::clear(key);
122        }
123    }
124
125    /// Java `CacheConfigTest.defaultsWhenUnset`.
126    #[test]
127    fn defaults_when_unset() {
128        let _guard = serial();
129        clear();
130        let config = CacheConfig::from_config();
131        assert_eq!("", config.key_prefix());
132        assert_eq!(3600, config.default_ttl_seconds());
133        assert_eq!("127.0.0.1", config.redis().host());
134        assert_eq!(6379, config.redis().port());
135    }
136
137    /// Java `CacheConfigTest.readsCacheTunablesAndPlainRedisNamespace`.
138    #[test]
139    fn reads_cache_tunables_and_the_plain_redis_namespace() {
140        let _guard = serial();
141        clear();
142        overrides::set("redis.cache.key.prefix", "app1:");
143        overrides::set("redis.cache.default.ttl", "10m");
144        overrides::set("redis.host", "cache.example.com");
145        overrides::set("redis.port", "6380");
146        let config = CacheConfig::from_config();
147        assert_eq!("app1:", config.key_prefix());
148        assert_eq!(600, config.default_ttl_seconds());
149        assert_eq!("cache.example.com", config.redis().host());
150        assert_eq!(6380, config.redis().port());
151        // a garbage ttl degrades to the built-in default, never to zero
152        overrides::set("redis.cache.default.ttl", "forever");
153        assert_eq!(3600, CacheConfig::from_config().default_ttl_seconds());
154        clear();
155    }
156
157    /// Java `CacheConfigTest.ignoresTheSoaNamespace`: sync-over-async's keys
158    /// never leak into the cache's connection.
159    #[test]
160    fn ignores_the_soa_namespace() {
161        let _guard = serial();
162        clear();
163        overrides::set("soa.redis.host", "rendezvous.example.com");
164        overrides::set("redis.host", "cache.example.com");
165        assert_eq!(
166            "cache.example.com",
167            CacheConfig::from_config().redis().host()
168        );
169        overrides::clear("redis.host");
170        assert_eq!(
171            "127.0.0.1",
172            CacheConfig::from_config().redis().host(),
173            "without redis.host the cache falls to the default, not to soa.redis.host"
174        );
175        clear();
176    }
177}