Class: Tina4::CacheBackends::RedisBackend

Inherits:
BaseBackend
  • Object
show all
Defined in:
lib/tina4/cache_backends/redis_backend.rb

Overview

Redis / Valkey backend (parity with Python _RedisBackend). Uses the redis gem if it is installed, otherwise falls back to the raw RESP protocol over a TCP socket — so it works with zero runtime dependencies.

URL form: scheme://[user[:password]@]host[/db]. Credentials may also be supplied via TINA4_CACHE_USERNAME / TINA4_CACHE_PASSWORD (parity with TINA4_DATABASE_USERNAME / TINA4_DATABASE_PASSWORD). Wrong credentials cause available? to return false, so the factory falls back to file.

Direct Known Subclasses

ValkeyBackend

Constant Summary collapse

PREFIX =
"tina4:cache:"

Instance Method Summary collapse

Methods inherited from BaseBackend

#sweep

Constructor Details

#initialize(url: "redis://localhost:6379", max_entries: 1000, name: "redis") ⇒ RedisBackend

Returns a new instance of RedisBackend.



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/tina4/cache_backends/redis_backend.rb', line 21

def initialize(url: "redis://localhost:6379", max_entries: 1000, name: "redis")
  @max_entries = max_entries
  @name = name
  @hits = 0
  @misses = 0
  @client = nil
  @use_raw = false

  parse_url(url)

  # Try the redis gem first.
  begin
    require "redis"
    kwargs = { host: @host, port: @port, db: @db, timeout: 5 }
    kwargs[:password] = @password if @password
    kwargs[:username] = @username if @username
    @client = Redis.new(**kwargs)
    @client.ping
    @available = true
  rescue LoadError, StandardError
    @client = nil
    @use_raw = true
    # No gem — usable only if the server answers (and authenticates).
    @available = probe
  end
end

Instance Method Details

#available?Boolean

Returns:

  • (Boolean)


48
49
50
# File 'lib/tina4/cache_backends/redis_backend.rb', line 48

def available?
  @available
end

#clearObject

Remove EVERY entry this cache can serve - on BOTH transports.

The raw RESP path used to be an empty branch with a comment: "no easy pattern delete, rely on TTL". That made clear() a no-op on the ZERO-DEPENDENCY default install, so a write never invalidated the persistent DB query cache and every instance kept serving pre-write rows until the TTL ran out (ADR-0024 rule 4: the default provider counts double).

SCAN, not KEYS: clear() runs on every write in persistent DB-cache mode, and KEYS is O(N) and blocks the whole server for its duration. Redis's own documentation says to prefer SCAN in production.

The scan is scoped to our prefix, so another application sharing the server is untouched. FLUSHALL/FLUSHDB would take their data with it and is never used here.



128
129
130
131
132
133
134
135
# File 'lib/tina4/cache_backends/redis_backend.rb', line 128

def clear
  @hits = 0
  @misses = 0
  return scan_delete_with_client if @client
  return unless @use_raw

  scan_delete_with_raw_resp
end

#delete(key) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/tina4/cache_backends/redis_backend.rb', line 97

def delete(key)
  full_key = PREFIX + key
  if @client
    begin
      @client.del(full_key) > 0
    rescue StandardError
      false
    end
  elsif @use_raw
    resp_command("DEL", full_key) == "1"
  else
    false
  end
end

#get(key) ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/tina4/cache_backends/redis_backend.rb', line 52

def get(key)
  full_key = PREFIX + key
  raw = if @client
          begin
            @client.get(full_key)
          rescue StandardError
            nil
          end
        elsif @use_raw
          resp_command("GET", full_key)
        end

  if raw.nil?
    @misses += 1
    return nil
  end
  @hits += 1
  begin
    JSON.parse(raw)
  rescue JSON::ParserError, TypeError
    raw
  end
end

#nameObject



148
149
150
# File 'lib/tina4/cache_backends/redis_backend.rb', line 148

def name
  @name
end

#set(key, value, ttl) ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/tina4/cache_backends/redis_backend.rb', line 76

def set(key, value, ttl)
  full_key = PREFIX + key
  serialized = JSON.generate(value)
  if @client
    begin
      if ttl > 0
        @client.setex(full_key, ttl, serialized)
      else
        @client.set(full_key, serialized)
      end
    rescue StandardError
    end
  elsif @use_raw
    if ttl > 0
      resp_command("SETEX", full_key, ttl.to_s, serialized)
    else
      resp_command("SET", full_key, serialized)
    end
  end
end

#statsObject

Report OUR entries, counted with a scoped SCAN.

size used to be a hardcoded 0 unless the redis GEM was loaded, so on the ZERO-DEPENDENCY raw RESP transport - the default install - stats reported 0 no matter how many entries were cached. Anything reading that number (a dashboard, db.cache_stats, an operator checking whether a clear worked) was reading a constant.



144
145
146
# File 'lib/tina4/cache_backends/redis_backend.rb', line 144

def stats
  { hits: @hits, misses: @misses, size: scan_count, backend: @name }
end