From a03e923e74f0fb587f6c189a373e1f218c893b88 Mon Sep 17 00:00:00 2001 From: Jonathan Roemer Date: Tue, 22 Sep 2026 13:25:37 -0500 Subject: [PATCH] Return redis connections to the worker pool pool = 5 was already set in DbSettings and unused. set_keepalive puts the socket back for the next request instead of dropping it, and SELECT then only runs on a new one. One pool per database keeps dev, test and prod apart. Release happens around the action because gin turns raise_error into a response through pcall, and nginx closes cosockets before log_by_lua. --- app/controllers/1/syncs_controller.lua | 32 ++++++++++++++++++++++++++ db/redis.lua | 31 +++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/app/controllers/1/syncs_controller.lua b/app/controllers/1/syncs_controller.lua index b4d0b1a..e26ad2a 100644 --- a/app/controllers/1/syncs_controller.lua +++ b/app/controllers/1/syncs_controller.lua @@ -295,4 +295,36 @@ function SyncsController:healthcheck() return 200, { state = 'OK' } end +-- gin dispatches straight to the action and turns raise_error into a response +-- with pcall, so an action has no return point every path passes through. The +-- connection goes back to the pool here, inside the content phase: nginx +-- finalizes cosockets before log_by_lua runs, where set_keepalive fails with +-- "closed". +local function releasing(action) + return function(self, ...) + local ok, status, body, headers = pcall(action, self, ...) + if self.redis then + Redis.release(self.redis) + self.redis = nil + end + if not ok then + error(status, 0) + end + return status, body, headers + end +end + +for _, action in ipairs({ + "auth_user", + "create_user", + "create_user_disabled", + "delete_user", + "update_password", + "get_progress", + "update_progress", + "healthcheck" +}) do + SyncsController[action] = releasing(SyncsController[action]) +end + return SyncsController diff --git a/db/redis.lua b/db/redis.lua index c8f2378..d7b2943 100644 --- a/db/redis.lua +++ b/db/redis.lua @@ -29,16 +29,43 @@ local Redis = { options = {}, } +-- How long an idle connection is held. config/redis.conf sets `timeout 0`, so +-- the server never closes one first. +local keepalive_timeout = 60000 + +-- One pool per database. A reused connection is the same socket, so it is +-- already on the database it selected, and connections that selected another +-- are in another pool. port is formatted, not concatenated: a unix socket +-- configuration leaves it unset. +local function pool_name(option) + return string.format("%s:%s/%s", option.host, tostring(option.port), option.database) +end + function Redis:new() local redis = require("resty.redis") local option = DbSettings[Gin.env] local red = redis:new() red:set_timeout(1000) -- 1 sec - local ok, err = red:connect(option.host, option.port) + local ok, err = red:connect(option.host, option.port, { + pool = pool_name(option), + pool_size = option.pool + }) if ok then - red:select(option.database) + -- Only a new socket needs it; see pool_name above. + if red:get_reused_times() == 0 then + red:select(option.database) + end return red end end +-- Returns the connection to the pool. set_keepalive refuses a socket with an +-- unread reply or a read or write error, and close is the fallback. +function Redis.release(red) + local option = DbSettings[Gin.env] + if not red:set_keepalive(keepalive_timeout, option.pool) then + red:close() + end +end + return Redis