tina4ruby 3.13.23 → 3.13.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/tina4/cache_backends/base_backend.rb +58 -0
- data/lib/tina4/cache_backends/database_backend.rb +121 -0
- data/lib/tina4/cache_backends/file_backend.rb +140 -0
- data/lib/tina4/cache_backends/memcached_backend.rb +103 -0
- data/lib/tina4/cache_backends/memory_backend.rb +81 -0
- data/lib/tina4/cache_backends/mongo_backend.rb +133 -0
- data/lib/tina4/cache_backends/redis_backend.rb +233 -0
- data/lib/tina4/cache_backends/valkey_backend.rb +16 -0
- data/lib/tina4/cache_backends.rb +91 -0
- data/lib/tina4/database.rb +95 -0
- data/lib/tina4/response_cache.rb +71 -243
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4.rb +1 -0
- metadata +24 -1
data/lib/tina4/response_cache.rb
CHANGED
|
@@ -14,16 +14,27 @@ module Tina4
|
|
|
14
14
|
# middleware by attaching ResponseCache to your route, not by calling
|
|
15
15
|
# the (private) internal_lookup / internal_store directly.
|
|
16
16
|
#
|
|
17
|
-
# Backends are selected via the TINA4_CACHE_BACKEND env var
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# file
|
|
17
|
+
# Backends are selected via the TINA4_CACHE_BACKEND env var and built by the
|
|
18
|
+
# unified factory (Tina4::CacheBackends.create_backend):
|
|
19
|
+
# memory — in-process LRU cache (default, zero deps)
|
|
20
|
+
# file — JSON files in data/cache/
|
|
21
|
+
# redis — Redis (redis gem or raw RESP over TCP)
|
|
22
|
+
# valkey — Valkey (Redis wire protocol; reports "valkey")
|
|
23
|
+
# memcached — Memcached (zero-dep text protocol over TCP)
|
|
24
|
+
# mongodb — MongoDB TTL collection (requires the mongo gem)
|
|
25
|
+
# database — tina4_cache table in any Tina4-supported database
|
|
26
|
+
#
|
|
27
|
+
# A configured network/driver backend that is unreachable degrades to the
|
|
28
|
+
# file backend (a real working cache), never a silent no-op.
|
|
21
29
|
#
|
|
22
30
|
# Environment:
|
|
23
|
-
# TINA4_CACHE_BACKEND — memory
|
|
24
|
-
# TINA4_CACHE_URL —
|
|
31
|
+
# TINA4_CACHE_BACKEND — memory|file|redis|valkey|memcached|mongodb|database
|
|
32
|
+
# TINA4_CACHE_URL — connection URL (redis/valkey/memcached/mongo) OR
|
|
33
|
+
# SQL URL for database (falls back to TINA4_DATABASE_URL)
|
|
25
34
|
# TINA4_CACHE_TTL — default TTL in seconds (default: 60)
|
|
26
35
|
# TINA4_CACHE_MAX_ENTRIES — maximum cache entries (default: 1000)
|
|
36
|
+
# TINA4_CACHE_DIR — file backend directory (default: data/cache)
|
|
37
|
+
# TINA4_CACHE_USERNAME / TINA4_CACHE_PASSWORD — credentials when not in the URL
|
|
27
38
|
#
|
|
28
39
|
class ResponseCache
|
|
29
40
|
CacheEntry = Struct.new(:body, :content_type, :status_code, :expires_at)
|
|
@@ -187,52 +198,32 @@ module Tina4
|
|
|
187
198
|
#
|
|
188
199
|
# @return [Hash] with :hits, :misses, :size, :backend, :keys
|
|
189
200
|
def cache_stats
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
when "memory"
|
|
201
|
+
if memory_backend?
|
|
202
|
+
@mutex.synchronize do
|
|
193
203
|
now = Time.now.to_f
|
|
194
204
|
@store.reject! { |_k, v| v.is_a?(CacheEntry) && now > v.expires_at }
|
|
195
205
|
{ hits: @hits, misses: @misses, size: @store.size, backend: @backend_name, keys: @store.keys.dup }
|
|
196
|
-
when "file"
|
|
197
|
-
sweep
|
|
198
|
-
files = Dir.glob(File.join(@cache_dir, "*.json"))
|
|
199
|
-
{ hits: @hits, misses: @misses, size: files.size, backend: @backend_name, keys: [] }
|
|
200
|
-
when "redis"
|
|
201
|
-
size = 0
|
|
202
|
-
if @redis_client
|
|
203
|
-
begin
|
|
204
|
-
keys = @redis_client.keys("tina4:cache:*")
|
|
205
|
-
size = keys.size
|
|
206
|
-
rescue StandardError
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
{ hits: @hits, misses: @misses, size: size, backend: @backend_name, keys: [] }
|
|
210
|
-
else
|
|
211
|
-
{ hits: @hits, misses: @misses, size: @store.size, backend: @backend_name, keys: @store.keys.dup }
|
|
212
206
|
end
|
|
207
|
+
else
|
|
208
|
+
st = @backend.stats
|
|
209
|
+
{ hits: @hits, misses: @misses, size: st[:size], backend: @backend_name, keys: [] }
|
|
213
210
|
end
|
|
214
211
|
end
|
|
215
212
|
|
|
216
213
|
# Clear all cached responses.
|
|
217
214
|
def clear_cache
|
|
218
|
-
|
|
219
|
-
@
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
case @backend_name
|
|
223
|
-
when "memory"
|
|
215
|
+
if memory_backend?
|
|
216
|
+
@mutex.synchronize do
|
|
217
|
+
@hits = 0
|
|
218
|
+
@misses = 0
|
|
224
219
|
@store.clear
|
|
225
|
-
when "file"
|
|
226
|
-
Dir.glob(File.join(@cache_dir, "*.json")).each { |f| File.delete(f) rescue nil }
|
|
227
|
-
when "redis"
|
|
228
|
-
if @redis_client
|
|
229
|
-
begin
|
|
230
|
-
keys = @redis_client.keys("tina4:cache:*")
|
|
231
|
-
@redis_client.del(*keys) unless keys.empty?
|
|
232
|
-
rescue StandardError
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
220
|
end
|
|
221
|
+
else
|
|
222
|
+
@mutex.synchronize do
|
|
223
|
+
@hits = 0
|
|
224
|
+
@misses = 0
|
|
225
|
+
end
|
|
226
|
+
@backend.clear
|
|
236
227
|
end
|
|
237
228
|
end
|
|
238
229
|
|
|
@@ -240,8 +231,7 @@ module Tina4
|
|
|
240
231
|
#
|
|
241
232
|
# @return [Integer] number of entries removed
|
|
242
233
|
def sweep
|
|
243
|
-
|
|
244
|
-
when "memory"
|
|
234
|
+
if memory_backend?
|
|
245
235
|
@mutex.synchronize do
|
|
246
236
|
now = Time.now.to_f
|
|
247
237
|
keys_to_remove = @store.select { |_k, v| v.is_a?(CacheEntry) && now > v.expires_at }.keys
|
|
@@ -249,21 +239,12 @@ module Tina4
|
|
|
249
239
|
keys_to_remove.each { |k| @store.delete(k) }
|
|
250
240
|
keys_to_remove.size
|
|
251
241
|
end
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
Dir.glob(File.join(@cache_dir, "*.json")).each do |f|
|
|
256
|
-
begin
|
|
257
|
-
data = JSON.parse(File.read(f))
|
|
258
|
-
if data["expires_at"] && now > data["expires_at"]
|
|
259
|
-
File.delete(f)
|
|
260
|
-
removed += 1
|
|
261
|
-
end
|
|
262
|
-
rescue StandardError
|
|
263
|
-
end
|
|
264
|
-
end
|
|
265
|
-
removed
|
|
242
|
+
elsif @backend.respond_to?(:sweep)
|
|
243
|
+
# The file backend supports an explicit sweep that returns a count.
|
|
244
|
+
@backend.sweep
|
|
266
245
|
else
|
|
246
|
+
# Network/db backends expire entries lazily (TTL) — parity with
|
|
247
|
+
# Python, whose non-memory backends return 0 from sweep.
|
|
267
248
|
0
|
|
268
249
|
end
|
|
269
250
|
end
|
|
@@ -370,62 +351,50 @@ module Tina4
|
|
|
370
351
|
|
|
371
352
|
# ── Backend initialization ─────────────────────────────────
|
|
372
353
|
|
|
354
|
+
# Build the storage backend. Memory keeps an in-process LRU store on the
|
|
355
|
+
# ResponseCache itself (for direct CacheEntry access + per-instance stats);
|
|
356
|
+
# every other backend delegates to a Tina4::CacheBackends object, which
|
|
357
|
+
# also handles graceful file-fallback when a network backend is
|
|
358
|
+
# unreachable. @backend_name is reconciled to the ACTUAL backend so a
|
|
359
|
+
# degraded redis correctly reports "file".
|
|
373
360
|
def init_backend
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
when "file"
|
|
378
|
-
init_file_dir
|
|
361
|
+
if @backend_name == "memory"
|
|
362
|
+
@backend = nil
|
|
363
|
+
return
|
|
379
364
|
end
|
|
380
|
-
end
|
|
381
|
-
|
|
382
|
-
def init_redis
|
|
383
|
-
@redis_client = nil
|
|
384
|
-
begin
|
|
385
|
-
require "redis"
|
|
386
|
-
parsed = parse_redis_url(@cache_url)
|
|
387
|
-
@redis_client = Redis.new(host: parsed[:host], port: parsed[:port], db: parsed[:db], timeout: 5)
|
|
388
|
-
@redis_client.ping
|
|
389
|
-
rescue LoadError, StandardError
|
|
390
|
-
@redis_client = nil
|
|
391
|
-
end
|
|
392
|
-
end
|
|
393
365
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
{ host: host, port: port, db: db }
|
|
366
|
+
@backend = Tina4::CacheBackends.create_backend(
|
|
367
|
+
backend: @backend_name,
|
|
368
|
+
url: @cache_url,
|
|
369
|
+
max_entries: @max_entries,
|
|
370
|
+
cache_dir: @cache_dir
|
|
371
|
+
)
|
|
372
|
+
# Reconcile reported name with reality (e.g. redis → file on fallback).
|
|
373
|
+
@backend_name = @backend.name
|
|
403
374
|
end
|
|
404
375
|
|
|
405
|
-
def
|
|
406
|
-
|
|
407
|
-
require "fileutils"
|
|
408
|
-
FileUtils.mkdir_p(@cache_dir)
|
|
376
|
+
def memory_backend?
|
|
377
|
+
@backend.nil?
|
|
409
378
|
end
|
|
410
379
|
|
|
411
380
|
# ── Backend operations ─────────────────────────────────────
|
|
381
|
+
#
|
|
382
|
+
# The middleware stores response entries as a Hash (entry_data) and the
|
|
383
|
+
# direct KV API stores its own wrapper Hash. Both round-trip through the
|
|
384
|
+
# unified backend's get/set/delete; for memory we keep the in-process
|
|
385
|
+
# store with LRU eviction (parity with the previous behaviour and the
|
|
386
|
+
# response-cache CacheEntry path).
|
|
412
387
|
|
|
413
388
|
def backend_get(key)
|
|
414
|
-
|
|
415
|
-
when "memory"
|
|
389
|
+
if memory_backend?
|
|
416
390
|
@mutex.synchronize { @store[key] }
|
|
417
|
-
when "redis"
|
|
418
|
-
redis_get(key)
|
|
419
|
-
when "file"
|
|
420
|
-
file_get(key)
|
|
421
391
|
else
|
|
422
|
-
@
|
|
392
|
+
@backend.get(key)
|
|
423
393
|
end
|
|
424
394
|
end
|
|
425
395
|
|
|
426
396
|
def backend_set(key, entry, ttl)
|
|
427
|
-
|
|
428
|
-
when "memory"
|
|
397
|
+
if memory_backend?
|
|
429
398
|
@mutex.synchronize do
|
|
430
399
|
if @store.size >= @max_entries && !@store.key?(key)
|
|
431
400
|
oldest_key = @store.keys.first
|
|
@@ -433,157 +402,16 @@ module Tina4
|
|
|
433
402
|
end
|
|
434
403
|
@store[key] = entry
|
|
435
404
|
end
|
|
436
|
-
when "redis"
|
|
437
|
-
redis_set(key, entry, ttl)
|
|
438
|
-
when "file"
|
|
439
|
-
file_set(key, entry)
|
|
440
|
-
end
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
def backend_delete(key)
|
|
444
|
-
case @backend_name
|
|
445
|
-
when "memory"
|
|
446
|
-
@mutex.synchronize do
|
|
447
|
-
!@store.delete(key).nil?
|
|
448
|
-
end
|
|
449
|
-
when "redis"
|
|
450
|
-
redis_delete(key)
|
|
451
|
-
when "file"
|
|
452
|
-
file_delete(key)
|
|
453
|
-
end
|
|
454
|
-
end
|
|
455
|
-
|
|
456
|
-
# ── Redis operations ───────────────────────────────────────
|
|
457
|
-
|
|
458
|
-
def redis_get(key)
|
|
459
|
-
full_key = "tina4:cache:#{key}"
|
|
460
|
-
if @redis_client
|
|
461
|
-
begin
|
|
462
|
-
raw = @redis_client.get(full_key)
|
|
463
|
-
return nil if raw.nil?
|
|
464
|
-
JSON.parse(raw)
|
|
465
|
-
rescue StandardError
|
|
466
|
-
nil
|
|
467
|
-
end
|
|
468
|
-
else
|
|
469
|
-
resp_get(full_key)
|
|
470
|
-
end
|
|
471
|
-
end
|
|
472
|
-
|
|
473
|
-
def redis_set(key, entry, ttl)
|
|
474
|
-
full_key = "tina4:cache:#{key}"
|
|
475
|
-
serialized = JSON.generate(entry)
|
|
476
|
-
if @redis_client
|
|
477
|
-
begin
|
|
478
|
-
if ttl > 0
|
|
479
|
-
@redis_client.setex(full_key, ttl, serialized)
|
|
480
|
-
else
|
|
481
|
-
@redis_client.set(full_key, serialized)
|
|
482
|
-
end
|
|
483
|
-
rescue StandardError
|
|
484
|
-
end
|
|
485
|
-
else
|
|
486
|
-
if ttl > 0
|
|
487
|
-
resp_command("SETEX", full_key, ttl.to_s, serialized)
|
|
488
|
-
else
|
|
489
|
-
resp_command("SET", full_key, serialized)
|
|
490
|
-
end
|
|
491
|
-
end
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
def redis_delete(key)
|
|
495
|
-
full_key = "tina4:cache:#{key}"
|
|
496
|
-
if @redis_client
|
|
497
|
-
begin
|
|
498
|
-
@redis_client.del(full_key) > 0
|
|
499
|
-
rescue StandardError
|
|
500
|
-
false
|
|
501
|
-
end
|
|
502
|
-
else
|
|
503
|
-
result = resp_command("DEL", full_key)
|
|
504
|
-
result == "1"
|
|
505
|
-
end
|
|
506
|
-
end
|
|
507
|
-
|
|
508
|
-
def resp_get(key)
|
|
509
|
-
result = resp_command("GET", key)
|
|
510
|
-
return nil if result.nil?
|
|
511
|
-
JSON.parse(result) rescue nil
|
|
512
|
-
end
|
|
513
|
-
|
|
514
|
-
def resp_command(*args)
|
|
515
|
-
parsed = parse_redis_url(@cache_url)
|
|
516
|
-
cmd = "*#{args.size}\r\n"
|
|
517
|
-
args.each { |arg| s = arg.to_s; cmd += "$#{s.bytesize}\r\n#{s}\r\n" }
|
|
518
|
-
|
|
519
|
-
sock = TCPSocket.new(parsed[:host], parsed[:port])
|
|
520
|
-
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, [5, 0].pack("l_2"))
|
|
521
|
-
if parsed[:db] > 0
|
|
522
|
-
select_cmd = "*2\r\n$6\r\nSELECT\r\n$#{parsed[:db].to_s.bytesize}\r\n#{parsed[:db]}\r\n"
|
|
523
|
-
sock.write(select_cmd)
|
|
524
|
-
sock.recv(1024)
|
|
525
|
-
end
|
|
526
|
-
sock.write(cmd)
|
|
527
|
-
response = sock.recv(65536)
|
|
528
|
-
sock.close
|
|
529
|
-
|
|
530
|
-
if response.start_with?("+")
|
|
531
|
-
response[1..].strip
|
|
532
|
-
elsif response.start_with?("$-1")
|
|
533
|
-
nil
|
|
534
|
-
elsif response.start_with?("$")
|
|
535
|
-
lines = response.split("\r\n")
|
|
536
|
-
lines[1]
|
|
537
|
-
elsif response.start_with?(":")
|
|
538
|
-
response[1..].strip
|
|
539
405
|
else
|
|
540
|
-
|
|
541
|
-
end
|
|
542
|
-
rescue StandardError
|
|
543
|
-
nil
|
|
544
|
-
end
|
|
545
|
-
|
|
546
|
-
# ── File operations ────────────────────────────────────────
|
|
547
|
-
|
|
548
|
-
def file_key_path(key)
|
|
549
|
-
require "digest"
|
|
550
|
-
safe = Digest::SHA256.hexdigest(key)
|
|
551
|
-
File.join(@cache_dir, "#{safe}.json")
|
|
552
|
-
end
|
|
553
|
-
|
|
554
|
-
def file_get(key)
|
|
555
|
-
path = file_key_path(key)
|
|
556
|
-
return nil unless File.exist?(path)
|
|
557
|
-
begin
|
|
558
|
-
data = JSON.parse(File.read(path))
|
|
559
|
-
if data["expires_at"] && Time.now.to_f > data["expires_at"]
|
|
560
|
-
File.delete(path) rescue nil
|
|
561
|
-
return nil
|
|
562
|
-
end
|
|
563
|
-
data
|
|
564
|
-
rescue StandardError
|
|
565
|
-
nil
|
|
406
|
+
@backend.set(key, entry, ttl)
|
|
566
407
|
end
|
|
567
408
|
end
|
|
568
409
|
|
|
569
|
-
def
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
while files.size >= @max_entries
|
|
573
|
-
File.delete(files.shift) rescue nil
|
|
574
|
-
end
|
|
575
|
-
path = file_key_path(key)
|
|
576
|
-
File.write(path, JSON.generate(entry))
|
|
577
|
-
rescue StandardError
|
|
578
|
-
end
|
|
579
|
-
|
|
580
|
-
def file_delete(key)
|
|
581
|
-
path = file_key_path(key)
|
|
582
|
-
if File.exist?(path)
|
|
583
|
-
File.delete(path) rescue nil
|
|
584
|
-
true
|
|
410
|
+
def backend_delete(key)
|
|
411
|
+
if memory_backend?
|
|
412
|
+
@mutex.synchronize { !@store.delete(key).nil? }
|
|
585
413
|
else
|
|
586
|
-
|
|
414
|
+
@backend.delete(key)
|
|
587
415
|
end
|
|
588
416
|
end
|
|
589
417
|
end
|
data/lib/tina4/version.rb
CHANGED
data/lib/tina4.rb
CHANGED
|
@@ -51,6 +51,7 @@ require_relative "tina4/dev_mailbox"
|
|
|
51
51
|
require_relative "tina4/ai"
|
|
52
52
|
require_relative "tina4/cache"
|
|
53
53
|
require_relative "tina4/sql_translation"
|
|
54
|
+
require_relative "tina4/cache_backends"
|
|
54
55
|
require_relative "tina4/response_cache"
|
|
55
56
|
require_relative "tina4/html_element"
|
|
56
57
|
require_relative "tina4/error_overlay"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.13.
|
|
4
|
+
version: 3.13.24
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
@@ -206,6 +206,20 @@ dependencies:
|
|
|
206
206
|
- - "~>"
|
|
207
207
|
- !ruby/object:Gem::Version
|
|
208
208
|
version: '3.8'
|
|
209
|
+
- !ruby/object:Gem::Dependency
|
|
210
|
+
name: mongo
|
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
|
212
|
+
requirements:
|
|
213
|
+
- - "~>"
|
|
214
|
+
- !ruby/object:Gem::Version
|
|
215
|
+
version: '2.19'
|
|
216
|
+
type: :development
|
|
217
|
+
prerelease: false
|
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
219
|
+
requirements:
|
|
220
|
+
- - "~>"
|
|
221
|
+
- !ruby/object:Gem::Version
|
|
222
|
+
version: '2.19'
|
|
209
223
|
- !ruby/object:Gem::Dependency
|
|
210
224
|
name: pg
|
|
211
225
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -283,6 +297,15 @@ files:
|
|
|
283
297
|
- lib/tina4/auto_crud.rb
|
|
284
298
|
- lib/tina4/background.rb
|
|
285
299
|
- lib/tina4/cache.rb
|
|
300
|
+
- lib/tina4/cache_backends.rb
|
|
301
|
+
- lib/tina4/cache_backends/base_backend.rb
|
|
302
|
+
- lib/tina4/cache_backends/database_backend.rb
|
|
303
|
+
- lib/tina4/cache_backends/file_backend.rb
|
|
304
|
+
- lib/tina4/cache_backends/memcached_backend.rb
|
|
305
|
+
- lib/tina4/cache_backends/memory_backend.rb
|
|
306
|
+
- lib/tina4/cache_backends/mongo_backend.rb
|
|
307
|
+
- lib/tina4/cache_backends/redis_backend.rb
|
|
308
|
+
- lib/tina4/cache_backends/valkey_backend.rb
|
|
286
309
|
- lib/tina4/cli.rb
|
|
287
310
|
- lib/tina4/constants.rb
|
|
288
311
|
- lib/tina4/container.rb
|