Make server able to run old or new binary so that we can compare the actix version with this one

This commit is contained in:
James Wilson
2021-07-16 17:58:26 +01:00
parent 81a0140c3b
commit 582c49413b
5 changed files with 344 additions and 208 deletions
+230 -143
View File
@@ -10,22 +10,62 @@ id_type! {
pub struct ProcessId(usize);
}
pub struct StartOpts {
/// Command to run to start a shard.
/// The `--listen` and `--log` arguments will be appended within and shouldn't be provided.
pub shard_command: Command,
/// Command to run to start a telemetry core process.
/// The `--listen` and `--log` arguments will be appended within and shouldn't be provided.
pub core_command: Command,
pub enum StartOpts {
/// Start a single core process that is expected
/// to have both `/feed` and `/submit` endpoints
SingleProcess {
/// Command to run to start the process.
/// The `--listen` and `--log` arguments will be appended within and shouldn't be provided.
command: Command,
},
/// Start a core process with a `/feed` andpoint as well as (optionally)
/// multiple shard processes with `/submit` endpoints.
ShardAndCore {
/// Command to run to start a shard.
/// The `--listen` and `--log` arguments will be appended within and shouldn't be provided.
shard_command: Command,
/// Command to run to start a telemetry core process.
/// The `--listen` and `--log` arguments will be appended within and shouldn't be provided.
core_command: Command,
},
/// Connect to existing process(es).
ConnectToExisting {
/// Where are the processes that we can `/submit` things to?
/// Eg: `vec![127.0.0.1:12345, 127.0.0.1:9091]`
submit_hosts: Vec<String>,
/// Where is the process that we can subscribe to the `/feed` of?
/// Eg: `127.0.0.1:3000`
feed_host: String,
}
}
pub struct ConnectToExistingOpts {
/// Details for connections to `telemetry_shard` /submit endpoints
pub shard_uris: Vec<http::Uri>,
/// Details for connections to `telemetry_core` /feed endpoints
pub feed_uri: http::Uri,
/// This represents a telemetry server. It can be in different modes
/// depending on how it was started, but the interface is similar in every case
/// so that tests are somewhat compatible with multiple configurations.
pub enum Server {
SingleProcessMode {
/// A virtual shard that we can hand out.
virtual_shard: ShardProcess,
/// Core process that we can connect to.
core: CoreProcess
},
ShardAndCoreMode {
/// Command to run to start a new shard.
shard_command: Command,
/// Shard processes that we can connect to.
shards: DenseMap<ProcessId, ShardProcess>,
/// Core process that we can connect to.
core: CoreProcess,
},
ConnectToExistingMode {
/// Shard processes that we can connect to.
shards: DenseMap<ProcessId, ShardProcess>,
/// Core process that we can connect to.
core: CoreProcess,
}
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Can't establsih connection: {0}")]
@@ -34,47 +74,44 @@ pub enum Error {
JoinError(#[from] tokio::task::JoinError),
#[error("Can't establsih connection: {0}")]
IoError(#[from] std::io::Error),
#[error("Could not obtain port for process: {0}")]
#[error("Could not obtain port for process as the line we waited for in log output didn't show up: {0}")]
ErrorObtainingPort(anyhow::Error),
#[error("Whoops; attempt to kill a process we didn't start (and so have no handle to)")]
CannotKillNoHandle,
#[error(
"Whoops; attempt to add a shard to a server we didn't start (and so have no handle to)"
"Can't add a shard: command not provided, or we are not in charge of spawning processes"
)]
CannotAddShardNoHandle,
}
/// This represents a telemetry core process and zero or more connected shards.
/// From this, you can add/remove shards, establish node/feed connections, and
/// send/receive relevant messages from each.
pub struct Server {
/// URI to connect a shard to core:
core_shard_submit_uri: Option<http::Uri>,
/// Command to run to start a new shard. Optional
/// because if we connect to running instances it'll
/// be unset.
shard_command: Option<Command>,
/// Shard processes that we can connect to
shards: DenseMap<ProcessId, ShardProcess>,
/// Core process that we can connect to
core: CoreProcess,
CannotAddShard,
#[error("The URI provided was invalid: {0}")]
InvalidUri(#[from] http::uri::InvalidUri)
}
impl Server {
pub fn get_core(&self) -> &CoreProcess {
&self.core
match self {
Server::SingleProcessMode { core, .. } => core,
Server::ShardAndCoreMode { core, ..} => core,
Server::ConnectToExistingMode { core, .. } => core
}
}
pub fn get_shard(&self, id: ProcessId) -> Option<&ShardProcess> {
self.shards.get(id)
}
pub fn iter_shards(&self) -> impl Iterator<Item = &ShardProcess> {
self.shards.iter().map(|(_, v)| v)
match self {
Server::SingleProcessMode { virtual_shard, .. } => Some(virtual_shard),
Server::ShardAndCoreMode { shards, ..} => shards.get(id),
Server::ConnectToExistingMode { shards, .. } => shards.get(id)
}
}
pub async fn kill_shard(&mut self, id: ProcessId) -> bool {
let shard = match self.shards.remove(id) {
let shard = match self {
// Can't remove the pretend shard:
Server::SingleProcessMode { .. } => return false,
Server::ShardAndCoreMode { shards, ..} => shards.remove(id),
Server::ConnectToExistingMode { shards, .. } => shards.remove(id)
};
let shard = match shard {
Some(shard) => shard,
None => return false,
};
@@ -94,9 +131,17 @@ impl Server {
// Spawn so we don't need to await cleanup if we don't care.
// Run all kill futs simultaneously.
let handle = tokio::spawn(async move {
let shard_kill_futs = self.shards.into_iter().map(|(_, s)| s.kill());
let (core, shards) = match self {
Server::SingleProcessMode { core, .. }
=> (core, DenseMap::new()),
Server::ShardAndCoreMode { core, shards, ..}
=> (core, shards),
Server::ConnectToExistingMode { core, shards, .. }
=> (core, shards)
};
let _ = tokio::join!(futures::future::join_all(shard_kill_futs), self.core.kill());
let shard_kill_futs = shards.into_iter().map(|(_, s)| s.kill());
let _ = tokio::join!(futures::future::join_all(shard_kill_futs), core.kill());
});
// You can wait for cleanup but aren't obliged to:
@@ -105,68 +150,119 @@ impl Server {
/// Connect a new shard and return a process that you can interact with:
pub async fn add_shard(&mut self) -> Result<ProcessId, Error> {
let core_uri = match &self.core_shard_submit_uri {
Some(uri) => uri,
None => return Err(Error::CannotAddShardNoHandle),
};
match self {
// Always get back the same "shard" in virtual mode; it's just the core anyway.
Server::SingleProcessMode { virtual_shard, .. } => {
Ok(virtual_shard.id)
},
// We're connecting to existing things; nothing sane to hand back.
Server::ConnectToExistingMode { .. } => {
Err(Error::CannotAddShard)
},
// Start a new process and return that.
Server::ShardAndCoreMode { shard_command, shards, core } => {
// Where is the URI we'll want to submit things to?
let core_shard_submit_uri = format!("http://{}/shard_submit", core.host);
let mut shard_cmd: TokioCommand = self
.shard_command
.clone()
.ok_or_else(|| Error::CannotAddShardNoHandle)?
.into();
let mut shard_cmd: TokioCommand = shard_command.clone().into();
shard_cmd
.arg("--listen")
.arg("127.0.0.1:0") // 0 to have a port picked by the kernel
.arg("--log")
.arg("info")
.arg("--core")
.arg(core_shard_submit_uri)
.kill_on_drop(true)
.stdout(std::process::Stdio::piped())
.stdin(std::process::Stdio::piped());
shard_cmd
.arg("--listen")
.arg("127.0.0.1:0") // 0 to have a port picked by the kernel
.arg("--log")
.arg("info")
.arg("--core")
.arg(core_uri.to_string())
.kill_on_drop(true)
.stdout(std::process::Stdio::piped())
.stdin(std::process::Stdio::piped());
let mut shard_process = shard_cmd.spawn()?;
let mut child_stdout = shard_process.stdout.take().expect("shard stdout");
let shard_port = utils::get_port(&mut child_stdout)
.await
.map_err(|e| Error::ErrorObtainingPort(e))?;
let mut shard_process = shard_cmd.spawn()?;
let mut child_stdout = shard_process.stdout.take().expect("shard stdout");
let shard_port = utils::get_port(&mut child_stdout)
.await
.map_err(|e| Error::ErrorObtainingPort(e))?;
// Attempt to wait until we've received word that the shard is connected to the
// core before continuing. If we don't wait for this, the connection may happen
// after we've attempted to connect node sockets, and they would be booted and
// made to reconnect, which we don't want to deal with in general.
let _ = utils::wait_for_line_containing(
&mut child_stdout,
|s| s.contains("Connected to telemetry core"),
std::time::Duration::from_secs(5),
)
.await;
// Attempt to wait until we've received word that the shard is connected to the
// core before continuing. If we don't wait for this, the connection may happen
// after we've attempted to connect node sockets, and they would be booted and
// made to reconnect, which we don't want to deal with in general.
let _ = utils::wait_for_line_containing(
&mut child_stdout,
"Connected to telemetry core",
std::time::Duration::from_secs(5),
)
.await;
// Since we're piping stdout from the child process, we need somewhere for it to go
// else the process will get stuck when it tries to produce output:
utils::drain(child_stdout, tokio::io::stderr());
// Since we're piping stdout from the child process, we need somewhere for it to go
// else the process will get stuck when it tries to produce output:
utils::drain(child_stdout, tokio::io::stderr());
let pid = shards.add_with(|id| Process {
id,
host: format!("127.0.0.1:{}", shard_port),
handle: Some(shard_process),
_channel_type: PhantomData,
});
let shard_uri = format!("http://127.0.0.1:{}/submit", shard_port)
.parse()
.expect("valid submit URI");
let pid = self.shards.add_with(|id| Process {
id,
handle: Some(shard_process),
uri: shard_uri,
_channel_type: PhantomData,
});
Ok(pid)
Ok(pid)
},
}
}
/// Start a telemetry_core process. From here, we can add/remove shards as needed.
/// Start a server.
pub async fn start(opts: StartOpts) -> Result<Server, Error> {
let mut core_cmd: TokioCommand = opts.core_command.into();
let server = match opts {
StartOpts::SingleProcess { command } => {
let core_process = Server::start_core(command).await?;
let virtual_shard_host = core_process.host.clone();
Server::SingleProcessMode {
core: core_process,
virtual_shard: Process {
id: ProcessId(0),
host: virtual_shard_host,
handle: None,
_channel_type: PhantomData
}
}
},
StartOpts::ShardAndCore { core_command, shard_command } => {
let core_process = Server::start_core(core_command).await?;
Server::ShardAndCoreMode {
core: core_process,
shard_command,
shards: DenseMap::new()
}
},
StartOpts::ConnectToExisting { feed_host, submit_hosts } => {
let mut shards = DenseMap::new();
for host in submit_hosts {
shards.add_with(|id| Process {
id,
host,
handle: None,
_channel_type: PhantomData,
});
}
let mut child = core_cmd
Server::ConnectToExistingMode {
shards,
core: Process {
id: ProcessId(0),
host: feed_host,
handle: None,
_channel_type: PhantomData,
},
}
}
};
Ok(server)
}
/// Start up a core process and return it.
async fn start_core(command: Command) -> Result<CoreProcess, Error> {
let mut tokio_core_cmd: TokioCommand = command.into();
let mut child = tokio_core_cmd
.arg("--listen")
.arg("127.0.0.1:0") // 0 to have a port picked by the kernel
.arg("--log")
@@ -186,52 +282,14 @@ impl Server {
// else the process will get stuck when it tries to produce output:
utils::drain(child_stdout, tokio::io::stderr());
// URI for feeds to connect to the core:
let feed_uri = format!("http://127.0.0.1:{}/feed", core_port)
.parse()
.expect("valid feed URI");
let core_process = Process {
id: ProcessId(0),
host: format!("127.0.0.1:{}", core_port),
handle: Some(child),
_channel_type: PhantomData,
};
Ok(Server {
shard_command: Some(opts.shard_command),
core_shard_submit_uri: Some(
format!("http://127.0.0.1:{}/shard_submit", core_port)
.parse()
.expect("valid shard_submit URI"),
),
shards: DenseMap::new(),
core: Process {
id: ProcessId(0),
handle: Some(child),
uri: feed_uri,
_channel_type: PhantomData,
},
})
}
/// Establshes the requested connections to existing processes.
pub fn connect_to_existing(opts: ConnectToExistingOpts) -> Server {
let mut shards = DenseMap::new();
for shard_uri in opts.shard_uris {
shards.add_with(|id| Process {
id,
uri: shard_uri,
handle: None,
_channel_type: PhantomData,
});
}
Server {
shard_command: None,
// We can't add shards if starting in this mode:
core_shard_submit_uri: None,
shards,
core: Process {
id: ProcessId(0),
uri: opts.feed_uri,
handle: None,
_channel_type: PhantomData,
},
}
Ok(core_process)
}
}
@@ -239,11 +297,11 @@ impl Server {
/// may be either a `telemetry_shard` or `telemetry_core`.
pub struct Process<Channel> {
id: ProcessId,
/// Host that the process is running on (eg 127.0.0.1:8080).
host: String,
/// If we started the processes ourselves, we'll have a handle to
/// them which we can use to kill them. Else, we may not.
handle: Option<process::Child>,
/// The URI that we can use to connect to the process socket.
uri: http::Uri,
/// The kind of the process (lets us add methods specific to shard/core).
_channel_type: PhantomData<Channel>,
}
@@ -272,25 +330,54 @@ impl<Channel> Process<Channel> {
impl<Send: From<ws_client::Sender>, Recv: From<ws_client::Receiver>> Process<(Send, Recv)> {
/// Establish a connection to the process
pub async fn connect(&self) -> Result<(Send, Recv), Error> {
ws_client::connect(&self.uri)
async fn connect_to_uri(&self, uri: &http::Uri) -> Result<(Send, Recv), Error> {
ws_client::connect(uri)
.await
.map(|(s, r)| (s.into(), r.into()))
.map_err(|e| e.into())
}
/// Establish multiple connections to the process
pub async fn connect_multiple(
async fn connect_multiple_to_uri(
&self,
uri: &http::Uri,
num_connections: usize,
) -> Result<Vec<(Send, Recv)>, Error> {
utils::connect_multiple_to_uri(&self.uri, num_connections)
utils::connect_multiple_to_uri(uri, num_connections)
.await
.map(|v| v.into_iter().map(|(s, r)| (s.into(), r.into())).collect())
.map_err(|e| e.into())
}
}
impl ShardProcess {
/// Establish a connection to the process
pub async fn connect_node(&self) -> Result<(channels::ShardSender, channels::ShardReceiver), Error> {
let uri = format!("http://{}/submit", self.host).parse()?;
self.connect_to_uri(&uri).await
}
/// Establish multiple connections to the process
pub async fn connect_multiple_nodes(&self, num_connections: usize) -> Result<Vec<(channels::ShardSender, channels::ShardReceiver)>, Error> {
let uri = format!("http://{}/submit", self.host).parse()?;
self.connect_multiple_to_uri(&uri, num_connections).await
}
}
impl CoreProcess {
/// Establish a connection to the process
pub async fn connect_feed(&self) -> Result<(channels::FeedSender, channels::FeedReceiver), Error> {
let uri = format!("http://{}/feed", self.host).parse()?;
self.connect_to_uri(&uri).await
}
/// Establish multiple connections to the process
pub async fn connect_multiple_feeds(&self, num_connections: usize) -> Result<Vec<(channels::FeedSender, channels::FeedReceiver)>, Error> {
let uri = format!("http://{}/feed", self.host).parse()?;
self.connect_multiple_to_uri(&uri, num_connections).await
}
}
/// This defines a command to run. This exists because [`tokio::process::Command`]
/// cannot be cloned, but we need to be able to clone our command to spawn multiple
/// processes with it.