Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ext-plugin): stop the runner with SIGTERM #4367

Merged
merged 3 commits into from
Jun 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apisix/cli/ngx_tpl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,12 @@ http {
apisix.http_init_worker()
}

{% if not use_openresty_1_17 then %}
exit_worker_by_lua_block {
apisix.http_exit_worker()
}
{% end %}

{% if enable_control then %}
server {
listen {* control_server_addr *};
Expand Down
10 changes: 8 additions & 2 deletions apisix/cli/ops.lua
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ version: print the version of apisix
end


local function check_version(cur_ver_s, need_ver_s)
local function version_greater_equal(cur_ver_s, need_ver_s)
local cur_vers = util.split(cur_ver_s, [[.]])
local need_vers = util.split(need_ver_s, [[.]])
local len = max(#cur_vers, #need_vers)
Expand Down Expand Up @@ -357,10 +357,15 @@ Please modify "admin_key" in conf/config.yaml .
end

local need_ver = "1.17.3"
if not check_version(or_ver, need_ver) then
if not version_greater_equal(or_ver, need_ver) then
util.die("openresty version must >=", need_ver, " current ", or_ver, "\n")
end

local use_openresty_1_17 = false
if not version_greater_equal(or_ver, "1.19.3") then
use_openresty_1_17 = true
end

local or_info = util.execute_cmd("openresty -V 2>&1")
local with_module_status = true
if or_info and not or_info:find("http_stub_status_module", 1, true) then
Expand Down Expand Up @@ -446,6 +451,7 @@ Please modify "admin_key" in conf/config.yaml .

-- Using template.render
local sys_conf = {
use_openresty_1_17 = use_openresty_1_17,
lua_path = env.pkg_path_org,
lua_cpath = env.pkg_cpath_org,
os_name = util.trim(util.execute_cmd("uname")),
Expand Down
34 changes: 34 additions & 0 deletions apisix/core/os.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,18 @@ local type = type


local _M = {}
local WNOHANG = 1


ffi.cdef[[
typedef int32_t pid_t;
typedef unsigned int useconds_t;

int setenv(const char *name, const char *value, int overwrite);
char *strerror(int errnum);

int usleep(useconds_t usec);
pid_t waitpid(pid_t pid, int *wstatus, int options);
]]


Expand All @@ -52,4 +59,31 @@ function _M.setenv(name, value)
end


local function waitpid_nohang(pid)
local res = C.waitpid(pid, nil, WNOHANG)
if res == -1 then
return nil, err()
end
return res > 0
end


function _M.waitpid(pid, timeout)
local count = 0
local step = 1000 * 10
local total = timeout * 1000 * 1000
while step * count < total do
count = count + 1
C.usleep(step)
local ok, err = waitpid_nohang(pid)
if err then
return nil, err
end
if ok then
return true
end
end
end


return _M
5 changes: 5 additions & 0 deletions apisix/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ function _M.http_init_worker()
end


function _M.http_exit_worker()
require("apisix.plugins.ext-plugin.init").exit_worker()
end


function _M.http_ssl_phase()
local ngx_ctx = ngx.ctx
local api_ctx = ngx_ctx.api_ctx
Expand Down
30 changes: 26 additions & 4 deletions apisix/plugins/ext-plugin/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ if is_http then
ngx_pipe = require("ngx.pipe")
events = require("resty.worker.events")
end
local resty_signal = require "resty.signal"
local bit = require("bit")
local band = bit.band
local lshift = bit.lshift
Expand Down Expand Up @@ -588,8 +589,10 @@ local function spawn_proc(cmd)
end


local runner
local function setup_runner(cmd)
local proc = spawn_proc(cmd)
runner = spawn_proc(cmd)

ngx_timer_at(0, function(premature)
if premature then
return
Expand All @@ -599,7 +602,7 @@ local function setup_runner(cmd)
while true do
-- drain output
local max = 3800 -- smaller than Nginx error log length limit
local data, err = proc:stdout_read_any(max)
local data, err = runner:stdout_read_any(max)
if not data then
if exiting() then
return
Expand All @@ -615,11 +618,13 @@ local function setup_runner(cmd)
end
end

local ok, reason, status = proc:wait()
local ok, reason, status = runner:wait()
if not ok then
core.log.warn("runner exited with reason: ", reason, ", status: ", status)
end

runner = nil

local ok, err = events.post(events_list._source, events_list.runner_exit)
if not ok then
core.log.error("post event failure with ", events_list._source, ", error: ", err)
Expand All @@ -628,7 +633,7 @@ local function setup_runner(cmd)
core.log.warn("respawn runner 3 seconds later with cmd: ", core.json.encode(cmd))
core.utils.sleep(3)
core.log.warn("respawning new runner...")
proc = spawn_proc(cmd)
runner = spawn_proc(cmd)
end
end)
end
Expand Down Expand Up @@ -656,4 +661,21 @@ function _M.init_worker()
end


function _M.exit_worker()
if process.type() == "privileged agent" and runner then
-- We need to send SIGTERM in the exit_worker phase, as:
-- 1. privileged agent doesn't support graceful exiting when I write this
-- 2. better to make it work without graceful exiting
local pid = runner:pid()
core.log.notice("terminate runner ", pid, " with SIGTERM")
local num = resty_signal.signum("TERM")
runner:kill(num)

-- give 1s to clean up the mess
core.os.waitpid(pid, 1)
-- then we KILL it via gc finalizer
end
end


return _M
11 changes: 11 additions & 0 deletions docs/en/latest/external-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,14 @@ nginx_config:
envs:
- MY_ENV_VAR
```

### APISIX terminates my runner with SIGKILL but not SIGTERM!

Since `v2.7`, APISIX will stop the runner with SIGTERM when it is running on
OpenResty 1.19+.

However, APISIX needs to wait the runner to quit so that we can ensure the resource
for the process group is freed.

Therefore, we send SIGTERM first. And then after 1 second, if the runner is still
running, we will send SIGKILL.
10 changes: 10 additions & 0 deletions t/APISIX.pm
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,17 @@ _EOC_
require("apisix").http_init_worker()
$extra_init_worker_by_lua
}
_EOC_

if ($version !~ m/\/1.17.8/) {
$http_config .= <<_EOC_;
exit_worker_by_lua_block {
require("apisix").http_exit_worker()
}
_EOC_
}

$http_config .= <<_EOC_;
log_format main escape=default '\$remote_addr - \$remote_user [\$time_local] \$http_host "\$request" \$status \$body_bytes_sent \$request_time "\$http_referer" "\$http_user_agent" \$upstream_addr \$upstream_status \$upstream_response_time "\$upstream_scheme://\$upstream_host\$upstream_uri"';

# fake server, only for test
Expand Down
23 changes: 23 additions & 0 deletions t/plugin/ext-plugin/runner_can_not_terminated.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
#
# 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.
#

term() {
eval sleep 1800
}
trap term SIGTERM
sleep 1800
65 changes: 65 additions & 0 deletions t/plugin/ext-plugin/sanity-openresty-1-19.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#
# 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.
#
use t::APISIX;

my $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';
my $version = eval { `$nginx_binary -V 2>&1` };

if ($version =~ m/\/1.17.8/) {
plan(skip_all => "require OpenResty 1.19+");
} else {
plan('no_plan');
}

repeat_each(1);
no_long_string();
no_root_location();
no_shuffle();
log_level("info");

add_block_preprocessor(sub {
my ($block) = @_;

my $cmd = $block->ext_plugin_cmd // "['sleep', '5s']";
my $extra_yaml_config = <<_EOC_;
ext-plugin:
cmd: $cmd
_EOC_
$block->set_value("extra_yaml_config", $extra_yaml_config);

if (!$block->request) {
$block->set_value("request", "GET /t");
}

if (!$block->error_log) {
$block->set_value("no_error_log", "[error]\n[alert]");
}
});

run_tests;

__DATA__

=== TEST 1: terminate spawn runner
--- ext_plugin_cmd
["t/plugin/ext-plugin/runner.sh", "3600"]
--- config
location /t {
return 200;
}
--- shutdown_error_log eval
qr/terminate runner \d+ with SIGTERM/
10 changes: 10 additions & 0 deletions t/plugin/ext-plugin/sanity.t
Original file line number Diff line number Diff line change
Expand Up @@ -445,3 +445,13 @@ MY_ENV_VAR foo
{"error_msg":"failed to check the configuration of plugin ext-plugin-pre-req err: property \"conf\" validation failed: failed to validate item 1: property \"name\" is required"}

{"error_msg":"failed to check the configuration of plugin ext-plugin-post-req err: property \"conf\" validation failed: failed to validate item 1: property \"value\" is required"}



=== TEST 15: spawn runner which can't be terminated, ensure APISIX won't be blocked
--- ext_plugin_cmd
["t/plugin/ext-plugin/runner_can_not_terminated.sh"]
--- config
location /t {
return 200;
}