Skip to content

Commit

Permalink
feat: support graceful restart
Browse files Browse the repository at this point in the history
  • Loading branch information
vicanso committed Apr 5, 2024
1 parent a4cd0f4 commit 685f61c
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 15 deletions.
21 changes: 20 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ humantime-serde = "1.1.1"
log = "0.4.21"
memory-stats = { version = "1.1.0", features = ["always_use_statm"] }
mime_guess = "2.0.4"
nix = { version = "0.28.0", features = ["signal"] }
num_cpus = "1.16.0"
once_cell = "1.19.0"
path-absolutize = "3.1.1"
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ A reverse proxy like nginx, built on [pingora](https://github.com/cloudflare/pin
## Feature

- Filter location by host and path
- static file serve
- HTTP 1/2 end to end proxy
- Graceful reload
- Template for http access log
Expand All @@ -17,17 +18,17 @@ A reverse proxy like nginx, built on [pingora](https://github.com/cloudflare/pin
Loads all configurations from `/opt/proxy` and run in the background. Log appends to `/opt/proxy/pingap.log`.

```bash
RUST_LOG=INFO pingap -c=/opt/proxy -d --log=/opt/proxy/pingap.log
RUST_LOG=INFO pingap -c=/opt/proxy/pingap.toml -d --log=/opt/proxy/pingap.log
```

## Graceful restart

Validate the configurations, send quit signal to pingap, then start a new process to handle all requests.

```bash
RUST_LOG=INFO pingap -c=/opt/proxy -t \
RUST_LOG=INFO pingap -c=/opt/proxy/pingap.toml -t \
&& pkill -SIGQUIT pingap \
&& RUST_LOG=INFO pingap -c=/opt/proxy -d -u --log=/opt/proxy/pingap.log
&& RUST_LOG=INFO pingap -c=/opt/proxy/pingap.toml -d -u --log=/opt/proxy/pingap.log
```

## Config
Expand Down
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@
- [x] add remark for config
- [x] support multi host for location?
- [ ] support set upstream_keepalive_pool_size
- [ ] graceful restart for admin web
- [x] graceful restart for admin web
- [ ] use stable pingora
14 changes: 6 additions & 8 deletions docs/introduction_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@ description: Pingap 简述

Pingap是基于[pingora](https://github.com/cloudflare/pingora)开发的,pingora提供了各类模块便于rust开发者使用,但并不方便非rust开发者使用,因此pingap提供了以toml的形式配置简单易用的反向代理,实现支持多location代理转发。特性如下:

- 可通过请求的路径与域名筛选对应的location
- 支持静态文件目录处理
- 支持mock的响应配置
- 支持HTTP1与HTTP2
- 无中断请求的配置更新
- 模板式的请求日志输出

TODO 接入http缓存的逻辑
- 支持多location配置,可通过请求的路径与域名筛选
- 支持静态文件目录处理,简单方便的chunk的形式响应静态文件
- 支持mock的响应配置,方便测试或应急使用
- 支持HTTP1与HTTP2两种协议
- 无中断请求的配置更新,方便实时更新应用配置
- 模板式的请求日志输出,可按模板指定各种输出

[Pingap处理流程](./phase_chart_zh.md)

Expand Down
43 changes: 43 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
use log::info;
use once_cell::sync::Lazy;
use once_cell::sync::OnceCell;
use std::io;
use std::path::PathBuf;
use std::process;
use std::process::Command;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

mod load;
Expand All @@ -24,3 +29,41 @@ static START_TIME: Lazy<Duration> = Lazy::new(|| {
pub fn get_start_time() -> u64 {
START_TIME.as_secs()
}

#[derive(Debug, Default)]
pub struct RestartProcessCommand {
pub exec_path: PathBuf,
pub log_level: String,
pub args: Vec<String>,
}

impl RestartProcessCommand {
fn exec(&self) -> io::Result<process::Output> {
Command::new(&self.exec_path)
.env("RUST_LOG", &self.log_level)
.args(&self.args)
.output()
}
}

static CMD: OnceCell<RestartProcessCommand> = OnceCell::new();

pub fn set_restart_process_command(data: RestartProcessCommand) {
CMD.get_or_init(|| data);
}

pub fn restart() -> io::Result<process::Output> {
info!("pingap will restart now");
if let Some(cmd) = CMD.get() {
nix::sys::signal::kill(
nix::unistd::Pid::from_raw(std::process::id() as i32),
nix::sys::signal::SIGQUIT,
)?;
cmd.exec()
} else {
Err(std::io::Error::new(
io::ErrorKind::NotFound,
"Command not found",
))
}
}
22 changes: 22 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,28 @@ fn run() -> Result<(), Box<dyn Error>> {
}
config::set_config_path(&args.conf);

if let Ok(exec_path) = std::env::current_exe() {
let mut cmd = config::RestartProcessCommand {
exec_path,
..Default::default()
};
if let Ok(env) = std::env::var("RUST_LOG") {
cmd.log_level = env;
}
let conf_path = utils::resolve_path(&args.conf);

let mut new_args = vec![
format!("-c={conf_path}"),
"-d".to_string(),
"-u".to_string(),
];
if let Some(log) = &args.log {
new_args.push(format!("--log={log}"));
}
cmd.args = new_args;
config::set_restart_process_command(cmd);
}

let opt = Opt {
upgrade: args.upgrade,
daemon: args.daemon,
Expand Down
6 changes: 6 additions & 0 deletions src/serve/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@ impl Serve for AdminServe {
memory,
})
.unwrap_or(HttpResponse::unknown_error())
} else if path == "/restart" {
if let Err(e) = config::restart() {
error!("Restart fail: {e}");
return Err(pingora::Error::new_str("restart fail"));
}
HttpResponse::no_content()
} else {
let mut file = path.substring(1, path.len());
if file.is_empty() {
Expand Down
61 changes: 59 additions & 2 deletions web/src/components/main-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,23 @@ import IconButton from "@mui/material/IconButton";
import SettingsSuggestIcon from "@mui/icons-material/SettingsSuggest";
import SwipeableDrawer from "@mui/material/SwipeableDrawer";
import CardContent from "@mui/material/CardContent";
import Divider from "@mui/material/Divider";
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";

import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import useBasicStore from "../states/basic";
import request from "../helpers/request";

export default function MainHeader() {
const [fetch] = useBasicStore((state) => [state.fetch]);
const [startAt, setStartAt] = React.useState("");
const [version, setVersion] = React.useState("");
const [memory, setMemory] = React.useState("");
const [showSetting, setShowSetting] = React.useState(false);
const [showRestartDialog, setShowRestartDialog] = React.useState(false);

useAsync(async () => {
try {
Expand All @@ -30,6 +36,16 @@ export default function MainHeader() {
console.error(err);
}
}, []);

const confirmRestart = async () => {
try {
await request.post("/restart");
setShowRestartDialog(false);
} catch (err) {
console.error(err);
alert(err);
}
};
const box = (
<React.Fragment>
<IconButton
Expand Down Expand Up @@ -62,10 +78,51 @@ export default function MainHeader() {
<Typography gutterBottom variant="body2">
Memory: {memory}
</Typography>
<Button
style={{
marginTop: "10px",
}}
fullWidth
variant="outlined"
onClick={() => {
setShowRestartDialog(true);
}}
>
Restart Pingap
</Button>
</Box>
</CardContent>
</Card>
</SwipeableDrawer>
<Dialog
open={showRestartDialog}
onClose={() => {
setShowRestartDialog(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{"Are you sure to restart pingap?"}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Pingap will graceful restart with new configuration.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
setShowRestartDialog(false);
}}
>
Cancel
</Button>
<Button onClick={confirmRestart} autoFocus>
Restart
</Button>
</DialogActions>
</Dialog>
</React.Fragment>
);
return (
Expand Down

0 comments on commit 685f61c

Please sign in to comment.