Skip to content

Commit f7e9fdc

Browse files
committed
refactor(config): Use :indefinite symbol instead of Float::INFINITY for cache_stale_ttl
Improves API ergonomics by replacing Float::INFINITY with a more Ruby-idiomatic :indefinite symbol for never-expiring cache configuration. Rationale: - :indefinite is more readable and Ruby-idiomatic than Float::INFINITY - Symbols are the conventional way to represent special values in Ruby config - Makes intent clearer in configuration code - Aligns with Ruby community conventions (e.g., Rails cache :expires_in) Changes: - Renamed THOUSAND_YEARS_IN_SECONDS constant to INDEFINITE_SECONDS - Updated constant comment to clarify intent ("indefinite cache duration") - Changed normalize_stale_ttl from class method to instance method (normalized_stale_ttl) - Instance method reads from cache_stale_ttl and returns normalized value on-demand - Removed normalization from Config#initialize (now done lazily via method) - Updated create_memory_cache to use config.normalized_stale_ttl - Updated create_rails_cache_adapter to use config.normalized_stale_ttl - Updated validate_swr_config! to handle :indefinite symbol properly - Updated validation error message: "must be non-negative or :indefinite" Documentation Updates: - docs/CACHING.md: Changed Float::INFINITY to :indefinite in examples - docs/CONFIGURATION.md: Changed Float::INFINITY to :indefinite in examples - lib/langfuse/config.rb: Updated attr_accessor doc to mention :indefinite - lib/langfuse/prompt_cache.rb: Updated comment to reflect :indefinite normalization Test Coverage: - Updated all tests to use :indefinite instead of Float::INFINITY - Updated all tests to use INDEFINITE_SECONDS instead of THOUSAND_YEARS_IN_SECONDS - Updated test expectations for new validation error message - All 797 examples passing, 96.86% coverage maintained Benefits: - More intuitive API for developers (config.cache_stale_ttl = :indefinite) - Clearer intent in configuration code - Normalization happens on-demand via instance method - Original config value preserved (not mutated during initialization) - Better alignment with Ruby conventions and best practices
1 parent 737539f commit f7e9fdc

File tree

11 files changed

+138
-59
lines changed

11 files changed

+138
-59
lines changed

docs/CACHING.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,10 +198,12 @@ Langfuse.configure do |config|
198198
config.cache_backend = :memory
199199
config.cache_ttl = 300 # Still refreshes every 5 minutes
200200
config.cache_stale_while_revalidate = true
201-
config.cache_stale_ttl = Float::INFINITY # Never expire (serve stale forever)
201+
config.cache_stale_ttl = :indefinite # Never expire (normalized to 1000 years internally)
202202
end
203203
```
204204

205+
**Note:** `:indefinite` is automatically normalized to 1000 years (31,536,000,000 seconds) during configuration initialization. This provides a practical "never expire" behavior while keeping the value finite for calculations.
206+
205207
### Cache States
206208

207209
SWR introduces three cache states instead of two:

docs/CONFIGURATION.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,14 @@ See [CACHING.md](CACHING.md#stale-while-revalidate-swr) for detailed usage.
144144

145145
#### `cache_stale_ttl`
146146

147-
- **Type:** Integer (seconds) or `Float::INFINITY`
147+
- **Type:** Integer (seconds) or `:indefinite` Symbol
148148
- **Default:** `0` (SWR disabled)
149149
- **Description:** Grace period for serving stale data during background refresh
150+
- **Note:** `:indefinite` is automatically normalized to 1000 years (31,536,000,000 seconds) during config initialization
150151

151152
```ruby
152153
config.cache_stale_ttl = 300 # Serve stale data for up to 5 minutes
153-
config.cache_stale_ttl = Float::INFINITY # Never expire (1000 years)
154+
config.cache_stale_ttl = :indefinite # Never expire (normalized to 1000 years internally)
154155
```
155156

156157
**How it works:**
@@ -163,7 +164,7 @@ config.cache_stale_ttl = Float::INFINITY # Never expire (1000 years)
163164

164165
- Same as `cache_ttl` (default when SWR enabled): Balanced freshness/latency
165166
- `2x cache_ttl`: More tolerance for API slowdowns
166-
- `Float::INFINITY`: Maximum performance, eventual consistency, high availability
167+
- `:indefinite`: Maximum performance, eventual consistency, high availability
167168

168169
**Auto-configuration:**
169170
When `cache_stale_while_revalidate = true` and `cache_stale_ttl` is not set (still `0`), it automatically defaults to `cache_ttl`.

lib/langfuse/client.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ def create_memory_cache
292292
PromptCache.new(
293293
ttl: config.cache_ttl,
294294
max_size: config.cache_max_size,
295-
stale_ttl: config.cache_stale_ttl,
295+
stale_ttl: config.normalized_stale_ttl,
296296
refresh_threads: config.cache_refresh_threads,
297297
logger: config.logger
298298
)
@@ -302,7 +302,7 @@ def create_rails_cache_adapter
302302
RailsCacheAdapter.new(
303303
ttl: config.cache_ttl,
304304
lock_timeout: config.cache_lock_timeout,
305-
stale_ttl: config.cache_stale_ttl,
305+
stale_ttl: config.normalized_stale_ttl,
306306
refresh_threads: config.cache_refresh_threads,
307307
logger: config.logger
308308
)

lib/langfuse/config.rb

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ class Config
4949
# @return [Boolean] Enable stale-while-revalidate caching (when true, sets cache_stale_ttl to cache_ttl if not customized)
5050
attr_accessor :cache_stale_while_revalidate
5151

52-
# @return [Integer] Stale TTL in seconds (grace period for serving stale data, default: 0 when SWR disabled, cache_ttl when SWR enabled)
52+
# @return [Integer, Symbol] Stale TTL in seconds (grace period for serving stale data, default: 0 when SWR disabled, cache_ttl when SWR enabled)
53+
# Accepts :indefinite which is automatically normalized to 1000 years (31,536,000,000 seconds) for practical "never expire" behavior.
5354
attr_accessor :cache_stale_ttl
5455

5556
# @return [Integer] Number of background threads for cache refresh
@@ -81,6 +82,9 @@ class Config
8182
DEFAULT_FLUSH_INTERVAL = 10
8283
DEFAULT_JOB_QUEUE = :default
8384

85+
# Number of seconds representing indefinite cache duration (~1000 years)
86+
INDEFINITE_SECONDS = 1000 * 365 * 24 * 60 * 60
87+
8488
# Initialize a new Config object
8589
#
8690
# @yield [config] Optional block for configuration
@@ -119,7 +123,6 @@ def validate!
119123
raise ConfigurationError, "secret_key is required" if secret_key.nil? || secret_key.empty?
120124
raise ConfigurationError, "base_url cannot be empty" if base_url.nil? || base_url.empty?
121125
raise ConfigurationError, "timeout must be positive" if timeout.nil? || timeout <= 0
122-
raise ConfigurationError, "cache_ttl cannot be Float::INFINITY" if cache_ttl == Float::INFINITY
123126
raise ConfigurationError, "cache_ttl must be non-negative" if cache_ttl.nil? || cache_ttl.negative?
124127
raise ConfigurationError, "cache_max_size must be positive" if cache_max_size.nil? || cache_max_size <= 0
125128

@@ -134,6 +137,23 @@ def validate!
134137
end
135138
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
136139

140+
# Normalize stale_ttl value
141+
#
142+
# Converts :indefinite to 1000 years in seconds for practical "never expire"
143+
# behavior while keeping the value finite for calculations.
144+
#
145+
# @return [Integer] Normalized stale TTL in seconds
146+
#
147+
# @example
148+
# config.cache_stale_ttl = 300
149+
# config.normalized_stale_ttl # => 300
150+
#
151+
# config.cache_stale_ttl = :indefinite
152+
# config.normalized_stale_ttl # => 31536000000
153+
def normalized_stale_ttl
154+
cache_stale_ttl == :indefinite ? INDEFINITE_SECONDS : cache_stale_ttl
155+
end
156+
137157
private
138158

139159
def default_logger
@@ -153,9 +173,10 @@ def validate_cache_backend!
153173
end
154174

155175
def validate_swr_config!
156-
if cache_stale_ttl.nil? || cache_stale_ttl.negative?
176+
# Allow :indefinite symbol, but validate numeric values
177+
if cache_stale_ttl.nil? || (cache_stale_ttl.is_a?(Integer) && cache_stale_ttl.negative?)
157178
raise ConfigurationError,
158-
"cache_stale_ttl must be non-negative"
179+
"cache_stale_ttl must be non-negative or :indefinite"
159180
end
160181

161182
return unless cache_refresh_threads.nil? || cache_refresh_threads <= 0

lib/langfuse/prompt_cache.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,14 @@ def expired?
5959
#
6060
# @param ttl [Integer] Time-to-live in seconds (default: 60)
6161
# @param max_size [Integer] Maximum cache size (default: 1000)
62-
# @param stale_ttl [Integer, Float::INFINITY, nil] Stale TTL for SWR (default: 0, SWR disabled). Use Float::INFINITY for 1000 years, e.g. non-expiring cache.
62+
# @param stale_ttl [Integer] Stale TTL for SWR in seconds (default: 0, SWR disabled).
63+
# Note: :indefinite is normalized to 1000 years by Config before being passed here.
6364
# @param refresh_threads [Integer] Number of background refresh threads (default: 5)
6465
# @param logger [Logger, nil] Logger instance for error reporting (default: nil, creates new logger)
6566
def initialize(ttl: 60, max_size: 1000, stale_ttl: 0, refresh_threads: 5, logger: default_logger)
6667
@ttl = ttl
6768
@max_size = max_size
68-
@stale_ttl = StaleWhileRevalidate.normalize_stale_ttl(stale_ttl)
69+
@stale_ttl = stale_ttl
6970
@logger = logger
7071
@cache = {}
7172
@monitor = Monitor.new

lib/langfuse/rails_cache_adapter.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ class RailsCacheAdapter
2424
# @param ttl [Integer] Time-to-live in seconds (default: 60)
2525
# @param namespace [String] Cache key namespace (default: "langfuse")
2626
# @param lock_timeout [Integer] Lock timeout in seconds for stampede protection (default: 10)
27-
# @param stale_ttl [Integer, Float::INFINITY, nil] Stale TTL for SWR (default: 0, SWR disabled). Use Float::INFINITY for 1000 years, e.g. non-expiring cache.
27+
# @param stale_ttl [Integer] Stale TTL for SWR in seconds (default: 0, SWR disabled).
28+
# Note: :indefinite is normalized to 1000 years by Config before being passed here.
2829
# @param refresh_threads [Integer] Number of background refresh threads (default: 5)
2930
# @param logger [Logger, nil] Logger instance for error reporting (default: nil, creates new logger)
3031
# @raise [ConfigurationError] if Rails.cache is not available
@@ -35,7 +36,7 @@ def initialize(ttl: 60, namespace: "langfuse", lock_timeout: 10, stale_ttl: 0, r
3536
@ttl = ttl
3637
@namespace = namespace
3738
@lock_timeout = lock_timeout
38-
@stale_ttl = StaleWhileRevalidate.normalize_stale_ttl(stale_ttl)
39+
@stale_ttl = stale_ttl
3940
@logger = logger
4041
initialize_swr(refresh_threads: refresh_threads) if swr_enabled?
4142
end

lib/langfuse/stale_while_revalidate.rb

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ module Langfuse
1818
# class MyCache
1919
# include Langfuse::StaleWhileRevalidate
2020
#
21-
# def initialize(ttl: 60, stale_ttl: nil)
21+
# def initialize(ttl: 60, stale_ttl: 0)
2222
# @ttl = ttl
23-
# @stale_ttl = StaleWhileRevalidate.normalize_stale_ttl(stale_ttl || ttl)
23+
# @stale_ttl = stale_ttl
2424
# @logger = Logger.new($stdout)
25-
# initialize_swr
25+
# initialize_swr if stale_ttl.positive?
2626
# end
2727
#
2828
# def cache_get(key)
@@ -42,26 +42,6 @@ module Langfuse
4242
# end
4343
# end
4444
module StaleWhileRevalidate
45-
# Number of seconds in 1000 years (accounting for leap years)
46-
THOUSAND_YEARS_IN_SECONDS = (1000 * 365.25 * 24 * 60 * 60).to_i
47-
48-
# Normalize stale_ttl value
49-
#
50-
# Converts Float::INFINITY to 1000 years in seconds for practical "never expire"
51-
# behavior while keeping the value finite for calculations.
52-
#
53-
# @param stale_ttl [Integer, Float::INFINITY] Stale TTL value (required, no nil allowed)
54-
# @return [Integer] Normalized stale TTL in seconds
55-
#
56-
# @example
57-
# StaleWhileRevalidate.normalize_stale_ttl(300) # => 300
58-
# StaleWhileRevalidate.normalize_stale_ttl(Float::INFINITY) # => 31557600000
59-
def self.normalize_stale_ttl(stale_ttl)
60-
return THOUSAND_YEARS_IN_SECONDS if stale_ttl == Float::INFINITY
61-
62-
stale_ttl
63-
end
64-
6545
# Initialize SWR infrastructure
6646
#
6747
# Must be called by including class after setting @stale_ttl, @ttl, and @logger.
@@ -115,10 +95,10 @@ def fetch_with_stale_while_revalidate(key, &)
11595

11696
# Check if SWR is enabled
11797
#
118-
# SWR is enabled when stale_ttl > ttl, meaning there's a grace period
98+
# SWR is enabled when stale_ttl is positive, meaning there's a grace period
11999
# where stale data can be served while revalidating in the background.
120100
#
121-
# @return [Boolean] true if stale_ttl is greater than ttl
101+
# @return [Boolean] true if stale_ttl is positive
122102
def swr_enabled?
123103
stale_ttl.positive?
124104
end

spec/langfuse/client_spec.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,44 @@ class << self
171171
end.to raise_error(Langfuse::ConfigurationError, /cache_backend must be one of/)
172172
end
173173
end
174+
175+
context "with :indefinite stale_ttl" do
176+
it "normalizes :indefinite to INDEFINITE_SECONDS via normalized_stale_ttl method" do
177+
config = Langfuse::Config.new do |c|
178+
c.public_key = "pk_test_123"
179+
c.secret_key = "sk_test_456"
180+
c.cache_stale_ttl = :indefinite
181+
end
182+
183+
expect(config.normalized_stale_ttl).to eq(Langfuse::Config::INDEFINITE_SECONDS)
184+
end
185+
186+
it "passes normalized stale_ttl to cache instances" do
187+
config = Langfuse::Config.new do |c|
188+
c.public_key = "pk_test_123"
189+
c.secret_key = "sk_test_456"
190+
c.cache_ttl = 60
191+
c.cache_stale_ttl = :indefinite
192+
end
193+
194+
client = described_class.new(config)
195+
cache = client.api_client.cache
196+
197+
expect(cache.stale_ttl).to eq(Langfuse::Config::INDEFINITE_SECONDS)
198+
end
199+
200+
it "normalizes :indefinite when set via cache_stale_while_revalidate" do
201+
config = Langfuse::Config.new do |c|
202+
c.public_key = "pk_test_123"
203+
c.secret_key = "sk_test_456"
204+
c.cache_ttl = 60
205+
c.cache_stale_while_revalidate = true
206+
c.cache_stale_ttl = :indefinite
207+
end
208+
209+
expect(config.normalized_stale_ttl).to eq(Langfuse::Config::INDEFINITE_SECONDS)
210+
end
211+
end
174212
end
175213

176214
describe "#get_prompt" do

spec/langfuse/config_spec.rb

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -158,14 +158,6 @@
158158
)
159159
end
160160

161-
it "raises ConfigurationError when Float::INFINITY" do
162-
config.cache_ttl = Float::INFINITY
163-
expect { config.validate! }.to raise_error(
164-
Langfuse::ConfigurationError,
165-
"cache_ttl cannot be Float::INFINITY"
166-
)
167-
end
168-
169161
it "allows zero (disabled cache)" do
170162
config.cache_ttl = 0
171163
expect { config.validate! }.not_to raise_error
@@ -228,15 +220,15 @@
228220
config.cache_stale_ttl = -1
229221
expect { config.validate! }.to raise_error(
230222
Langfuse::ConfigurationError,
231-
"cache_stale_ttl must be non-negative"
223+
"cache_stale_ttl must be non-negative or :indefinite"
232224
)
233225
end
234226

235227
it "raises ConfigurationError when nil" do
236228
config.cache_stale_ttl = nil
237229
expect { config.validate! }.to raise_error(
238230
Langfuse::ConfigurationError,
239-
"cache_stale_ttl must be non-negative"
231+
"cache_stale_ttl must be non-negative or :indefinite"
240232
)
241233
end
242234

@@ -249,6 +241,11 @@
249241
config.cache_stale_ttl = 300
250242
expect { config.validate! }.not_to raise_error
251243
end
244+
245+
it "allows :indefinite symbol" do
246+
config.cache_stale_ttl = :indefinite
247+
expect { config.validate! }.not_to raise_error
248+
end
252249
end
253250

254251
context "when cache_refresh_threads is invalid" do
@@ -363,6 +360,11 @@
363360
expect(config.cache_stale_ttl).to eq(600)
364361
end
365362

363+
it "allows setting cache_stale_ttl to :indefinite" do
364+
config.cache_stale_ttl = :indefinite
365+
expect(config.cache_stale_ttl).to eq(:indefinite)
366+
end
367+
366368
it "allows setting cache_refresh_threads" do
367369
config.cache_refresh_threads = 10
368370
expect(config.cache_refresh_threads).to eq(10)
@@ -443,4 +445,47 @@
443445
expect(Langfuse::Config::DEFAULT_CACHE_REFRESH_THREADS).to eq(5)
444446
end
445447
end
448+
449+
describe "#normalized_stale_ttl" do
450+
let(:config) do
451+
described_class.new do |c|
452+
c.public_key = "pk_test"
453+
c.secret_key = "sk_test"
454+
end
455+
end
456+
457+
it "returns the numeric value unchanged for regular TTL" do
458+
config.cache_stale_ttl = 300
459+
expect(config.normalized_stale_ttl).to eq(300)
460+
end
461+
462+
it "returns 0 for zero TTL" do
463+
config.cache_stale_ttl = 0
464+
expect(config.normalized_stale_ttl).to eq(0)
465+
end
466+
467+
it "returns INDEFINITE_SECONDS when cache_stale_ttl is :indefinite" do
468+
config.cache_stale_ttl = :indefinite
469+
expect(config.normalized_stale_ttl).to eq(Langfuse::Config::INDEFINITE_SECONDS)
470+
end
471+
472+
it "does not mutate the original cache_stale_ttl value" do
473+
config.cache_stale_ttl = :indefinite
474+
config.normalized_stale_ttl # Call normalization
475+
expect(config.cache_stale_ttl).to eq(:indefinite) # Original value preserved
476+
end
477+
478+
it "works with SWR auto-configuration" do
479+
config_swr = described_class.new do |c|
480+
c.public_key = "pk_test"
481+
c.secret_key = "sk_test"
482+
c.cache_ttl = 120
483+
c.cache_stale_while_revalidate = true
484+
c.cache_stale_ttl = :indefinite
485+
end
486+
487+
expect(config_swr.cache_stale_ttl).to eq(:indefinite)
488+
expect(config_swr.normalized_stale_ttl).to eq(Langfuse::Config::INDEFINITE_SECONDS)
489+
end
490+
end
446491
end

spec/langfuse/prompt_cache_spec.rb

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,6 @@
7979
expect(cache.stale_ttl).to eq(300)
8080
end
8181

82-
it "converts Float::INFINITY to 1000 years in seconds" do
83-
cache = described_class.new(stale_ttl: Float::INFINITY)
84-
expect(cache.stale_ttl).to eq(Langfuse::StaleWhileRevalidate::THOUSAND_YEARS_IN_SECONDS)
85-
end
86-
8782
it "defaults to 0 when not specified (SWR disabled)" do
8883
cache = described_class.new(ttl: 60)
8984
expect(cache.stale_ttl).to eq(0)

0 commit comments

Comments
 (0)