Skip to content

Commit f85c00a

Browse files
committed
feat(prompt_cache): Add SWR to PromptCache
1 parent d798182 commit f85c00a

File tree

9 files changed

+603
-262
lines changed

9 files changed

+603
-262
lines changed

lib/langfuse/api_client.rb

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,9 @@ def list_prompts(page: nil, limit: nil)
106106
# @raise [ApiError] for other API errors
107107
def get_prompt(name, version: nil, label: nil)
108108
raise ArgumentError, "Cannot specify both version and label" if version && label
109+
return fetch_prompt_from_api(name, version: version, label: label) if cache.nil?
109110

110111
cache_key = PromptCache.build_key(name, version: version, label: label)
111-
112112
fetch_with_appropriate_caching_strategy(cache_key, name, version, label)
113113
end
114114

@@ -126,26 +126,19 @@ def fetch_with_appropriate_caching_strategy(cache_key, name, version, label)
126126
fetch_with_swr_cache(cache_key, name, version, label)
127127
elsif distributed_cache_available?
128128
fetch_with_distributed_cache(cache_key, name, version, label)
129-
elsif simple_cache_available?
130-
fetch_with_simple_cache(cache_key, name, version, label)
131129
else
132-
fetch_prompt_from_api(name, version: version, label: label)
130+
fetch_with_simple_cache(cache_key, name, version, label)
133131
end
134132
end
135133

136134
# Check if SWR cache is available
137135
def swr_cache_available?
138-
cache&.respond_to?(:fetch_with_stale_while_revalidate)
136+
cache.respond_to?(:swr_enabled?) && cache.swr_enabled?
139137
end
140138

141139
# Check if distributed cache is available
142140
def distributed_cache_available?
143-
cache&.respond_to?(:fetch_with_lock)
144-
end
145-
146-
# Check if simple cache is available
147-
def simple_cache_available?
148-
!cache.nil?
141+
cache.respond_to?(:fetch_with_lock)
149142
end
150143

151144
# Fetch with SWR cache

lib/langfuse/client.rb

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,17 +151,27 @@ def cache_enabled?
151151
def create_cache
152152
case config.cache_backend
153153
when :memory
154-
PromptCache.new(
155-
ttl: config.cache_ttl,
156-
max_size: config.cache_max_size
157-
)
154+
create_memory_cache
158155
when :rails
159156
create_rails_cache_adapter
160157
else
161158
raise ConfigurationError, "Unknown cache backend: #{config.cache_backend}"
162159
end
163160
end
164161

162+
# Create in-memory cache with SWR support if enabled
163+
#
164+
# @return [PromptCache]
165+
def create_memory_cache
166+
PromptCache.new(
167+
ttl: config.cache_ttl,
168+
max_size: config.cache_max_size,
169+
stale_ttl: config.cache_stale_while_revalidate ? config.cache_stale_ttl : nil,
170+
refresh_threads: config.cache_refresh_threads,
171+
logger: config.logger
172+
)
173+
end
174+
165175
def create_rails_cache_adapter
166176
RailsCacheAdapter.new(
167177
ttl: config.cache_ttl,

lib/langfuse/config.rb

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,9 @@ def validate_swr_config!
154154
raise ConfigurationError, "cache_stale_ttl must be non-negative"
155155
end
156156

157-
if cache_refresh_threads.nil? || cache_refresh_threads <= 0
158-
raise ConfigurationError, "cache_refresh_threads must be positive"
159-
end
160-
161-
return unless cache_stale_while_revalidate && cache_backend != :rails
157+
return unless cache_refresh_threads.nil? || cache_refresh_threads <= 0
162158

163-
raise ConfigurationError,
164-
"cache_stale_while_revalidate requires cache_backend to be :rails"
159+
raise ConfigurationError, "cache_refresh_threads must be positive"
165160
end
166161
end
167162
end

lib/langfuse/prompt_cache.rb

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "monitor"
4+
require_relative "stale_while_revalidate"
45

56
module 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
162236
end

0 commit comments

Comments
 (0)