blob: 00518d984e0d6737aa1f66887ad7261da955fa13 [file]
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you 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.
*/
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <getopt.h>
#include <inttypes.h>
#include <pthread.h>
#include <atomic>
#include <chrono>
#include <thread>
#include <vector>
#include "ts_lua_util.h"
#include "tsutil/StringCompare.h"
extern "C" {
#include "luajit.h"
} // extern "C"
#define TS_LUA_MAX_STATE_COUNT 256
#define TS_LUA_STATS_TIMEOUT 5000 // 5s -- convert to configurable
#define TS_LUA_STATS_BUFFER_SIZE 10 // stats buffer
#define TS_LUA_IND_STATE 0
#define TS_LUA_IND_GC_BYTES 1
#define TS_LUA_IND_THREADS 2
#define TS_LUA_IND_SIZE 3
static uint64_t ts_lua_http_next_id = 0;
static uint64_t ts_lua_g_http_next_id = 0;
static ts_lua_main_ctx *ts_lua_main_ctx_array = nullptr;
static ts_lua_main_ctx *ts_lua_g_main_ctx_array = nullptr;
static pthread_key_t lua_g_state_key;
static pthread_key_t lua_state_key;
// records.yaml entry injected by plugin
static char const *const ts_lua_mgmt_state_str = "proxy.config.plugin.lua.max_states";
static char const *const ts_lua_mgmt_state_regex = "^[1-9][0-9]*$";
// this is set the first time global configuration is probed.
static int ts_lua_max_state_count = 0;
// Set once the shutdown barrier owns every Lua state mutex and keeps them. Read by
// TSRemapDeleteInstance, on the event thread that continues shutting ATS down.
static std::atomic<bool> shutdown_barrier_engaged{false};
// lifecycle message tag
static char const *const print_tag = "stats_print";
static char const *const reset_tag = "stats_reset";
// stat record strings
static char const *const ts_lua_stat_strs[] = {
"plugin.lua.remap.states",
"plugin.lua.remap.gc_bytes",
"plugin.lua.remap.threads",
nullptr,
};
static char const *const ts_lua_g_stat_strs[] = {
"plugin.lua.global.states",
"plugin.lua.global.gc_bytes",
"plugin.lua.global.threads",
nullptr,
};
struct ts_lua_plugin_stats {
ts_lua_main_ctx *main_ctx_array;
TSMgmtInt gc_kb; // last collected gc in kb
TSMgmtInt threads; // last collected number active threads
int stat_inds[TS_LUA_IND_SIZE]; // stats indices
};
ts_lua_plugin_stats *
create_plugin_stats(ts_lua_main_ctx *const main_ctx_array, char const *const *stat_strs)
{
auto *const stats = TSRalloc<ts_lua_plugin_stats>();
memset(stats, 0, sizeof(ts_lua_plugin_stats));
stats->main_ctx_array = main_ctx_array;
// sample buffers
stats->gc_kb = 0;
stats->threads = 0;
int const max_state_count = ts_lua_max_state_count;
for (int ind = 0; ind < TS_LUA_IND_SIZE; ++ind) {
stats->stat_inds[ind] = TSStatCreate(stat_strs[ind], TS_RECORDDATATYPE_INT, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM);
}
// initialize the number of states stat
int const sid = stats->stat_inds[TS_LUA_IND_STATE];
if (TS_ERROR != sid) {
TSStatIntSet(sid, max_state_count);
}
return stats;
}
ts_lua_main_ctx *
create_lua_vms()
{
ts_lua_main_ctx *ctx_array = nullptr;
// Inject the setting into records.yaml
static bool ts_mgt_int_inserted = false;
if (!ts_mgt_int_inserted) {
if (TS_SUCCESS == TSMgmtIntCreate(TS_RECORDTYPE_CONFIG, ts_lua_mgmt_state_str, TS_LUA_MAX_STATE_COUNT,
TS_RECORDUPDATE_RESTART_TS, TS_RECORDCHECK_INT, ts_lua_mgmt_state_regex,
TS_RECORDACCESS_READ_ONLY)) {
Dbg(dbg_ctl, "[%s] registered config string %s: with default [%d]", __FUNCTION__, ts_lua_mgmt_state_str,
TS_LUA_MAX_STATE_COUNT);
} else {
TSError("[%s][%s] failed to register %s", TS_LUA_DEBUG_TAG, __FUNCTION__, ts_lua_mgmt_state_str);
}
ts_mgt_int_inserted = true;
}
if (0 == ts_lua_max_state_count) {
TSMgmtInt mgmt_state = 0;
if (TS_SUCCESS != TSMgmtIntGet(ts_lua_mgmt_state_str, &mgmt_state)) {
Dbg(dbg_ctl, "[%s] setting max state to default: %d", __FUNCTION__, TS_LUA_MAX_STATE_COUNT);
ts_lua_max_state_count = TS_LUA_MAX_STATE_COUNT;
} else {
ts_lua_max_state_count = (int)mgmt_state;
Dbg(dbg_ctl, "[%s] found %s: [%d]", __FUNCTION__, ts_lua_mgmt_state_str, ts_lua_max_state_count);
}
if (ts_lua_max_state_count < 1) {
TSError("[ts_lua][%s] invalid %s: %d", __FUNCTION__, ts_lua_mgmt_state_str, ts_lua_max_state_count);
ts_lua_max_state_count = 0;
return nullptr;
}
}
ctx_array = TSRalloc<ts_lua_main_ctx>(ts_lua_max_state_count);
memset(ctx_array, 0, sizeof(ts_lua_main_ctx) * ts_lua_max_state_count);
int const ret = ts_lua_create_vm(ctx_array, ts_lua_max_state_count);
if (ret) {
ts_lua_destroy_vm(ctx_array, ts_lua_max_state_count);
TSfree(ctx_array);
ctx_array = nullptr;
return nullptr;
}
// Initialize the GC numbers, no need to lock here
for (int index = 0; index < ts_lua_max_state_count; ++index) {
ts_lua_main_ctx *const main_ctx = (ctx_array + index);
lua_State *const lstate = main_ctx->lua;
ts_lua_ctx_stats *const stats = main_ctx->stats;
stats->gc_kb = stats->gc_kb_max = lua_getgccount(lstate);
}
return ctx_array;
}
// dump exhaustive per state summary stats
static void
collectStats(ts_lua_plugin_stats *const plugin_stats)
{
TSMgmtInt gc_kb_total = 0;
TSMgmtInt threads_total = 0;
ts_lua_main_ctx *const main_ctx_array = plugin_stats->main_ctx_array;
// aggregate stats on the states
for (int index = 0; index < ts_lua_max_state_count; ++index) {
ts_lua_main_ctx *const main_ctx = (main_ctx_array + index);
if (nullptr != main_ctx) {
ts_lua_ctx_stats *const stats = main_ctx->stats;
TSMutexLock(stats->mutexp);
gc_kb_total += stats->gc_kb;
threads_total += stats->threads;
TSMutexUnlock(stats->mutexp);
}
}
// set the stats sample slot
plugin_stats->gc_kb = gc_kb_total;
plugin_stats->threads = threads_total;
}
static void
publishStats(ts_lua_plugin_stats *const plugin_stats)
{
TSMgmtInt const gc_bytes = plugin_stats->gc_kb * 1024;
TSStatIntSet(plugin_stats->stat_inds[TS_LUA_IND_GC_BYTES], gc_bytes);
TSStatIntSet(plugin_stats->stat_inds[TS_LUA_IND_THREADS], plugin_stats->threads);
}
// dump exhaustive per state summary stats
static int
statsHandler(TSCont contp, TSEvent /* event ATS_UNUSED */, void * /* edata ATS_UNUSED */)
{
ts_lua_plugin_stats *const plugin_stats = (ts_lua_plugin_stats *)TSContDataGet(contp);
collectStats(plugin_stats);
publishStats(plugin_stats);
TSContScheduleOnPool(contp, TS_LUA_STATS_TIMEOUT, TS_THREAD_POOL_TASK);
return TS_EVENT_NONE;
}
static void
get_time_now_str(char *const buf, size_t const buflen)
{
TSHRTime const timenowusec = TShrtime();
int64_t const timemsec = (int64_t)(timenowusec / 1000000);
time_t const timesec = (time_t)(timemsec / 1000);
int const ms = (int)(timemsec % 1000);
struct tm tm;
gmtime_r(&timesec, &tm);
size_t const dtlen = strftime(buf, buflen, "%b %e %H:%M:%S", &tm);
// tack on the ms
snprintf(buf + dtlen, buflen - dtlen, ".%03d", ms);
}
// dump exhaustive per state summary stats
static int
lifecycleHandler(TSCont contp, TSEvent /* event ATS_UNUSED */, void *edata)
{
// ensure the message is for ts_lua
TSPluginMsg *const msgp = (TSPluginMsg *)edata;
if (!ts::iequals(msgp->tag, TS_LUA_DEBUG_TAG)) {
return TS_EVENT_NONE;
}
ts_lua_main_ctx *const main_ctx_array = (ts_lua_main_ctx *)TSContDataGet(contp);
static char const *const remapstr = "remap";
static char const *const globalstr = "global";
char const *labelstr = nullptr;
if (main_ctx_array == ts_lua_main_ctx_array) {
labelstr = remapstr;
} else {
labelstr = globalstr;
}
char timebuf[128];
get_time_now_str(timebuf, 128);
char const *const msgstr = (char *)msgp->data;
enum State { Print, Reset } state;
state = Print;
size_t const reset_tag_len = strlen(reset_tag);
if (reset_tag_len <= msgp->data_size && 0 == strncasecmp(reset_tag, msgstr, reset_tag_len)) {
Dbg(dbg_ctl, "[%s] LIFECYCLE_MSG: %s", __FUNCTION__, reset_tag);
state = Reset;
fprintf(stderr, "[%s] %s (%s) resetting per state gc_kb_max and threads_max\n", timebuf, TS_LUA_DEBUG_TAG, labelstr);
} else {
Dbg(dbg_ctl, "[%s] LIFECYCLE_MSG: %s", __FUNCTION__, print_tag);
}
for (int index = 0; index < ts_lua_max_state_count; ++index) {
ts_lua_main_ctx *const main_ctx = (main_ctx_array + index);
if (nullptr != main_ctx) {
ts_lua_ctx_stats *const stats = main_ctx->stats;
if (nullptr != main_ctx) {
TSMutexLock(stats->mutexp);
switch (state) {
case Reset:
stats->threads_max = stats->threads;
stats->gc_kb_max = stats->gc_kb;
break;
case Print:
default:
fprintf(stderr, "[%s] %s (%s) id: %3d gc_kb: %6d gc_kb_max: %6d threads: %4d threads_max: %4d\n", timebuf,
TS_LUA_DEBUG_TAG, labelstr, index, stats->gc_kb, stats->gc_kb_max, stats->threads, stats->threads_max);
break;
}
TSMutexUnlock(stats->mutexp);
}
}
}
return TS_EVENT_NONE;
}
TSReturnCode
TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
{
if (!api_info || api_info->size < sizeof(TSRemapInterface)) {
strncpy(errbuf, "[TSRemapInit] - Incorrect size of TSRemapInterface structure", errbuf_size - 1);
errbuf[errbuf_size - 1] = '\0';
return TS_ERROR;
}
if (nullptr == ts_lua_main_ctx_array) {
ts_lua_main_ctx_array = create_lua_vms();
if (nullptr != ts_lua_main_ctx_array) {
pthread_key_create(&lua_state_key, nullptr);
TSCont const lcontp = TSContCreate(lifecycleHandler, TSMutexCreate());
TSContDataSet(lcontp, ts_lua_main_ctx_array);
TSLifecycleHookAdd(TS_LIFECYCLE_MSG_HOOK, lcontp);
ts_lua_plugin_stats *const plugin_stats = create_plugin_stats(ts_lua_main_ctx_array, ts_lua_stat_strs);
// start the stats management
if (nullptr != plugin_stats) {
Dbg(dbg_ctl, "Starting up stats management continuation");
TSCont const scontp = TSContCreate(statsHandler, TSMutexCreate());
TSContDataSet(scontp, plugin_stats);
TSContScheduleOnPool(scontp, TS_LUA_STATS_TIMEOUT, TS_THREAD_POOL_TASK);
}
} else {
return TS_ERROR;
}
}
return TS_SUCCESS;
}
TSReturnCode
TSRemapNewInstance(int argc, char *argv[], void **ih, char *errbuf, int errbuf_size)
{
int ret;
char script[TS_LUA_MAX_SCRIPT_FNAME_LENGTH] = "";
char const *inline_script = "";
int fn = 0;
int states = ts_lua_max_state_count;
int ljgc = 0;
int jit = 1;
static const struct option longopt[] = {
{"states", required_argument, 0, 's'},
{"jit", required_argument, 0, 'j'},
{"inline", required_argument, 0, 'i'},
{"ljgc", required_argument, 0, 'g'},
{0, 0, 0, 0 },
};
argc--;
argv++;
for (;;) {
int opt;
opt = getopt_long(argc, (char *const *)argv, "", longopt, nullptr);
switch (opt) {
case 's':
states = atoi(optarg);
Dbg(dbg_ctl, "[%s] setting number of lua VMs [%d]", __FUNCTION__, states);
// set state
break;
case 'j':
jit = atoi(optarg);
if (jit == 0) {
Dbg(dbg_ctl, "[%s] disable JIT mode for remap plugin", __FUNCTION__);
for (int index = 0; index < ts_lua_max_state_count; ++index) {
ts_lua_main_ctx *const main_ctx = (ts_lua_main_ctx_array + index);
lua_State *const lstate = main_ctx->lua;
if (luaJIT_setmode(lstate, 0, LUAJIT_MODE_ENGINE | LUAJIT_MODE_OFF) == 0) {
TSError("[ts_lua][%s] Failed to disable JIT mode for remap plugin", __FUNCTION__);
}
}
}
break;
case 'i':
inline_script = optarg;
break;
case 'g':
ljgc = atoi(optarg);
break;
}
if (opt == -1) {
break;
}
}
if (states < 1 || ts_lua_max_state_count < states) {
snprintf(errbuf, errbuf_size, "[TSRemapNewInstance] - invalid state in option input. Must be between 1 and %d",
ts_lua_max_state_count);
return TS_ERROR;
}
if (argc - optind > 0) {
fn = 1;
if (argv[optind][0] == '/') {
snprintf(script, sizeof(script), "%s", argv[optind]);
} else {
snprintf(script, sizeof(script), "%s/%s", TSConfigDirGet(), argv[optind]);
}
}
if (strlen(inline_script) == 0 && argc - optind < 1) {
strncpy(errbuf, "[TSRemapNewInstance] - lua script file or string is required !!", errbuf_size - 1);
errbuf[errbuf_size - 1] = '\0';
return TS_ERROR;
}
if (strlen(script) >= TS_LUA_MAX_SCRIPT_FNAME_LENGTH - 16) {
strncpy(errbuf, "[TSRemapNewInstance] - lua script file name too long !!", errbuf_size - 1);
errbuf[errbuf_size - 1] = '\0';
return TS_ERROR;
}
ts_lua_instance_conf *conf = nullptr;
// check to make sure it is a lua file and there is no parameter for the lua file
if (fn && (argc - optind < 2)) {
Dbg(dbg_ctl, "[%s] checking if script has been registered", __FUNCTION__);
// we only need to check the first lua VM for script registration
TSMutexLock(ts_lua_main_ctx_array[0].mutexp);
conf = ts_lua_script_registered(ts_lua_main_ctx_array[0].lua, script);
TSMutexUnlock(ts_lua_main_ctx_array[0].mutexp);
}
if (!conf) {
Dbg(dbg_ctl, "[%s] creating new conf instance", __FUNCTION__);
conf = TSRalloc<ts_lua_instance_conf>();
if (!conf) {
strncpy(errbuf, "[TSRemapNewInstance] malloc failed!!", errbuf_size - 1);
errbuf[errbuf_size - 1] = '\0';
return TS_ERROR;
}
memset(conf, 0, sizeof(ts_lua_instance_conf));
conf->states = states;
conf->remap = 1;
conf->init_func = 0;
conf->ref_count = 1;
conf->ljgc = ljgc;
Dbg(dbg_ctl, "Reference Count = %d , creating new instance...", conf->ref_count);
if (fn) {
snprintf(conf->script, TS_LUA_MAX_SCRIPT_FNAME_LENGTH, "%s", script);
} else {
conf->content = inline_script;
}
ts_lua_init_instance(conf);
ret = ts_lua_add_module(conf, ts_lua_main_ctx_array, conf->states, argc - optind, &argv[optind], errbuf, errbuf_size);
if (ret != 0) {
return TS_ERROR;
}
// register the script only if it is from a file and has no __init__ function
if (fn && !conf->init_func) {
// we only need to register the script for the first lua VM
TSMutexLock(ts_lua_main_ctx_array[0].mutexp);
ts_lua_script_register(ts_lua_main_ctx_array[0].lua, conf->script, conf);
TSMutexUnlock(ts_lua_main_ctx_array[0].mutexp);
}
} else {
conf->ref_count++;
Dbg(dbg_ctl, "Reference Count = %d , reference existing instance...", conf->ref_count);
}
*ih = conf;
return TS_SUCCESS;
}
void
TSRemapDeleteInstance(void *ih)
{
// Once the shutdown barrier owns the Lua state mutexes it never gives them back,
// so ts_lua_del_module() would block here for good, on the event thread that
// still has the rest of ATS shutdown to run. ATS as it stands does not get here
// during shutdown -- it leaks the remap configuration instead, deliberately,
// because plugin teardown after shutdown is unsafe -- but a hung restart is a bad
// enough outcome to guard against that changing. The process is exiting, so
// leaving the instance alone costs nothing but a __clean__ call.
if (shutdown_barrier_engaged.load(std::memory_order_acquire)) {
Dbg(dbg_ctl, "shutdown barrier engaged, skipping remap instance teardown for '%s'", ((ts_lua_instance_conf *)ih)->script);
return;
}
int states = ((ts_lua_instance_conf *)ih)->states;
ts_lua_del_module((ts_lua_instance_conf *)ih, ts_lua_main_ctx_array, states);
ts_lua_del_instance(static_cast<ts_lua_instance_conf *>(ih));
((ts_lua_instance_conf *)ih)->ref_count--;
if (((ts_lua_instance_conf *)ih)->ref_count == 0) {
Dbg(dbg_ctl, "Reference Count = %d , freeing...", ((ts_lua_instance_conf *)ih)->ref_count);
TSfree(ih);
} else {
Dbg(dbg_ctl, "Reference Count = %d , not freeing...", ((ts_lua_instance_conf *)ih)->ref_count);
}
return;
}
static TSRemapStatus
ts_lua_remap_plugin_init(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
{
int ret;
uint64_t req_id;
TSCont contp;
lua_State *L;
ts_lua_main_ctx *main_ctx;
ts_lua_http_ctx *http_ctx;
ts_lua_cont_info *ci;
ts_lua_instance_conf *instance_conf;
int remap = (rri == nullptr ? 0 : 1);
instance_conf = (ts_lua_instance_conf *)ih;
main_ctx = static_cast<decltype(main_ctx)>(pthread_getspecific(lua_state_key));
if (main_ctx == nullptr || static_cast<int>(main_ctx - ts_lua_main_ctx_array) >= instance_conf->states) {
req_id = __sync_fetch_and_add(&ts_lua_http_next_id, 1);
main_ctx = &ts_lua_main_ctx_array[req_id % instance_conf->states];
pthread_setspecific(lua_state_key, main_ctx);
}
TSMutexLockGuard lock(main_ctx->mutexp);
http_ctx = ts_lua_create_http_ctx(main_ctx, instance_conf);
http_ctx->txnp = rh;
http_ctx->has_hook = 0;
http_ctx->from_remap = (rri != nullptr) ? 1 : 0;
http_ctx->rri = rri;
if (rri != nullptr) {
http_ctx->client_request_bufp = rri->requestBufp;
http_ctx->client_request_hdrp = rri->requestHdrp;
http_ctx->client_request_url = rri->requestUrl;
}
ci = &http_ctx->cinfo;
L = ci->routine.lua;
contp = TSContCreate(ts_lua_http_cont_handler, nullptr);
TSContDataSet(contp, http_ctx);
ci->contp = contp;
ci->mutex = TSContMutexGet((TSCont)rh);
lua_getglobal(L, (remap ? TS_LUA_FUNCTION_REMAP : TS_LUA_FUNCTION_OS_RESPONSE));
if (lua_type(L, -1) != LUA_TFUNCTION) {
lua_pop(L, 1);
ts_lua_destroy_http_ctx(http_ctx);
return TSREMAP_NO_REMAP;
}
ts_lua_set_cont_info(L, nullptr);
if (lua_pcall(L, 0, 1, 0) != 0) {
TSError("[ts_lua][%s] lua_pcall failed: %s", __FUNCTION__, lua_tostring(L, -1));
ret = TSREMAP_NO_REMAP;
} else {
ret = lua_tointeger(L, -1);
}
lua_pop(L, 1);
// rri lives on the caller's stack; clear it so post-remap hooks
// see ts.remap.* return nil per the documented do_remap-only
// context. Destructors gate handle release on from_remap, not rri.
http_ctx->rri = nullptr;
if (http_ctx->has_hook) {
Dbg(dbg_ctl, "[%s] has txn hook -> adding txn close hook handler to release resources", __FUNCTION__);
TSHttpTxnHookAdd(rh, TS_HTTP_TXN_CLOSE_HOOK, contp);
} else {
Dbg(dbg_ctl, "[%s] no txn hook -> release resources now", __FUNCTION__);
ts_lua_destroy_http_ctx(http_ctx);
}
return TSRemapStatus(ret);
}
void
TSRemapOSResponse(void *ih, TSHttpTxn rh, int os_response_type)
{
Dbg(dbg_ctl, "[%s] os response function and type - %d", __FUNCTION__, os_response_type);
ts_lua_remap_plugin_init(ih, rh, nullptr);
}
TSRemapStatus
TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
{
Dbg(dbg_ctl, "[%s] remap function", __FUNCTION__);
return ts_lua_remap_plugin_init(ih, rh, rri);
}
static int
configHandler(TSCont contp, TSEvent event ATS_UNUSED, void *edata ATS_UNUSED)
{
Dbg(dbg_ctl, "[%s] calling configuration handler", __FUNCTION__);
ts_lua_instance_conf *conf = (ts_lua_instance_conf *)TSContDataGet(contp);
ts_lua_reload_module(conf, ts_lua_g_main_ctx_array, conf->states);
return 0;
}
static int
vconnHookHandler(TSCont contp, TSEvent event, void *edata)
{
TSVConn vconn = (TSVConn)edata;
int ret;
uint64_t req_id;
lua_State *l;
ts_lua_main_ctx *main_ctx;
ts_lua_vconn_ctx *vconn_ctx;
ts_lua_instance_conf *conf = (ts_lua_instance_conf *)TSContDataGet(contp);
main_ctx = static_cast<decltype(main_ctx)>(pthread_getspecific(lua_g_state_key));
if (main_ctx == NULL || static_cast<int>(main_ctx - ts_lua_g_main_ctx_array) >= conf->states) {
req_id = __sync_fetch_and_add(&ts_lua_g_http_next_id, 1);
Dbg(dbg_ctl, "[%s] req_id for vconn handler: %" PRId64, __FUNCTION__, req_id);
main_ctx = &ts_lua_g_main_ctx_array[req_id % conf->states];
pthread_setspecific(lua_g_state_key, main_ctx);
}
TSMutexLock(main_ctx->mutexp);
vconn_ctx = ts_lua_create_vconn_ctx(main_ctx, conf);
vconn_ctx->vconn = vconn;
l = vconn_ctx->lua;
switch (event) {
case TS_EVENT_VCONN_START:
lua_getglobal(l, TS_LUA_FUNCTION_G_VCONN_START);
break;
default:
ts_lua_destroy_vconn_ctx(vconn_ctx);
TSMutexUnlock(main_ctx->mutexp);
TSVConnReenable(vconn);
return 0;
}
if (lua_type(l, -1) != LUA_TFUNCTION) {
lua_pop(l, 1);
ts_lua_destroy_vconn_ctx(vconn_ctx);
TSMutexUnlock(main_ctx->mutexp);
TSVConnReenable(vconn);
return 0;
}
if (lua_pcall(l, 0, 1, 0) != 0) {
TSError("[ts_lua][%s] lua_pcall failed: %s", __FUNCTION__, lua_tostring(l, -1));
}
ret = lua_tointeger(l, -1);
lua_pop(l, 1);
ts_lua_destroy_vconn_ctx(vconn_ctx);
TSMutexUnlock(main_ctx->mutexp);
if (ret) {
TSError("[ts_lua][%s] error returned", __FUNCTION__);
} else {
Dbg(dbg_ctl, "[%s] no error returned", __FUNCTION__);
}
TSVConnReenable(vconn);
return 0;
}
static int
globalHookHandler(TSCont contp, TSEvent event ATS_UNUSED, void *edata)
{
TSHttpTxn txnp = (TSHttpTxn)edata;
TSMBuffer bufp;
TSMLoc hdr_loc;
TSMLoc url_loc;
int ret;
uint64_t req_id;
TSCont txn_contp;
lua_State *l;
ts_lua_main_ctx *main_ctx;
ts_lua_http_ctx *http_ctx;
ts_lua_cont_info *ci;
ts_lua_instance_conf *conf = (ts_lua_instance_conf *)TSContDataGet(contp);
main_ctx = static_cast<decltype(main_ctx)>(pthread_getspecific(lua_g_state_key));
if (main_ctx == nullptr || static_cast<int>(main_ctx - ts_lua_g_main_ctx_array) >= conf->states) {
req_id = __sync_fetch_and_add(&ts_lua_g_http_next_id, 1);
Dbg(dbg_ctl, "[%s] req_id: %" PRId64, __FUNCTION__, req_id);
main_ctx = &ts_lua_g_main_ctx_array[req_id % conf->states];
pthread_setspecific(lua_g_state_key, main_ctx);
}
TSMutexLock(main_ctx->mutexp);
http_ctx = ts_lua_create_http_ctx(main_ctx, conf);
http_ctx->txnp = txnp;
http_ctx->rri = nullptr;
http_ctx->has_hook = 0;
if (!http_ctx->client_request_bufp) {
if (TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc) == TS_SUCCESS) {
http_ctx->client_request_bufp = bufp;
http_ctx->client_request_hdrp = hdr_loc;
if (TSHttpHdrUrlGet(bufp, hdr_loc, &url_loc) == TS_SUCCESS) {
http_ctx->client_request_url = url_loc;
}
}
}
if (!http_ctx->client_request_hdrp) {
ts_lua_destroy_http_ctx(http_ctx);
TSMutexUnlock(main_ctx->mutexp);
TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
return 0;
}
txn_contp = TSContCreate(ts_lua_http_cont_handler, nullptr);
TSContDataSet(txn_contp, http_ctx);
ci = &http_ctx->cinfo;
ci->contp = txn_contp;
ci->mutex = TSContMutexGet((TSCont)txnp);
l = ci->routine.lua;
switch (event) {
case TS_EVENT_HTTP_READ_REQUEST_HDR:
lua_getglobal(l, TS_LUA_FUNCTION_G_READ_REQUEST);
break;
case TS_EVENT_HTTP_SEND_REQUEST_HDR:
lua_getglobal(l, TS_LUA_FUNCTION_G_SEND_REQUEST);
break;
case TS_EVENT_HTTP_READ_RESPONSE_HDR:
lua_getglobal(l, TS_LUA_FUNCTION_G_READ_RESPONSE);
break;
case TS_EVENT_HTTP_SEND_RESPONSE_HDR:
lua_getglobal(l, TS_LUA_FUNCTION_G_SEND_RESPONSE);
break;
case TS_EVENT_HTTP_CACHE_LOOKUP_COMPLETE:
lua_getglobal(l, TS_LUA_FUNCTION_G_CACHE_LOOKUP_COMPLETE);
break;
case TS_EVENT_HTTP_TXN_START:
lua_getglobal(l, TS_LUA_FUNCTION_G_TXN_START);
break;
case TS_EVENT_HTTP_PRE_REMAP:
lua_getglobal(l, TS_LUA_FUNCTION_G_PRE_REMAP);
break;
case TS_EVENT_HTTP_POST_REMAP:
lua_getglobal(l, TS_LUA_FUNCTION_G_POST_REMAP);
break;
case TS_EVENT_HTTP_OS_DNS:
lua_getglobal(l, TS_LUA_FUNCTION_G_OS_DNS);
break;
case TS_EVENT_HTTP_READ_CACHE_HDR:
lua_getglobal(l, TS_LUA_FUNCTION_G_READ_CACHE);
break;
case TS_EVENT_HTTP_TXN_CLOSE:
lua_getglobal(l, TS_LUA_FUNCTION_G_TXN_CLOSE);
break;
default:
ts_lua_destroy_http_ctx(http_ctx);
TSMutexUnlock(main_ctx->mutexp);
TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
return 0;
}
if (lua_type(l, -1) != LUA_TFUNCTION) {
lua_pop(l, 1);
ts_lua_destroy_http_ctx(http_ctx);
TSMutexUnlock(main_ctx->mutexp);
TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
return 0;
}
ts_lua_set_cont_info(l, nullptr);
if (lua_pcall(l, 0, 1, 0) != 0) {
TSError("[ts_lua][%s] lua_pcall failed: %s", __FUNCTION__, lua_tostring(l, -1));
}
ret = lua_tointeger(l, -1);
lua_pop(l, 1);
ts_lua_clear_http_ctx(http_ctx);
if (http_ctx->has_hook) {
// add a hook to release resources for context
Dbg(dbg_ctl, "[%s] has txn hook -> adding txn close hook handler to release resources", __FUNCTION__);
TSHttpTxnHookAdd(txnp, TS_HTTP_TXN_CLOSE_HOOK, txn_contp);
} else {
Dbg(dbg_ctl, "[%s] no txn hook -> release resources now", __FUNCTION__);
ts_lua_destroy_http_ctx(http_ctx);
}
TSMutexUnlock(main_ctx->mutexp);
if (ret) {
TSHttpTxnReenable(txnp, TS_EVENT_HTTP_ERROR);
} else {
TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
}
return 0;
}
// Longest time the shutdown barrier waits for the Lua states to go idle. A Lua
// callback is expected to run for microseconds, so exceeding this means a script
// is stuck rather than merely busy.
constexpr std::chrono::milliseconds shutdown_barrier_timeout{5000};
constexpr std::chrono::milliseconds shutdown_barrier_poll{5};
// The scripts with a __shutdown__ function, in plugin.config order. A single
// continuation runs all of them, so the barrier is taken exactly once.
static std::vector<ts_lua_instance_conf *> shutdown_confs;
static TSCont shutdown_contp = nullptr;
static bool
lockStates(ts_lua_main_ctx *const ctx_array, std::chrono::steady_clock::time_point const deadline, std::vector<TSMutex> &locked)
{
if (nullptr == ctx_array) {
return true;
}
// Ascending index order, matching every other multi-state walk in the plugin.
for (int index = 0; index < ts_lua_max_state_count; ++index) {
TSMutex const mutexp = ctx_array[index].mutexp;
while (TS_SUCCESS != TSMutexLockTry(mutexp)) {
if (deadline <= std::chrono::steady_clock::now()) {
return false;
}
std::this_thread::sleep_for(shutdown_barrier_poll);
}
locked.push_back(mutexp);
}
return true;
}
static void
unlockStates(std::vector<TSMutex> &locked)
{
for (auto mutexp = locked.rbegin(); mutexp != locked.rend(); ++mutexp) {
TSMutexUnlock(*mutexp);
}
locked.clear();
}
// Invoke every __shutdown__ function with no Lua code running anywhere else in
// the process.
//
// A __shutdown__ function typically releases process global resources, often in
// a native library reached through FFI. Each Lua execution path locks only the
// one main state it was assigned, so locking a single state is not enough: the
// callback running in state 0 can tear those resources down while a request
// callback is still using them in state 1. Holding every main state mutex is
// what makes __shutdown__ exclusive with all Lua code.
//
// This runs on a thread of its own rather than on the event thread that
// dispatched TS_LIFECYCLE_SHUTDOWN_HOOK because ProxyMutex is recursive per
// thread: an event thread that holds these mutexes can still enter Lua itself,
// and the dispatching event thread does exactly that when it returns to its
// event loop for the last time.
static void *
runShutdownCallbacks(void * /* data ATS_UNUSED */)
{
auto const deadline = std::chrono::steady_clock::now() + shutdown_barrier_timeout;
std::vector<TSMutex> global_states;
std::vector<TSMutex> remap_states;
global_states.reserve(ts_lua_max_state_count);
remap_states.reserve(ts_lua_max_state_count);
// The remap states are only in this image when a remap instance was loaded
// without proxy.config.plugin.dynamic_reload_mode; with dynamic reload the
// remap instance is a private copy of the plugin with states of its own, which
// this barrier cannot reach.
if (!lockStates(ts_lua_g_main_ctx_array, deadline, global_states) || !lockStates(ts_lua_main_ctx_array, deadline, remap_states)) {
// A partial barrier excludes nothing that matters and stalls the states it
// did lock, so give them all back.
unlockStates(remap_states);
unlockStates(global_states);
TSError("[ts_lua][%s] Lua states still active after %lld ms, skipping %s", __FUNCTION__,
static_cast<long long>(shutdown_barrier_timeout.count()), TS_LUA_FUNCTION_G_SHUT_DOWN);
return nullptr;
}
Dbg(dbg_ctl, "[%s] shutdown barrier acquired for %zu Lua states", __FUNCTION__, global_states.size() + remap_states.size());
// Published while every state mutex is owned here, so TSRemapDeleteInstance
// cannot see it change while it is entering Lua.
shutdown_barrier_engaged.store(true, std::memory_order_release);
for (ts_lua_instance_conf *const conf : shutdown_confs) {
for (int index = 0; index < conf->states; ++index) {
ts_lua_main_ctx *const main_ctx = &ts_lua_g_main_ctx_array[index];
lua_State *const L = main_ctx->lua;
// Restore the conf-specific global table so lua_getglobal resolves
// functions from the loaded script, matching ts_lua_reload_module.
lua_pushlightuserdata(L, conf);
lua_rawget(L, LUA_REGISTRYINDEX);
lua_replace(L, LUA_GLOBALSINDEX);
lua_getglobal(L, TS_LUA_FUNCTION_G_SHUT_DOWN);
if (lua_type(L, -1) == LUA_TFUNCTION) {
if (lua_pcall(L, 0, 0, 0) != 0) {
TSError("[ts_lua][%s] lua_pcall failed for script '%s' state %d: %s", __FUNCTION__, conf->script, index,
lua_tostring(L, -1));
lua_pop(L, 1);
}
} else {
lua_pop(L, 1);
}
// Restore LUA_GLOBALSINDEX to an empty table, matching the resting state
// established by ts_lua_add_module and ts_lua_reload_module.
lua_newtable(L);
lua_replace(L, LUA_GLOBALSINDEX);
}
}
// Every state mutex is deliberately kept, by a thread that is about to exit.
// TS_LIFECYCLE_SHUTDOWN_HOOK is terminal and no event thread is joined before
// the process exits, so releasing any of them would only let a callback that is
// already queued behind one enter Lua after __shutdown__ freed what that
// callback uses. An event thread blocked on one of these mutexes does not keep
// the process from exiting. TSRemapDeleteInstance, which ATS runs later in
// shutdown and which would otherwise block on the remap mutexes, is what
// shutdown_barrier_engaged is for.
return nullptr;
}
static int
shutdownHookHandler(TSCont /* contp ATS_UNUSED */, TSEvent /* event ATS_UNUSED */, void * /* edata ATS_UNUSED */)
{
TSThread const barrier = TSThreadCreate(runShutdownCallbacks, nullptr);
if (nullptr == barrier) {
TSError("[ts_lua][%s] could not create the shutdown barrier thread, skipping %s", __FUNCTION__, TS_LUA_FUNCTION_G_SHUT_DOWN);
return 0;
}
// ATS continues shutting down as soon as this returns, so wait for the
// callbacks. The thread itself is not destroyed: the mutexes it still holds
// record it as their owner.
TSThreadWait(barrier);
return 0;
}
void
TSPluginInit(int argc, const char *argv[])
{
TSPluginRegistrationInfo info;
info.plugin_name = "ts_lua";
info.vendor_name = "Apache Software Foundation";
info.support_email = "dev@trafficserver.apache.org";
if (TSPluginRegister(&info) != TS_SUCCESS) {
TSError("[ts_lua][%s] Plugin registration failed", __FUNCTION__);
}
if (nullptr == ts_lua_g_main_ctx_array) {
ts_lua_g_main_ctx_array = create_lua_vms();
if (nullptr != ts_lua_g_main_ctx_array) {
pthread_key_create(&lua_g_state_key, nullptr);
TSCont const contp = TSContCreate(lifecycleHandler, TSMutexCreate());
TSContDataSet(contp, ts_lua_g_main_ctx_array);
TSLifecycleHookAdd(TS_LIFECYCLE_MSG_HOOK, contp);
ts_lua_plugin_stats *const plugin_stats = create_plugin_stats(ts_lua_g_main_ctx_array, ts_lua_g_stat_strs);
if (nullptr != plugin_stats) {
TSCont const scontp = TSContCreate(statsHandler, TSMutexCreate());
TSContDataSet(scontp, plugin_stats);
TSContScheduleOnPool(scontp, TS_LUA_STATS_TIMEOUT, TS_THREAD_POOL_TASK);
}
} else {
return;
}
}
int states = ts_lua_max_state_count;
int jit = 1;
int reload = 0;
static const struct option longopt[] = {
{"states", required_argument, 0, 's'},
{"jit", required_argument, 0, 'j'},
{"enable-reload", no_argument, 0, 'r'},
{0, 0, 0, 0 },
};
for (;;) {
int opt;
opt = getopt_long(argc, (char *const *)argv, "", longopt, nullptr);
switch (opt) {
case 's':
states = atoi(optarg);
// set state
break;
case 'j':
jit = atoi(optarg);
if (jit == 0) {
Dbg(dbg_ctl, "[%s] disable JIT mode", __FUNCTION__);
for (int index = 0; index < ts_lua_max_state_count; ++index) {
ts_lua_main_ctx *const main_ctx = (ts_lua_g_main_ctx_array + index);
lua_State *const lstate = main_ctx->lua;
if (luaJIT_setmode(lstate, 0, LUAJIT_MODE_ENGINE | LUAJIT_MODE_OFF) == 0) {
TSError("[ts_lua][%s] Failed to disable JIT mode", __FUNCTION__);
}
}
}
break;
case 'r':
reload = 1;
Dbg(dbg_ctl, "[%s] enable global plugin reload [%d]", __FUNCTION__, reload);
break;
}
if (opt == -1) {
break;
}
}
if (states < 1 || ts_lua_max_state_count < states) {
TSError("[ts_lua][%s] invalid # of states from option input. Must be between 1 and %d", __FUNCTION__, ts_lua_max_state_count);
return;
}
if (argc - optind < 1) {
TSError("[ts_lua][%s] lua script file required !!", __FUNCTION__);
return;
}
if (strlen(argv[optind]) >= TS_LUA_MAX_SCRIPT_FNAME_LENGTH - 16) {
TSError("[ts_lua][%s] lua script file name too long !!", __FUNCTION__);
return;
}
auto *conf = TSRalloc<ts_lua_instance_conf>();
if (!conf) {
TSError("[ts_lua][%s] malloc failed !!", __FUNCTION__);
return;
}
memset(conf, 0, sizeof(ts_lua_instance_conf));
conf->remap = 0;
conf->states = states;
if (argv[optind][0] == '/') {
snprintf(conf->script, TS_LUA_MAX_SCRIPT_FNAME_LENGTH, "%s", argv[optind]);
} else {
snprintf(conf->script, TS_LUA_MAX_SCRIPT_FNAME_LENGTH, "%s/%s", TSConfigDirGet(), argv[optind]);
}
ts_lua_init_instance(conf);
char errbuf[TS_LUA_MAX_STR_LENGTH];
int const errbuf_len = sizeof(errbuf);
int const ret =
ts_lua_add_module(conf, ts_lua_g_main_ctx_array, conf->states, argc - optind, (char **)&argv[optind], errbuf, errbuf_len);
if (ret != 0) {
TSError("[ts_lua]%.*s", (int)errbuf_len, errbuf);
TSEmergency("[ts_lua][%s] ts_lua_add_module failed", __FUNCTION__);
return;
}
TSCont global_contp = TSContCreate(globalHookHandler, nullptr);
if (!global_contp) {
TSError("[ts_lua][%s] could not create transaction start continuation", __FUNCTION__);
return;
}
TSContDataSet(global_contp, conf);
// adding hook based on whether the lua global function exists.
ts_lua_main_ctx *main_ctx = &ts_lua_g_main_ctx_array[0];
ts_lua_http_ctx *http_ctx = ts_lua_create_http_ctx(main_ctx, conf);
lua_State *l = http_ctx->cinfo.routine.lua;
lua_getglobal(l, TS_LUA_FUNCTION_G_SEND_REQUEST);
if (lua_type(l, -1) == LUA_TFUNCTION) {
TSHttpHookAdd(TS_HTTP_SEND_REQUEST_HDR_HOOK, global_contp);
Dbg(dbg_ctl, "send_request_hdr_hook added");
}
lua_pop(l, 1);
lua_getglobal(l, TS_LUA_FUNCTION_G_READ_RESPONSE);
if (lua_type(l, -1) == LUA_TFUNCTION) {
TSHttpHookAdd(TS_HTTP_READ_RESPONSE_HDR_HOOK, global_contp);
Dbg(dbg_ctl, "read_response_hdr_hook added");
}
lua_pop(l, 1);
lua_getglobal(l, TS_LUA_FUNCTION_G_SEND_RESPONSE);
if (lua_type(l, -1) == LUA_TFUNCTION) {
TSHttpHookAdd(TS_HTTP_SEND_RESPONSE_HDR_HOOK, global_contp);
Dbg(dbg_ctl, "send_response_hdr_hook added");
}
lua_pop(l, 1);
lua_getglobal(l, TS_LUA_FUNCTION_G_CACHE_LOOKUP_COMPLETE);
if (lua_type(l, -1) == LUA_TFUNCTION) {
TSHttpHookAdd(TS_HTTP_CACHE_LOOKUP_COMPLETE_HOOK, global_contp);
Dbg(dbg_ctl, "cache_lookup_complete_hook added");
}
lua_pop(l, 1);
lua_getglobal(l, TS_LUA_FUNCTION_G_READ_REQUEST);
if (lua_type(l, -1) == LUA_TFUNCTION) {
TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, global_contp);
Dbg(dbg_ctl, "read_request_hdr_hook added");
}
lua_pop(l, 1);
lua_getglobal(l, TS_LUA_FUNCTION_G_TXN_START);
if (lua_type(l, -1) == LUA_TFUNCTION) {
TSHttpHookAdd(TS_HTTP_TXN_START_HOOK, global_contp);
Dbg(dbg_ctl, "txn_start_hook added");
}
lua_pop(l, 1);
lua_getglobal(l, TS_LUA_FUNCTION_G_PRE_REMAP);
if (lua_type(l, -1) == LUA_TFUNCTION) {
TSHttpHookAdd(TS_HTTP_PRE_REMAP_HOOK, global_contp);
Dbg(dbg_ctl, "pre_remap_hook added");
}
lua_pop(l, 1);
lua_getglobal(l, TS_LUA_FUNCTION_G_POST_REMAP);
if (lua_type(l, -1) == LUA_TFUNCTION) {
TSHttpHookAdd(TS_HTTP_POST_REMAP_HOOK, global_contp);
Dbg(dbg_ctl, "post_remap_hook added");
}
lua_pop(l, 1);
lua_getglobal(l, TS_LUA_FUNCTION_G_OS_DNS);
if (lua_type(l, -1) == LUA_TFUNCTION) {
TSHttpHookAdd(TS_HTTP_OS_DNS_HOOK, global_contp);
Dbg(dbg_ctl, "os_dns_hook added");
}
lua_pop(l, 1);
lua_getglobal(l, TS_LUA_FUNCTION_G_READ_CACHE);
if (lua_type(l, -1) == LUA_TFUNCTION) {
TSHttpHookAdd(TS_HTTP_READ_CACHE_HDR_HOOK, global_contp);
Dbg(dbg_ctl, "read_cache_hdr_hook added");
}
lua_pop(l, 1);
lua_getglobal(l, TS_LUA_FUNCTION_G_TXN_CLOSE);
if (lua_type(l, -1) == LUA_TFUNCTION) {
TSHttpHookAdd(TS_HTTP_TXN_CLOSE_HOOK, global_contp);
Dbg(dbg_ctl, "txn_close_hook added");
}
lua_pop(l, 1);
ts_lua_destroy_http_ctx(http_ctx);
TSCont vconn_contp = TSContCreate(vconnHookHandler, nullptr);
if (!vconn_contp) {
TSError("[ts_lua][%s] could not create vconn continuation", __FUNCTION__);
return;
}
TSContDataSet(vconn_contp, conf);
// adding hook based on whether the lua global vconn function exists
ts_lua_vconn_ctx *vconn_ctx = ts_lua_create_vconn_ctx(main_ctx, conf);
lua_State *vl = vconn_ctx->lua;
lua_getglobal(vl, TS_LUA_FUNCTION_G_VCONN_START);
if (lua_type(vl, -1) == LUA_TFUNCTION) {
TSHttpHookAdd(TS_VCONN_START_HOOK, vconn_contp);
Dbg(dbg_ctl, "vconn_start_hook added");
}
lua_pop(vl, 1);
ts_lua_destroy_vconn_ctx(vconn_ctx);
// adding shutdown hook if the lua global shutdown function exists
ts_lua_main_ctx *shutdown_main_ctx = &ts_lua_g_main_ctx_array[0];
ts_lua_http_ctx *shutdown_http_ctx = ts_lua_create_http_ctx(shutdown_main_ctx, conf);
lua_State *sl = shutdown_http_ctx->cinfo.routine.lua;
lua_getglobal(sl, TS_LUA_FUNCTION_G_SHUT_DOWN);
if (lua_type(sl, -1) == LUA_TFUNCTION) {
shutdown_confs.push_back(conf);
// One continuation invokes every script's __shutdown__, so that the shutdown
// barrier is taken once for the whole process.
if (nullptr == shutdown_contp) {
TSMutex shutdown_mutex = TSMutexCreate();
shutdown_contp = TSContCreate(shutdownHookHandler, shutdown_mutex);
if (!shutdown_contp) {
TSError("[ts_lua][%s] could not create shutdown continuation", __FUNCTION__);
if (shutdown_mutex) {
TSMutexDestroy(shutdown_mutex);
}
} else {
TSLifecycleHookAdd(TS_LIFECYCLE_SHUTDOWN_HOOK, shutdown_contp);
Dbg(dbg_ctl, "shutdown_hook added");
}
}
}
lua_pop(sl, 1);
ts_lua_destroy_http_ctx(shutdown_http_ctx);
// support for reload as global plugin
if (reload) {
TSCont config_contp = TSContCreate(configHandler, nullptr);
if (!config_contp) {
TSError("[ts_lua][%s] could not create configuration continuation", __FUNCTION__);
return;
}
TSContDataSet(config_contp, conf);
TSMgmtUpdateRegister(config_contp, "ts_lua");
}
}