Fix a worker-blocking Lua socket call and add gzip/caching for static assets
- ops/nginx_conf/targetinfo.lua's wildcard-subdomain lookup fallback used
classic LuaSocket (require("socket.unix")) instead of an OpenResty
cosocket. LuaSocket is blocking, and called from an nginx worker it
stalls the ENTIRE worker — every other in-flight connection on it — for
the round-trip to the Node app. Worse, the Node side never
newline-terminated its response, so the old blocking receive() only ever
returned via its read-timeout-then-partial-read fallback, meaning every
single cache-miss lookup paid a fixed timeout penalty while blocking the
whole worker. Replaced with an ngx.socket.tcp() cosocket (unix-domain via
"unix:/path", the only cosocket API this lua-nginx-module ships) and
newline-terminated the Node service's responses so receive() actually
completes instead of timing out. Verified against a live container:
previously this crashed OpenResty's Lua VM entirely
(ngx.socket.unix doesn't exist); fixed version resolves fresh wildcard
subdomains in ~2ms.
- Add gzip compression (`compression` middleware) and far-future
Cache-Control on static assets (7d for vendor libs under
/static-modules, 1h for the app's own /static JS/CSS, which isn't
cache-busted). The admin UI is a traditional multi-page app that loads
~13 separate vendor/app JS+CSS files on every full navigation; previously
none of them were compressed and Cache-Control was `max-age=0` (Express's
default), forcing a revalidation round-trip for every asset on every page
view.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const ejs = require('ejs')
|
const ejs = require('ejs')
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const compression = require('compression');
|
||||||
|
|
||||||
// Set up the express app.
|
// Set up the express app.
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -52,6 +53,13 @@ app.onListen.push(function(){
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Gzip text responses (HTML/JS/CSS/JSON). The admin UI loads ~13 separate,
|
||||||
|
// uncompressed vendor JS/CSS files on every full page navigation (a
|
||||||
|
// traditional multi-page app, not an SPA) — this alone meaningfully cuts
|
||||||
|
// bytes-over-the-wire and perceived load time on a real network, where it
|
||||||
|
// matters far more than on localhost.
|
||||||
|
app.use(compression());
|
||||||
|
|
||||||
// load the JSON parser middleware. Express will parse JSON into native objects
|
// load the JSON parser middleware. Express will parse JSON into native objects
|
||||||
// for any request that has JSON in its content type.
|
// for any request that has JSON in its content type.
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|||||||
Generated
+84
@@ -16,6 +16,7 @@
|
|||||||
"axios": "^1.13.5",
|
"axios": "^1.13.5",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
|
"compression": "^1.8.1",
|
||||||
"ejs": "^6.0.1",
|
"ejs": "^6.0.1",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"express-rate-limit": "^8.5.2",
|
"express-rate-limit": "^8.5.2",
|
||||||
@@ -609,6 +610,60 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/compressible": {
|
||||||
|
"version": "2.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
||||||
|
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": ">= 1.43.0 < 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "3.1.2",
|
||||||
|
"compressible": "~2.0.18",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"negotiator": "~0.6.4",
|
||||||
|
"on-headers": "~1.1.0",
|
||||||
|
"safe-buffer": "5.2.1",
|
||||||
|
"vary": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/negotiator": {
|
||||||
|
"version": "0.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
|
||||||
|
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/content-disposition": {
|
"node_modules/content-disposition": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
||||||
@@ -1569,6 +1624,15 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/on-headers": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/once": {
|
"node_modules/once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
@@ -1754,6 +1818,26 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/safer-buffer": {
|
"node_modules/safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"axios": "^1.13.5",
|
"axios": "^1.13.5",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
|
"compression": "^1.8.1",
|
||||||
"ejs": "^6.0.1",
|
"ejs": "^6.0.1",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"express-rate-limit": "^8.5.2",
|
"express-rate-limit": "^8.5.2",
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const socket = new SocketServerJson({
|
|||||||
let parentHost = Host.lookUp(data['domain']);
|
let parentHost = Host.lookUp(data['domain']);
|
||||||
|
|
||||||
// If we don't have a match, return empty object
|
// If we don't have a match, return empty object
|
||||||
if(!parentHost) return clientSocket.write(JSON.stringify({}));
|
if(!parentHost) return clientSocket.write(JSON.stringify({}) + '\n');
|
||||||
|
|
||||||
// lookUp returns the live #record object stored inside the shared
|
// lookUp returns the live #record object stored inside the shared
|
||||||
// lookup tree. Everything below mutates parentHost (sets
|
// lookup tree. Everything below mutates parentHost (sets
|
||||||
@@ -50,7 +50,7 @@ const socket = new SocketServerJson({
|
|||||||
// subdomain and must not be routed to the wildcard parent.
|
// subdomain and must not be routed to the wildcard parent.
|
||||||
if(parentHost.is_wildcard && !parentHost.wildcard_matchAny
|
if(parentHost.is_wildcard && !parentHost.wildcard_matchAny
|
||||||
&& parentHost.host !== data['domain']){
|
&& parentHost.host !== data['domain']){
|
||||||
return clientSocket.write(JSON.stringify({}));
|
return clientSocket.write(JSON.stringify({}) + '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the matched host belongs to a wildcard domain, set wildcard_parent
|
// If the matched host belongs to a wildcard domain, set wildcard_parent
|
||||||
@@ -66,7 +66,13 @@ const socket = new SocketServerJson({
|
|||||||
parentHost[key] = String(value);
|
parentHost[key] = String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
clientSocket.write(JSON.stringify(parentHost));
|
// Terminate with a newline: the Lua client (ops/nginx_conf/targetinfo.lua)
|
||||||
|
// reads a single line per lookup via a cosocket receive() -- without a
|
||||||
|
// delimiter it would block for the full read timeout on every request
|
||||||
|
// waiting for a newline that never arrives (this was masked before by
|
||||||
|
// blocking LuaSocket's timeout+partial-read behavior, which silently
|
||||||
|
// paid that same timeout on every single lookup).
|
||||||
|
clientSocket.write(JSON.stringify(parentHost) + '\n');
|
||||||
}catch(error){
|
}catch(error){
|
||||||
console.error('services/host_lookup onData error', error);
|
console.error('services/host_lookup onData error', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,39 @@
|
|||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
-- Function to connect to a Unix socket
|
-- Query the Node app's host-lookup service for a domain that missed the
|
||||||
local function connect(path)
|
-- Redis fast path (wildcard subdomains not yet cached, see Host.addCache).
|
||||||
local socket = require("socket.unix")()
|
-- Uses an OpenResty cosocket rather than the classic LuaSocket socket.unix()
|
||||||
assert(socket:settimeout(.1))
|
-- the previous version of this file used: LuaSocket's API is blocking and,
|
||||||
local status, err = pcall(function() assert(socket:connect(path)) end)
|
-- called from an nginx worker, stalls the ENTIRE worker (every other
|
||||||
if status then return true end
|
-- in-flight connection on it) for the round-trip -- a real source of
|
||||||
return false
|
-- intermittent request latency for any wildcard host whose on-demand cache
|
||||||
|
-- entry (1h TTL, conf.cacheTTL) had expired. resty.redis (used just above)
|
||||||
|
-- is cosocket-based already and works fine from both the phases this module
|
||||||
|
-- is called from (access_by_lua_block and the SSL request_domain callback),
|
||||||
|
-- so a unix-domain cosocket is safe here too.
|
||||||
|
local function unixLookup(json, domain)
|
||||||
|
-- The ngx_lua cosocket API has no separate ngx.socket.unix -- a plain
|
||||||
|
-- ngx.socket.tcp() connects to a unix domain socket when given a
|
||||||
|
-- "unix:/path" address instead of a host/port pair.
|
||||||
|
local sock = ngx.socket.tcp()
|
||||||
|
sock:settimeouts(100, 100, 100) -- connect, send, read (ms)
|
||||||
|
|
||||||
|
local ok = sock:connect("unix:/var/run/proxy_lookup.socket")
|
||||||
|
if not ok then return nil end
|
||||||
|
|
||||||
|
local ok = sock:send(json.encode({domain = domain}))
|
||||||
|
if not ok then
|
||||||
|
sock:close()
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local line = sock:receive()
|
||||||
|
sock:close()
|
||||||
|
if not line then return nil end
|
||||||
|
|
||||||
|
local decodeOk, decoded = pcall(json.decode, line)
|
||||||
|
if not decodeOk then return nil end
|
||||||
|
return decoded
|
||||||
end
|
end
|
||||||
|
|
||||||
print("In targetInfo module")
|
print("In targetInfo module")
|
||||||
@@ -61,20 +88,7 @@ function M.get(ngx, domain, targetInfo)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if not res["ip"] then
|
if not res["ip"] then
|
||||||
if connect("/var/run/proxy_lookup.socket") then
|
res = unixLookup(json, domain) or res
|
||||||
local socket = require("socket.unix")()
|
|
||||||
assert(socket:settimeout(.1))
|
|
||||||
assert(socket:connect("/var/run/proxy_lookup.socket"))
|
|
||||||
assert(socket:send(json.encode({domain = domain})))
|
|
||||||
while true do
|
|
||||||
local s, status, partial = socket:receive()
|
|
||||||
if partial then
|
|
||||||
res = json.decode(partial)
|
|
||||||
socket:close()
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if not res["ip"] then
|
if not res["ip"] then
|
||||||
|
|||||||
Reference in New Issue
Block a user