HEX
Server: Apache/2
System: Linux nexus-01 4.18.0-553.120.1.el8_10.x86_64 #1 SMP Mon Apr 20 18:04:27 EDT 2026 x86_64
User: aglcoke (1118)
PHP: 8.2.31
Disabled: mail,exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname
Upload Files
File: //usr/share/rspamd/lualib/lua_fuzzy_redis.lua
--[[
Copyright (c) 2026, Vsevolod Stakhov <vsevolod@rspamd.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]

-- Lua module for fuzzy Redis backend update path (following the Bayes pattern)

local exports = {}
local lua_redis = require "lua_redis"
local logger = require "rspamd_logger"

local N = "fuzzy_redis"

local function gen_update_functor(redis_params, update_script_id)
  -- Returns function(ev_base, prefix, updates, src, expire, callback)
  -- updates is an array of tables: {op, digest, flag, value, is_weak, shingle_keys}
  -- callback(success_boolean) is called when all operations complete
  return function(ev_base, prefix, updates, src, expire, callback)
    local n_ops = 0
    local n_completed = 0
    local has_error = false
    local count_key = prefix .. "_count"

    -- Count actual operations (skip "dup")
    for _, upd in ipairs(updates) do
      if upd.op ~= "dup" then
        n_ops = n_ops + 1
      end
    end

    -- Final step: INCR version key and invoke callback
    local function do_version_incr()
      local version_key = prefix .. src

      local function version_cb(err, _)
        if err then
          logger.errx(rspamd_config, '%s: version INCR failed for %s: %s',
              N, version_key, err)
        end
        callback(not has_error)
      end

      if not lua_redis.redis_make_request_taskless(ev_base, rspamd_config,
          redis_params, version_key, true, version_cb, 'INCR', { version_key }) then
        logger.errx(rspamd_config, '%s: cannot make version INCR request', N)
        callback(false)
      end
    end

    -- Called when one exec_redis_script completes
    local function on_op_complete()
      n_completed = n_completed + 1
      if n_completed >= n_ops then
        do_version_incr()
      end
    end

    -- If no actual operations, just do version INCR
    if n_ops == 0 then
      do_version_incr()
      return
    end

    for _, upd in ipairs(updates) do
      if upd.op ~= "dup" then
        local hash_key = prefix .. upd.digest

        -- Build KEYS array (only actual Redis key names) and ARGV (parameters)
        local keys = {
          hash_key,
          count_key,
        }

        -- Append shingle keys if present
        if upd.shingle_keys then
          for _, sk in ipairs(upd.shingle_keys) do
            keys[#keys + 1] = sk
          end
        end

        local args = {
          upd.op,
          tostring(upd.flag),
          tostring(upd.value),
          tostring(expire),
          tostring(upd.timestamp),
          tostring(upd.is_weak),
          upd.digest,
        }

        local function update_cb(err, _)
          if err then
            logger.errx(rspamd_config, '%s: update script failed: %s', N, err)
            has_error = true
          end
          on_op_complete()
        end

        lua_redis.exec_redis_script(update_script_id,
            { ev_base = ev_base, is_write = true, key = hash_key },
            update_cb, keys, args)
      end
    end
  end
end

-- Initialize fuzzy Redis update module
-- @param redis_params table returned by lua_redis.try_load_redis_servers
-- @return update functor or nil on error
exports.lua_fuzzy_redis_init = function(redis_params)
  if not redis_params then
    logger.errx(rspamd_config, '%s: no redis params provided', N)
    return nil
  end

  local update_script_id, err = lua_redis.load_redis_script_from_file(
      "fuzzy_update.lua", redis_params)
  if not update_script_id then
    logger.errx(rspamd_config, '%s: cannot load fuzzy_update.lua: %s', N,
        err or "unknown error")
    return nil
  end

  return gen_update_functor(redis_params, update_script_id)
end

return exports