11# frozen_string_literal: true
22
33require "monitor"
4+ require_relative "stale_while_revalidate"
45
56module Langfuse
67 # Simple in-memory cache for prompt data with TTL
@@ -14,6 +15,8 @@ module Langfuse
1415 # cache.get("greeting:1") # => prompt_data
1516 #
1617 class PromptCache
18+ include StaleWhileRevalidate
19+
1720 # Cache entry with data and expiration time
1821 #
1922 # Supports stale-while-revalidate pattern:
@@ -50,17 +53,24 @@ def expired?
5053 end
5154 end
5255
53- attr_reader :ttl , :max_size
56+ attr_reader :ttl , :max_size , :stale_ttl , :logger
5457
5558 # Initialize a new cache
5659 #
5760 # @param ttl [Integer] Time-to-live in seconds (default: 60)
5861 # @param max_size [Integer] Maximum cache size (default: 1000)
59- def initialize ( ttl : 60 , max_size : 1000 )
62+ # @param stale_ttl [Integer, nil] Stale TTL for SWR (default: nil, disabled)
63+ # @param refresh_threads [Integer] Number of background refresh threads (default: 5)
64+ # @param logger [Logger, nil] Logger instance for error reporting (default: nil, creates new logger)
65+ def initialize ( ttl : 60 , max_size : 1000 , stale_ttl : nil , refresh_threads : 5 , logger : default_logger )
6066 @ttl = ttl
6167 @max_size = max_size
68+ @stale_ttl = stale_ttl
69+ @logger = logger
6270 @cache = { }
6371 @monitor = Monitor . new
72+ @locks = { } # Track locks for in-memory locking
73+ initialize_swr ( refresh_threads : refresh_threads ) if stale_ttl
6474 end
6575
6676 # Get a value from the cache
@@ -81,15 +91,16 @@ def get(key)
8191 #
8292 # @param key [String] Cache key
8393 # @param value [Object] Value to cache
94+ # @param expires_in [Integer] Optional TTL override (default: uses @ttl)
8495 # @return [Object] The cached value
85- def set ( key , value )
96+ def set ( key , value , expires_in : ttl )
8697 @monitor . synchronize do
8798 # Evict oldest entry if at max size
8899 evict_oldest if @cache . size >= max_size
89100
90101 now = Time . now
91- fresh_until = now + ttl
92- stale_until = now + ttl
102+ fresh_until = now + expires_in
103+ stale_until = now + expires_in
93104 @cache [ key ] = CacheEntry . new ( value , fresh_until , stale_until )
94105 value
95106 end
@@ -148,6 +159,62 @@ def self.build_key(name, version: nil, label: nil)
148159
149160 private
150161
162+ # Implementation of StaleWhileRevalidate abstract methods
163+
164+ # Get value from cache (SWR interface)
165+ #
166+ # @param key [String] Cache key
167+ # @return [Object, nil] Cached value
168+ def cache_get ( key )
169+ @monitor . synchronize do
170+ @cache [ key ]
171+ end
172+ end
173+
174+ # Set value in cache (SWR interface)
175+ #
176+ # @param key [String] Cache key
177+ # @param value [Object] Value to cache
178+ # @param expires_in [Integer] TTL in seconds
179+ # @return [Object] The cached value
180+ def cache_set ( key , value , expires_in : nil )
181+ @monitor . synchronize do
182+ # Evict oldest entry if at max size
183+ evict_oldest if @cache . size >= max_size
184+
185+ # NOTE: expires_in is accepted for interface compatibility with StaleWhileRevalidate
186+ # but not used here since CacheEntry objects manage their own expiration times
187+ _ = expires_in
188+ @cache [ key ] = value
189+ value
190+ end
191+ end
192+
193+ # Acquire a refresh lock using in-memory locking
194+ #
195+ # @param lock_key [String] Lock key
196+ # @return [Boolean] true if lock was acquired, false if already held
197+ def acquire_refresh_lock ( lock_key )
198+ @monitor . synchronize do
199+ return false if @locks [ lock_key ]
200+
201+ @locks [ lock_key ] = true
202+ true
203+ end
204+ end
205+
206+ # Release a refresh lock
207+ #
208+ # @param lock_key [String] Lock key
209+ # @return [void]
210+ def release_refresh_lock ( lock_key )
211+ @monitor . synchronize do
212+ @locks . delete ( lock_key )
213+ end
214+ end
215+
216+ # In-memory cache helper methods
217+
151218 # Evict the oldest entry from cache
152219 #
153220 # @return [void]
@@ -158,5 +225,12 @@ def evict_oldest
158225 oldest_key = @cache . min_by { |_key , entry | entry . stale_until } &.first
159226 @cache . delete ( oldest_key ) if oldest_key
160227 end
228+
229+ # Create a default logger
230+ #
231+ # @return [Logger]
232+ def default_logger
233+ Logger . new ( $stdout, level : Logger ::WARN )
234+ end
161235 end
162236end
0 commit comments