I am trying to set up a simple actix-web server with one endpoint called plot. It essentially just consumes some data, plots it with gnuplot and returns the bytes of the resulting PNG. the issue is that, as you will see in the code, I haven't found a way to do this all in memory, which means I have to persist the file to disk, reopen it into a reader and then send the response back. Depending on the level of concurrency, I will start getting { code: 24, kind: Other, message: "Too many open files" }
messages.
Does anyone have any idea how I would do this so that the entire process is done in-memory? I am using:
actix-web = "3"
gnuplot = "0.0.37"
image = "0.23.12"
Any help would be appreciated, here is the code:
use actix_web::{post, web, App, HttpResponse, HttpServer, Responder};
use gnuplot::{AxesCommon, Color, Figure, LineWidth};
use image::io::Reader;
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use std::any::type_name;
use std::collections::HashMap;
use std::fs;
#[post("/")]
async fn plot(req_body: web::Json<HashMap<String, Vec<f64>>>) -> impl Responder {
let data = req_body.get("data").unwrap();
let mut fg = Figure::new();
let fid: String = thread_rng().sample_iter(&Alphanumeric).take(10).collect();
let fname: String = format!("./{fid}.png", fid = fid);
fg.set_terminal("pngcairo", &fname);
let ax = fg.axes2d();
ax.set_border(false, &[], &[]);
ax.set_pos(0.0, 0.0);
ax.set_x_ticks(None, &[], &[]);
ax.set_y_ticks(None, &[], &[]);
let x: Vec<usize> = (1..data.len()).collect();
ax.lines(&x, data, &[LineWidth(4.0), Color("black")]);
fg.set_post_commands("unset output").show();
let image = Reader::open(&fname).unwrap().decode().unwrap();
let mut bytes: Vec<u8> = Vec::new();
image.write_to(&mut bytes, image::ImageOutputFormat::Png);
fs::remove_file(fname);
HttpResponse::Ok().body(bytes)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(plot))
.bind("127.0.0.1:8080")?
.run()
.await
}
To avoid creating a file, you can do what Akihito KIRISAKI described. You do that by calling set_terminal()
but instead of a file name, you pass an empty string. Then you create a Command
and echo()
into stdin
.
use std::process::{Command, Stdio};
#[post("/")]
async fn plot(req_body: web::Json<HashMap<String, Vec<f64>>>) -> impl Responder {
...
fg.set_terminal("pngcairo", "");
...
fg.set_post_commands("unset output");
let mut child = Command::new("gnuplot")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("expected gnuplot");
let mut stdin = child.stdin.take().expect("expected stdin");
fg.echo(&mut stdin);
// Drop `stdin` such that it is flused and closed,
// otherwise some programs might block until stdin
// is closed.
drop(stdin);
let output = child.wait_with_output().unwrap();
let png_image_data = output.stdout;
HttpResponse::Ok().body(png_image_data)
}
You also need to remove the call to show()
.