feat(Database::Videos): built-in video cache and support for multiple caching backends
Some checks failed
Invidious CI / build (push) Failing after 38s

I did this to get rid of Redis compatible DBs and for speed purposes.
This is considered experimental, but everything works fine from
what I have tested.

Here are some benchmarks using the built-in benchmark library of
crystal:

\# built-in release
cache get  19.79M ( 50.54ns) (± 4.12%)  32.0B/op  fastest
cache insert   7.88k (126.86µs) (± 2.20%)  65.5kB/op  fastest
cache get   4.31k (232.11µs) (± 5.50%)  104kB/op  fastest

\# redis release
cache get  22.27k ( 44.90µs) (± 6.40%)  264B/op  fastest
cache insert   4.74k (211.01µs) (± 4.72%)  65.7kB/op  fastest
cache get   2.51k (399.11µs) (±13.15%)  129kB/op  fastest

---

OP/s are way higher, and memory usage per call is lower, so it's a win
win.
This commit is contained in:
Fijxu 2025-02-26 17:51:54 -03:00
parent 62cc10d2ca
commit e76867aaba
Signed by: Fijxu
GPG key ID: 32C1DDF333EDA6A4
3 changed files with 164 additions and 46 deletions

View file

@ -156,19 +156,6 @@ end
OUTPUT = CONFIG.output.upcase == "STDOUT" ? STDOUT : File.open(CONFIG.output, mode: "a")
LOGGER = Invidious::LogHandler.new(OUTPUT, CONFIG.log_level, CONFIG.colorize_logs)
REDIS_DB = begin
LOGGER.info "Connecting to Redis compatible DB"
redis = Redis::PooledClient.new(unixsocket: CONFIG.redis_socket || nil, url: CONFIG.redis_url || nil)
if redis.ping
LOGGER.info "Connected to Redis compatible DB via unix domain socket at '#{CONFIG.redis_socket}'" if CONFIG.redis_socket
LOGGER.info "Connected to Redis compatible DB via TCP socket at '#{CONFIG.redis_url}'" if CONFIG.redis_url
end
redis
rescue ex
LOGGER.error "Failed to connect to a Redis compatible DB. Invidious will store the video cache on the PostgresSQL DB"
nil
end
# Check table integrity
Invidious::Database.check_integrity(CONFIG)

View file

@ -217,7 +217,16 @@ class Config
property tokens_server : String = ""
property video_cache : Bool = true
property video_cache : VideoCacheConfig
class VideoCacheConfig
include YAML::Serializable
property enabled : Bool = true
property backend : Int32 = 1
# Max quantity of keys that can be held on the LRU cache
property lru_max_size : Int32 = 18432 # ~512MB
end
{% if flag?(:linux) %}
property reload_config_automatically : Bool = true
@ -395,6 +404,16 @@ class Config
end
end
if config.video_cache.enabled
if !config.video_cache.backend.in?(0, 1, 2)
puts "Config: 'video_cache_storage', can only be:"
puts "0 (PostgreSQL)"
puts "1 (Redis compatible DB) (Default)"
puts "2 (In memory LRU)"
exit(1)
end
end
return config
end
end

View file

@ -1,42 +1,63 @@
require "./base.cr"
require "redis"
VideoCache = Invidious::Database::Videos::Cache.new
module Invidious::Database::Videos
module DBCache
extend self
class Cache
def initialize
case CONFIG.video_cache.backend
when 0
@cache = CacheMethods::PostgresSQL.new
when 1
@cache = CacheMethods::Redis_.new
when 2
@cache = CacheMethods::LRU.new
else
LOGGER.debug "Video Cache: Using default cache method to store video cache (PostgreSQL)"
@cache = CacheMethods::PostgresSQL.new
end
end
def set(video : Video, expire_time)
if redis = REDIS_DB
redis.set(video.id, video.info.to_json, expire_time)
redis.set(video.id + ":time", video.updated, expire_time)
else
request = <<-SQL
INSERT INTO videos
VALUES ($1, $2, $3)
ON CONFLICT (id) DO NOTHING
SQL
PG_DB.exec(request, video.id, video.info.to_json, video.updated)
end
@cache.set(video, expire_time)
end
def del(id : String)
if redis = REDIS_DB
redis.del(id)
redis.del(id + ":time")
else
request = <<-SQL
DELETE FROM videos *
WHERE id = $1
SQL
PG_DB.exec(request, id)
end
@cache.del(id)
end
def get(id : String)
if redis = REDIS_DB
info = redis.get(id)
time = redis.get(id + ":time")
return @cache.get(id)
end
end
module CacheMethods
# TODO: Save the cache on a file with a Job
class LRU
@max_size : Int32
@lru = {} of String => String
@access = [] of String
def initialize(@max_size = CONFIG.video_cache.lru_max_size)
LOGGER.info "Video Cache: Using in memory LRU to store video cache"
LOGGER.info "Video Cache, LRU: LRU cache max size set to #{@max_size}"
end
# TODO: Handle expire_time with a Job
def set(video : Video, expire_time)
self[video.id] = video.info.to_json
self[video.id + ":time"] = "#{video.updated}"
end
def del(id : String)
self.delete(id)
self.delete(id + ":time")
end
def get(id : String)
info = self[id]
time = self[id + ":time"]
if info && time
return Video.new({
id: id,
@ -46,7 +67,98 @@ module Invidious::Database::Videos
else
return nil
end
else
end
private def [](key)
if @lru[key]?
@access.delete(key)
@access.push(key)
@lru[key]
else
nil
end
end
private def []=(key, value)
if @lru.size >= @max_size
lru_key = @access.shift
@lru.delete(lru_key)
end
@lru[key] = value
@access.push(key)
end
private def delete(key)
if @lru[key]?
@lru.delete(key)
@access.delete(key)
end
end
end
class Redis_
@redis : Redis::PooledClient
def initialize
@redis = Redis::PooledClient.new(unixsocket: CONFIG.redis_socket || nil, url: CONFIG.redis_url || nil)
LOGGER.info "Video Cache: Using Redis compatible DB to store video cache"
LOGGER.info "Connecting to Redis compatible DB"
if @redis.ping
LOGGER.info "Connected to Redis compatible DB via unix domain socket at '#{CONFIG.redis_socket}'" if CONFIG.redis_socket
LOGGER.info "Connected to Redis compatible DB via TCP socket at '#{CONFIG.redis_url}'" if CONFIG.redis_url
end
end
def set(video : Video, expire_time)
@redis.set(video.id, video.info.to_json, expire_time)
@redis.set(video.id + ":time", video.updated, expire_time)
end
def del(id : String)
@redis.del(id)
@redis.del(id + ":time")
end
def get(id : String)
info = @redis.get(id)
time = @redis.get(id + ":time")
if info && time
return Video.new({
id: id,
info: JSON.parse(info).as_h,
updated: Time.parse(time, "%Y-%m-%d %H:%M:%S %z", Time::Location::UTC),
})
else
return nil
end
end
end
class PostgresSQL
def initialize
LOGGER.info "Video Cache: Using PostgreSQL to store video cache"
end
def set(video : Video, expire_time)
request = <<-SQL
INSERT INTO videos
VALUES ($1, $2, $3)
ON CONFLICT (id) DO NOTHING
SQL
PG_DB.exec(request, video.id, video.info.to_json, video.updated)
end
def del(id)
request = <<-SQL
DELETE FROM videos *
WHERE id = $1
SQL
PG_DB.exec(request, id)
end
def get(id : String) : Video?
request = <<-SQL
SELECT * FROM videos
WHERE id = $1
@ -60,11 +172,11 @@ module Invidious::Database::Videos
extend self
def insert(video : Video)
DBCache.set(video: video, expire_time: 14400) if CONFIG.video_cache
VideoCache.set(video: video, expire_time: 14400) if CONFIG.video_cache.enabled
end
def delete(id)
DBCache.del(id)
VideoCache.del(id)
end
def delete_expired
@ -87,6 +199,6 @@ module Invidious::Database::Videos
end
def select(id : String) : Video?
return DBCache.get(id)
return VideoCache.get(id)
end
end