Class: BetterService::CacheService

Inherits:
Object
  • Object
show all
Defined in:
lib/better_service/cache_service.rb

Overview

CacheService - Provides cache invalidation and management for BetterService

This service handles cache invalidation based on contexts defined in the Cacheable concern. It provides methods to invalidate cache keys for specific users, contexts, or globally.

Supports cascading invalidation through INVALIDATION_MAP - when a context is invalidated, all related contexts are also invalidated automatically.

Examples:

Invalidate cache for a specific context and user

BetterService::CacheService.invalidate_for_context(current_user, "products")

Invalidate cache globally for a context

BetterService::CacheService.invalidate_global("sidebar")

Invalidate all cache for a user

BetterService::CacheService.invalidate_for_user(current_user)

Configure invalidation map

BetterService::CacheService.configure_invalidation_map(
  'products' => %w[products inventory reports],
  'orders' => %w[orders products reports]
)

Defined Under Namespace

Classes: CacheInvalidationJob

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.invalidation_mapHash<String, Array<String>> (readonly)

Get the current invalidation map

Returns:

  • (Hash<String, Array<String>>)

    Current invalidation mappings



38
39
40
# File 'lib/better_service/cache_service.rb', line 38

def invalidation_map
  @invalidation_map
end

Class Method Details

.add_invalidation_rules(entries) ⇒ void

This method returns an undefined value.

Add entries to the invalidation map without replacing existing ones

Examples:

Add new context invalidation rules

BetterService::CacheService.add_invalidation_rules(
  'payments' => %w[payments statistics invoices]
)

Parameters:

  • entries (Hash<String, Array<String>>)

    New invalidation mappings to add



72
73
74
75
76
77
# File 'lib/better_service/cache_service.rb', line 72

def add_invalidation_rules(entries)
  new_entries = entries.transform_keys(&:to_s).transform_values do |contexts|
    Array(contexts).map(&:to_s)
  end
  @invalidation_map = (@invalidation_map || {}).merge(new_entries).freeze
end

.clear_allInteger

Clear all BetterService cache

WARNING: This deletes ALL cache keys that match BetterService patterns. Use with caution, preferably only in development/testing.

Examples:

# In test setup
BetterService::CacheService.clear_all

Returns:

  • (Integer)

    Number of keys deleted (if supported by cache store)



242
243
244
245
246
247
# File 'lib/better_service/cache_service.rb', line 242

def clear_all
  pattern = "*:user_*:*" # Match all BetterService cache keys
  result = delete_matched(pattern)
  # Ensure we return Integer, not Array
  result.is_a?(Array) ? result.size : (result || 0)
end

.configure_invalidation_map(map) ⇒ void

This method returns an undefined value.

Configure the invalidation map for cascading cache invalidation

The invalidation map defines which cache contexts should be invalidated together. When a primary context is invalidated, all related contexts in the map are also invalidated.

Examples:

Configure invalidation relationships

BetterService::CacheService.configure_invalidation_map(
  'products' => %w[products inventory reports],
  'orders' => %w[orders products reports],
  'users' => %w[users orders reports],
  'categories' => %w[categories products reports],
  'inventory' => %w[inventory products reports]
)

Parameters:

  • map (Hash<String, Array<String>>)

    Invalidation mappings



57
58
59
60
61
# File 'lib/better_service/cache_service.rb', line 57

def configure_invalidation_map(map)
  @invalidation_map = map.transform_keys(&:to_s).transform_values do |contexts|
    Array(contexts).map(&:to_s)
  end.freeze
end

.contexts_to_invalidate(context) ⇒ Array<String>

Get all contexts that should be invalidated for a given context

If the context exists in the invalidation map, returns all mapped contexts. Otherwise, returns an array containing just the original context.

Examples:

contexts_for('products')  # => ['products', 'inventory', 'reports']
contexts_for('unknown')   # => ['unknown']

Parameters:

  • context (String, Symbol)

    The primary context

Returns:

  • (Array<String>)

    All contexts to invalidate



90
91
92
93
# File 'lib/better_service/cache_service.rb', line 90

def contexts_to_invalidate(context)
  context_str = context.to_s
  (@invalidation_map || {})[context_str] || [ context_str ]
end

.exist?(key) ⇒ Boolean

Check if a key exists in cache

Parameters:

  • key (String)

    The cache key to check

Returns:

  • (Boolean)

    true if key exists, false otherwise



273
274
275
276
277
# File 'lib/better_service/cache_service.rb', line 273

def exist?(key)
  return false unless key && !key.to_s.strip.empty?

  Rails.cache.exist?(key)
end

.fetch(key, options = {}, &block) ⇒ Object

Fetch from cache with block

Wrapper around Rails.cache.fetch with BetterService conventions. If the key exists, returns cached value. Otherwise, executes block, caches the result, and returns it.

Examples:

result = BetterService::CacheService.fetch("my_key", expires_in: 1.hour) do
  expensive_computation
end

Parameters:

  • key (String)

    The cache key

  • options (Hash) (defaults to: {})

    Options passed to Rails.cache.fetch

Options Hash (options):

  • :expires_in (Integer)

    TTL in seconds

  • :force (Boolean)

    Force cache refresh

Returns:

  • (Object)

    The cached or computed value



265
266
267
# File 'lib/better_service/cache_service.rb', line 265

def fetch(key, options = {}, &block)
  Rails.cache.fetch(key, options, &block)
end

.invalidate_for_context(user, context, async: false, cascade: true) ⇒ Integer

Invalidate cache for a specific context and user

Deletes all cache keys that match the pattern for the given user and context. Uses cascading invalidation - if the context exists in the invalidation map, all related contexts will also be invalidated.

This is useful when data changes that affects a specific user’s cached results.

Examples:

Basic invalidation

# After creating a product, invalidate products cache for user
BetterService::CacheService.invalidate_for_context(user, "products")

With cascading (if map configured for orders -> [orders, products, reports])

BetterService::CacheService.invalidate_for_context(user, "orders")
# Invalidates: orders, products, reports caches

Without cascading

BetterService::CacheService.invalidate_for_context(user, "orders", cascade: false)
# Invalidates: only orders cache

Parameters:

  • user (Object)

    The user whose cache should be invalidated

  • context (String)

    The context name (e.g., “products”, “sidebar”)

  • async (Boolean) (defaults to: false)

    Whether to perform invalidation asynchronously

  • cascade (Boolean) (defaults to: true)

    Whether to use cascading invalidation (default: true)

Returns:

  • (Integer)

    Number of keys deleted (if supported by cache store)



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/better_service/cache_service.rb', line 120

def invalidate_for_context(user, context, async: false, cascade: true)
  return 0 unless user && context && !context.to_s.strip.empty?

  # Get all contexts to invalidate (cascading or single)
  contexts = cascade ? contexts_to_invalidate(context) : [ context.to_s ]
  total_deleted = 0

  contexts.each do |ctx|
    pattern = build_user_context_pattern(user, ctx)

    if async
      invalidate_async(pattern)
    else
      result = delete_matched(pattern)
      count = result.is_a?(Array) ? result.size : (result || 0)
      total_deleted += count
    end
  end

  log_cascading_invalidation(context, contexts) if cascade && contexts.size > 1
  total_deleted
end

.invalidate_for_user(user, async: false) ⇒ Integer

Invalidate all cache for a specific user

Deletes all cache keys associated with the given user. This is useful when a user logs out or their permissions change.

Examples:

# After user role changes
BetterService::CacheService.invalidate_for_user(current_user)

Parameters:

  • user (Object)

    The user whose cache should be invalidated

  • async (Boolean) (defaults to: false)

    Whether to perform invalidation asynchronously

Returns:

  • (Integer)

    Number of keys deleted (if supported by cache store)



198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/better_service/cache_service.rb', line 198

def invalidate_for_user(user, async: false)
  return 0 unless user

  pattern = build_user_pattern(user)

  if async
    invalidate_async(pattern)
    0
  else
    result = delete_matched(pattern)
    # Ensure we return Integer, not Array
    result.is_a?(Array) ? result.size : (result || 0)
  end
end

.invalidate_global(context, async: false, cascade: true) ⇒ Integer

Invalidate cache globally for a context

Deletes all cache keys for the given context across all users. Uses cascading invalidation - if the context exists in the invalidation map, all related contexts will also be invalidated.

This is useful when data changes that affects everyone (e.g., global settings).

Examples:

Basic global invalidation

# After updating global sidebar settings
BetterService::CacheService.invalidate_global("sidebar")

With cascading

BetterService::CacheService.invalidate_global("orders")
# Invalidates: orders, products, reports caches globally

Parameters:

  • context (String)

    The context name

  • async (Boolean) (defaults to: false)

    Whether to perform invalidation asynchronously

  • cascade (Boolean) (defaults to: true)

    Whether to use cascading invalidation (default: true)

Returns:

  • (Integer)

    Number of keys deleted (if supported by cache store)



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/better_service/cache_service.rb', line 163

def invalidate_global(context, async: false, cascade: true)
  return 0 unless context && !context.to_s.strip.empty?

  # Get all contexts to invalidate (cascading or single)
  contexts = cascade ? contexts_to_invalidate(context) : [ context.to_s ]
  total_deleted = 0

  contexts.each do |ctx|
    pattern = build_global_context_pattern(ctx)

    if async
      invalidate_async(pattern)
    else
      result = delete_matched(pattern)
      count = result.is_a?(Array) ? result.size : (result || 0)
      total_deleted += count
    end
  end

  log_cascading_invalidation(context, contexts, global: true) if cascade && contexts.size > 1
  total_deleted
end

.invalidate_key(key) ⇒ Boolean

Invalidate specific cache key

Deletes a single cache key. Useful when you know the exact key.

Examples:

BetterService::CacheService.invalidate_key("products_index:user_123:abc123")

Parameters:

  • key (String)

    The cache key to delete

Returns:

  • (Boolean)

    true if deleted, false otherwise



222
223
224
225
226
227
228
229
230
# File 'lib/better_service/cache_service.rb', line 222

def invalidate_key(key)
  return false unless key && !key.to_s.strip.empty?

  Rails.cache.delete(key)
  true
rescue ArgumentError
  # Rails.cache.delete raises ArgumentError for invalid keys
  false
end

.statsHash

Get cache statistics

Returns information about cache store and BetterService cache usage. Note: Detailed stats only available with certain cache stores (Redis).

Returns:

  • (Hash)

    Cache statistics



285
286
287
288
289
290
291
292
293
# File 'lib/better_service/cache_service.rb', line 285

def stats
  {
    cache_store: Rails.cache.class.name,
    supports_pattern_deletion: supports_delete_matched?,
    supports_async: defined?(ActiveJob) ? true : false,
    invalidation_map_configured: (@invalidation_map || {}).any?,
    invalidation_map_contexts: (@invalidation_map || {}).keys
  }
end