From af5cd69f7b60c07c4830f2eca9b8b1544c7c4972 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Tue, 31 May 2022 21:08:56 +0200 Subject: [PATCH 01/15] set up gprc server --- planetwars-server/Cargo.toml | 5 ++++ planetwars-server/build.rs | 9 +++++++ planetwars-server/src/modules/bot_api.rs | 30 ++++++++++++++++++++++++ planetwars-server/src/modules/mod.rs | 1 + proto/bot_api.proto | 15 ++++++++++++ 5 files changed, 60 insertions(+) create mode 100644 planetwars-server/build.rs create mode 100644 planetwars-server/src/modules/bot_api.rs create mode 100644 proto/bot_api.proto diff --git a/planetwars-server/Cargo.toml b/planetwars-server/Cargo.toml index 4c6ddfc..6b96b04 100644 --- a/planetwars-server/Cargo.toml +++ b/planetwars-server/Cargo.toml @@ -26,9 +26,14 @@ toml = "0.5" planetwars-matchrunner = { path = "../planetwars-matchrunner" } config = { version = "0.12", features = ["toml"] } thiserror = "1.0.31" +prost = "0.10" +tonic = "0.7.2" # TODO: remove me shlex = "1.1" +[build-dependencies] +tonic-build = "0.7.2" + [dev-dependencies] parking_lot = "0.11" diff --git a/planetwars-server/build.rs b/planetwars-server/build.rs new file mode 100644 index 0000000..97bf355 --- /dev/null +++ b/planetwars-server/build.rs @@ -0,0 +1,9 @@ +extern crate tonic_build; + +fn main() -> Result<(), Box> { + tonic_build::configure() + .build_server(true) + .build_client(false) + .compile(&["../proto/bot_api.proto"], &["../proto"])?; + Ok(()) +} diff --git a/planetwars-server/src/modules/bot_api.rs b/planetwars-server/src/modules/bot_api.rs new file mode 100644 index 0000000..1941136 --- /dev/null +++ b/planetwars-server/src/modules/bot_api.rs @@ -0,0 +1,30 @@ +pub mod pb { + tonic::include_proto!("grpc.planetwars.bot_api"); +} + +use std::net::SocketAddr; + +use tonic; +use tonic::transport::Server; +use tonic::{Request, Response, Status}; + +pub struct BotApiServer {} + +#[tonic::async_trait] +impl pb::test_service_server::TestService for BotApiServer { + async fn greet(&self, req: Request) -> Result, Status> { + Ok(Response::new(pb::HelloResponse { + response: format!("hallo {}", req.get_ref().hello_message), + })) + } +} + +pub async fn run_bot_api() { + let server = BotApiServer {}; + let addr = SocketAddr::from(([127, 0, 0, 1], 50051)); + Server::builder() + .add_service(pb::test_service_server::TestServiceServer::new(server)) + .serve(addr) + .await + .unwrap() +} diff --git a/planetwars-server/src/modules/mod.rs b/planetwars-server/src/modules/mod.rs index bea28e0..43c2507 100644 --- a/planetwars-server/src/modules/mod.rs +++ b/planetwars-server/src/modules/mod.rs @@ -1,5 +1,6 @@ // This module implements general domain logic, not directly // tied to the database or API layers. +pub mod bot_api; pub mod bots; pub mod matches; pub mod ranking; diff --git a/proto/bot_api.proto b/proto/bot_api.proto new file mode 100644 index 0000000..ad0ee2f --- /dev/null +++ b/proto/bot_api.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package grpc.planetwars.bot_api; + +message Hello { + string hello_message = 1; +} + +message HelloResponse { + string response = 1; +} + +service TestService { + rpc greet(Hello) returns (HelloResponse); +} From 0f80b196149a0fb75d84b61c8bbbeb9a71267129 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Wed, 1 Jun 2022 20:19:13 +0200 Subject: [PATCH 02/15] set up stub grpc client --- Cargo.toml | 2 +- planetwars-client/Cargo.toml | 14 ++++++++++++++ planetwars-client/build.rs | 9 +++++++++ planetwars-client/src/main.rs | 21 +++++++++++++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 planetwars-client/Cargo.toml create mode 100644 planetwars-client/build.rs create mode 100644 planetwars-client/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index cebf247..0fe450a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,6 @@ members = [ "planetwars-rules", "planetwars-matchrunner", - "planetwars-cli", "planetwars-server", + "planetwars-client", ] diff --git a/planetwars-client/Cargo.toml b/planetwars-client/Cargo.toml new file mode 100644 index 0000000..10de887 --- /dev/null +++ b/planetwars-client/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "planetwars-client" +version = "0.0.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tokio = { version = "1.15", features = ["full"] } +prost = "0.10" +tonic = "0.7.2" + +[build-dependencies] +tonic-build = "0.7.2" diff --git a/planetwars-client/build.rs b/planetwars-client/build.rs new file mode 100644 index 0000000..acabd08 --- /dev/null +++ b/planetwars-client/build.rs @@ -0,0 +1,9 @@ +extern crate tonic_build; + +fn main() -> Result<(), Box> { + tonic_build::configure() + .build_server(false) + .build_client(true) + .compile(&["../proto/bot_api.proto"], &["../proto"])?; + Ok(()) +} diff --git a/planetwars-client/src/main.rs b/planetwars-client/src/main.rs new file mode 100644 index 0000000..9d9bdab --- /dev/null +++ b/planetwars-client/src/main.rs @@ -0,0 +1,21 @@ +pub mod pb { + tonic::include_proto!("grpc.planetwars.bot_api"); +} + +use pb::test_service_client::TestServiceClient; +use pb::{Hello, HelloResponse}; +use tonic::Response; + +#[tokio::main] +async fn main() { + let mut client = TestServiceClient::connect("http://localhost:50051") + .await + .unwrap(); + let response: Response = client + .greet(Hello { + hello_message: "robbe".to_string(), + }) + .await + .unwrap(); + println!("{}", response.get_ref().response); +} From c3d32e051cfeb1deffffbdfe533d17736f72aeda Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Fri, 3 Jun 2022 21:24:18 +0200 Subject: [PATCH 03/15] basic bot api proto definition --- proto/bot_api.proto | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/proto/bot_api.proto b/proto/bot_api.proto index ad0ee2f..0892270 100644 --- a/proto/bot_api.proto +++ b/proto/bot_api.proto @@ -10,6 +10,17 @@ message HelloResponse { string response = 1; } -service TestService { - rpc greet(Hello) returns (HelloResponse); +message PlayerRequest { + int32 request_id = 1; + bytes content = 2; +} + +message PlayerRequestResponse { + int32 request_id = 1; + bytes content = 2; +} + +service BotApiService { + // server sends requests to the player, player responds + rpc ConnectBot(stream PlayerRequestResponse) returns (stream PlayerRequest); } From 90ecb13a1772dfdab20a006b421102c0aa584f60 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Sun, 5 Jun 2022 21:22:38 +0200 Subject: [PATCH 04/15] baby steps towards a working bot api --- planetwars-client/Cargo.toml | 1 + planetwars-client/src/main.rs | 32 +++-- planetwars-server/Cargo.toml | 1 + planetwars-server/src/modules/bot_api.rs | 150 +++++++++++++++++++++-- 4 files changed, 165 insertions(+), 19 deletions(-) diff --git a/planetwars-client/Cargo.toml b/planetwars-client/Cargo.toml index 10de887..52c3c64 100644 --- a/planetwars-client/Cargo.toml +++ b/planetwars-client/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] tokio = { version = "1.15", features = ["full"] } +tokio-stream = "0.1.9" prost = "0.10" tonic = "0.7.2" diff --git a/planetwars-client/src/main.rs b/planetwars-client/src/main.rs index 9d9bdab..d995ebc 100644 --- a/planetwars-client/src/main.rs +++ b/planetwars-client/src/main.rs @@ -2,20 +2,34 @@ pub mod pb { tonic::include_proto!("grpc.planetwars.bot_api"); } -use pb::test_service_client::TestServiceClient; -use pb::{Hello, HelloResponse}; -use tonic::Response; +use pb::bot_api_service_client::BotApiServiceClient; +use tokio_stream::wrappers::UnboundedReceiverStream; + +use tokio::sync::mpsc; #[tokio::main] async fn main() { - let mut client = TestServiceClient::connect("http://localhost:50051") + let mut client = BotApiServiceClient::connect("http://localhost:50051") .await .unwrap(); - let response: Response = client - .greet(Hello { - hello_message: "robbe".to_string(), + + let (tx, rx) = mpsc::unbounded_channel(); + let mut stream = client + .connect_bot(UnboundedReceiverStream::new(rx)) + .await + .unwrap() + .into_inner(); + while let Some(message) = stream.message().await.unwrap() { + let state = String::from_utf8(message.content).unwrap(); + println!("{}", state); + let response = r#"{ moves: [] }"#; + tx.send(pb::PlayerRequestResponse { + request_id: message.request_id, + content: response.as_bytes().to_vec(), }) - .await .unwrap(); - println!("{}", response.get_ref().response); + } + std::mem::drop(tx); + // for clean exit + std::mem::drop(client); } diff --git a/planetwars-server/Cargo.toml b/planetwars-server/Cargo.toml index 6b96b04..0ceabbc 100644 --- a/planetwars-server/Cargo.toml +++ b/planetwars-server/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] tokio = { version = "1.15", features = ["full"] } +tokio-stream = "0.1.9" hyper = "0.14" axum = { version = "0.4", features = ["json", "headers", "multipart"] } diesel = { version = "1.4.4", features = ["postgres", "chrono"] } diff --git a/planetwars-server/src/modules/bot_api.rs b/planetwars-server/src/modules/bot_api.rs index 1941136..0f1ff82 100644 --- a/planetwars-server/src/modules/bot_api.rs +++ b/planetwars-server/src/modules/bot_api.rs @@ -3,27 +3,157 @@ pub mod pb { } use std::net::SocketAddr; +use std::ops::DerefMut; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use runner::match_context::{EventBus, PlayerHandle, RequestMessage}; +use runner::match_log::MatchLogger; +use tokio::sync::{mpsc, oneshot}; +use tokio_stream::wrappers::UnboundedReceiverStream; use tonic; use tonic::transport::Server; -use tonic::{Request, Response, Status}; +use tonic::{Request, Response, Status, Streaming}; -pub struct BotApiServer {} +use planetwars_matchrunner as runner; + +use crate::db; +use crate::{ConnectionPool, MAPS_DIR, MATCHES_DIR}; + +use super::matches::code_bundle_to_botspec; + +pub struct BotApiServer { + sync_thing: ServerSyncThing, +} #[tonic::async_trait] -impl pb::test_service_server::TestService for BotApiServer { - async fn greet(&self, req: Request) -> Result, Status> { - Ok(Response::new(pb::HelloResponse { - response: format!("hallo {}", req.get_ref().hello_message), - })) +impl pb::bot_api_service_server::BotApiService for BotApiServer { + type ConnectBotStream = UnboundedReceiverStream>; + + async fn connect_bot( + &self, + req: Request>, + ) -> Result, Status> { + println!("bot connected"); + let stream = req.into_inner(); + let sync_data = self.sync_thing.streams.lock().unwrap().take().unwrap(); + sync_data.tx.send(stream).unwrap(); + Ok(Response::new(UnboundedReceiverStream::new( + sync_data.server_messages, + ))) } } -pub async fn run_bot_api() { - let server = BotApiServer {}; +#[derive(Clone)] +struct ServerSyncThing { + streams: Arc>>, +} + +struct SyncThingData { + tx: oneshot::Sender>, + server_messages: mpsc::UnboundedReceiver>, +} + +impl ServerSyncThing { + fn new() -> Self { + ServerSyncThing { + streams: Arc::new(Mutex::new(None)), + } + } +} + +struct RemoteBotSpec { + sync_thing: ServerSyncThing, +} + +#[tonic::async_trait] +impl runner::BotSpec for RemoteBotSpec { + async fn run_bot( + &self, + player_id: u32, + event_bus: Arc>, + _match_logger: MatchLogger, + ) -> Box { + let (tx, rx) = oneshot::channel(); + let (server_msg_snd, server_msg_recv) = mpsc::unbounded_channel(); + *self.sync_thing.streams.lock().unwrap().deref_mut() = Some(SyncThingData { + tx, + server_messages: server_msg_recv, + }); + + let client_messages = rx.await.unwrap(); + tokio::spawn(handle_bot_messages(player_id, event_bus, client_messages)); + + Box::new(RemoteBotHandle { + sender: server_msg_snd, + }) + } +} + +async fn handle_bot_messages( + player_id: u32, + event_bus: Arc>, + mut messages: Streaming, +) { + while let Some(message) = messages.message().await.unwrap() { + let request_id = (player_id, message.request_id as u32); + event_bus + .lock() + .unwrap() + .resolve_request(request_id, Ok(message.content)); + } +} + +struct RemoteBotHandle { + sender: mpsc::UnboundedSender>, +} + +impl PlayerHandle for RemoteBotHandle { + fn send_request(&mut self, r: RequestMessage) { + self.sender + .send(Ok(pb::PlayerRequest { + request_id: r.request_id as i32, + content: r.content, + })) + .unwrap(); + } +} + +async fn run_match(sync_thing: ServerSyncThing, pool: ConnectionPool) { + let conn = pool.get().await.unwrap(); + + let opponent = db::bots::find_bot_by_name("simplebot", &conn).unwrap(); + let opponent_code_bundle = db::bots::active_code_bundle(opponent.id, &conn).unwrap(); + + let log_file_name = "remote_match.log"; + + let remote_bot_spec = RemoteBotSpec { sync_thing }; + + let match_config = runner::MatchConfig { + map_path: PathBuf::from(MAPS_DIR).join("hex.json"), + map_name: "hex".to_string(), + log_path: PathBuf::from(MATCHES_DIR).join(&log_file_name), + players: vec![ + runner::MatchPlayer { + bot_spec: Box::new(remote_bot_spec), + }, + runner::MatchPlayer { + bot_spec: code_bundle_to_botspec(&opponent_code_bundle), + }, + ], + }; + + runner::run_match(match_config).await; +} + +pub async fn run_bot_api(pool: ConnectionPool) { + let sync_thing = ServerSyncThing::new(); + tokio::spawn(run_match(sync_thing.clone(), pool)); + let server = BotApiServer { sync_thing }; + let addr = SocketAddr::from(([127, 0, 0, 1], 50051)); Server::builder() - .add_service(pb::test_service_server::TestServiceServer::new(server)) + .add_service(pb::bot_api_service_server::BotApiServiceServer::new(server)) .serve(addr) .await .unwrap() From d0faec7d1f4deb132554db7f946df4b9d4e9711b Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Mon, 6 Jun 2022 13:08:43 +0200 Subject: [PATCH 05/15] implement PlayerRouter --- planetwars-server/src/modules/bot_api.rs | 71 +++++++++++++++--------- 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/planetwars-server/src/modules/bot_api.rs b/planetwars-server/src/modules/bot_api.rs index 0f1ff82..2face62 100644 --- a/planetwars-server/src/modules/bot_api.rs +++ b/planetwars-server/src/modules/bot_api.rs @@ -2,8 +2,8 @@ pub mod pb { tonic::include_proto!("grpc.planetwars.bot_api"); } +use std::collections::HashMap; use std::net::SocketAddr; -use std::ops::DerefMut; use std::path::PathBuf; use std::sync::{Arc, Mutex}; @@ -23,7 +23,35 @@ use crate::{ConnectionPool, MAPS_DIR, MATCHES_DIR}; use super::matches::code_bundle_to_botspec; pub struct BotApiServer { - sync_thing: ServerSyncThing, + router: PlayerRouter, +} + +/// Routes players to their handler +#[derive(Clone)] +struct PlayerRouter { + routing_table: Arc>>, +} + +impl PlayerRouter { + pub fn new() -> Self { + PlayerRouter { + routing_table: Arc::new(Mutex::new(HashMap::new())), + } + } +} + +// TODO: implement a way to expire entries +impl PlayerRouter { + fn put(&self, player_id: String, entry: SyncThingData) { + let mut routing_table = self.routing_table.lock().unwrap(); + routing_table.insert(player_id, entry); + } + + fn get(&self, player_id: &str) -> Option { + // TODO: this design does not allow for reconnects. Is this desired? + let mut routing_table = self.routing_table.lock().unwrap(); + routing_table.remove(player_id) + } } #[tonic::async_trait] @@ -36,7 +64,8 @@ impl pb::bot_api_service_server::BotApiService for BotApiServer { ) -> Result, Status> { println!("bot connected"); let stream = req.into_inner(); - let sync_data = self.sync_thing.streams.lock().unwrap().take().unwrap(); + // TODO: return error when player does not exist + let sync_data = self.router.get("test_player").unwrap(); sync_data.tx.send(stream).unwrap(); Ok(Response::new(UnboundedReceiverStream::new( sync_data.server_messages, @@ -44,26 +73,13 @@ impl pb::bot_api_service_server::BotApiService for BotApiServer { } } -#[derive(Clone)] -struct ServerSyncThing { - streams: Arc>>, -} - struct SyncThingData { tx: oneshot::Sender>, server_messages: mpsc::UnboundedReceiver>, } -impl ServerSyncThing { - fn new() -> Self { - ServerSyncThing { - streams: Arc::new(Mutex::new(None)), - } - } -} - struct RemoteBotSpec { - sync_thing: ServerSyncThing, + router: PlayerRouter, } #[tonic::async_trait] @@ -76,10 +92,13 @@ impl runner::BotSpec for RemoteBotSpec { ) -> Box { let (tx, rx) = oneshot::channel(); let (server_msg_snd, server_msg_recv) = mpsc::unbounded_channel(); - *self.sync_thing.streams.lock().unwrap().deref_mut() = Some(SyncThingData { - tx, - server_messages: server_msg_recv, - }); + self.router.put( + "test_player".to_string(), + SyncThingData { + tx, + server_messages: server_msg_recv, + }, + ); let client_messages = rx.await.unwrap(); tokio::spawn(handle_bot_messages(player_id, event_bus, client_messages)); @@ -119,7 +138,7 @@ impl PlayerHandle for RemoteBotHandle { } } -async fn run_match(sync_thing: ServerSyncThing, pool: ConnectionPool) { +async fn run_match(router: PlayerRouter, pool: ConnectionPool) { let conn = pool.get().await.unwrap(); let opponent = db::bots::find_bot_by_name("simplebot", &conn).unwrap(); @@ -127,7 +146,7 @@ async fn run_match(sync_thing: ServerSyncThing, pool: ConnectionPool) { let log_file_name = "remote_match.log"; - let remote_bot_spec = RemoteBotSpec { sync_thing }; + let remote_bot_spec = RemoteBotSpec { router }; let match_config = runner::MatchConfig { map_path: PathBuf::from(MAPS_DIR).join("hex.json"), @@ -147,9 +166,9 @@ async fn run_match(sync_thing: ServerSyncThing, pool: ConnectionPool) { } pub async fn run_bot_api(pool: ConnectionPool) { - let sync_thing = ServerSyncThing::new(); - tokio::spawn(run_match(sync_thing.clone(), pool)); - let server = BotApiServer { sync_thing }; + let router = PlayerRouter::new(); + tokio::spawn(run_match(router.clone(), pool)); + let server = BotApiServer { router }; let addr = SocketAddr::from(([127, 0, 0, 1], 50051)); Server::builder() From 2f915af91982073644be94bb2c68e095ffd35596 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Mon, 6 Jun 2022 14:25:56 +0200 Subject: [PATCH 06/15] send player_id through request metadata --- planetwars-client/src/main.rs | 10 +++++++++- planetwars-server/src/modules/bot_api.rs | 19 ++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/planetwars-client/src/main.rs b/planetwars-client/src/main.rs index d995ebc..0fbcdb2 100644 --- a/planetwars-client/src/main.rs +++ b/planetwars-client/src/main.rs @@ -6,13 +6,21 @@ use pb::bot_api_service_client::BotApiServiceClient; use tokio_stream::wrappers::UnboundedReceiverStream; use tokio::sync::mpsc; +use tonic::{metadata::MetadataValue, transport::Channel, Request}; #[tokio::main] async fn main() { - let mut client = BotApiServiceClient::connect("http://localhost:50051") + let channel = Channel::from_static("http://localhost:50051") + .connect() .await .unwrap(); + let mut client = BotApiServiceClient::with_interceptor(channel, |mut req: Request<()>| { + let player_id: MetadataValue<_> = "test_player".parse().unwrap(); + req.metadata_mut().insert("player_id", player_id); + Ok(req) + }); + let (tx, rx) = mpsc::unbounded_channel(); let mut stream = client .connect_bot(UnboundedReceiverStream::new(rx)) diff --git a/planetwars-server/src/modules/bot_api.rs b/planetwars-server/src/modules/bot_api.rs index 2face62..f6e4d5c 100644 --- a/planetwars-server/src/modules/bot_api.rs +++ b/planetwars-server/src/modules/bot_api.rs @@ -62,10 +62,23 @@ impl pb::bot_api_service_server::BotApiService for BotApiServer { &self, req: Request>, ) -> Result, Status> { - println!("bot connected"); + // TODO: clean up errors + let player_id = req + .metadata() + .get("player_id") + .ok_or_else(|| Status::unauthenticated("no player_id provided"))?; + + let player_id_str = player_id + .to_str() + .map_err(|_| Status::invalid_argument("unreadable string"))?; + + let sync_data = self + .router + .get(player_id_str) + .ok_or_else(|| Status::not_found("player_id not found"))?; + let stream = req.into_inner(); - // TODO: return error when player does not exist - let sync_data = self.router.get("test_player").unwrap(); + sync_data.tx.send(stream).unwrap(); Ok(Response::new(UnboundedReceiverStream::new( sync_data.server_messages, From 69421d7b25090724eaa9399f83f83ca36deab882 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Mon, 6 Jun 2022 20:23:01 +0200 Subject: [PATCH 07/15] bot api: handle timeouts and disconnects --- planetwars-server/src/modules/bot_api.rs | 63 +++++++++++++++++++++--- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/planetwars-server/src/modules/bot_api.rs b/planetwars-server/src/modules/bot_api.rs index f6e4d5c..f5aae20 100644 --- a/planetwars-server/src/modules/bot_api.rs +++ b/planetwars-server/src/modules/bot_api.rs @@ -6,8 +6,9 @@ use std::collections::HashMap; use std::net::SocketAddr; use std::path::PathBuf; use std::sync::{Arc, Mutex}; +use std::time::Duration; -use runner::match_context::{EventBus, PlayerHandle, RequestMessage}; +use runner::match_context::{EventBus, PlayerHandle, RequestError, RequestMessage}; use runner::match_log::MatchLogger; use tokio::sync::{mpsc, oneshot}; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -114,10 +115,16 @@ impl runner::BotSpec for RemoteBotSpec { ); let client_messages = rx.await.unwrap(); - tokio::spawn(handle_bot_messages(player_id, event_bus, client_messages)); + tokio::spawn(handle_bot_messages( + player_id, + event_bus.clone(), + client_messages, + )); Box::new(RemoteBotHandle { sender: server_msg_snd, + player_id, + event_bus, }) } } @@ -138,19 +145,59 @@ async fn handle_bot_messages( struct RemoteBotHandle { sender: mpsc::UnboundedSender>, + player_id: u32, + event_bus: Arc>, } impl PlayerHandle for RemoteBotHandle { fn send_request(&mut self, r: RequestMessage) { - self.sender - .send(Ok(pb::PlayerRequest { - request_id: r.request_id as i32, - content: r.content, - })) - .unwrap(); + let res = self.sender.send(Ok(pb::PlayerRequest { + request_id: r.request_id as i32, + content: r.content, + })); + match res { + Ok(()) => { + // schedule a timeout. See comments at method implementation + tokio::spawn(schedule_timeout( + (self.player_id, r.request_id), + r.timeout, + self.event_bus.clone(), + )); + } + Err(_send_error) => { + // cannot contact the remote bot anymore; + // directly mark all requests as timed out. + // TODO: create a dedicated error type for this. + // should it be logged? + self.event_bus + .lock() + .unwrap() + .resolve_request((self.player_id, r.request_id), Err(RequestError::Timeout)); + } + } } } +// TODO: this will spawn a task for every request, which might not be ideal. +// Some alternatives: +// - create a single task that manages all time-outs. +// - intersperse timeouts with incoming client messages +// - push timeouts upwards, into the matchrunner logic (before we hit the playerhandle). +// This was initially not done to allow timer start to be delayed until the message actually arrived +// with the player. Is this still needed, or is there a different way to do this? +// +async fn schedule_timeout( + request_id: (u32, u32), + duration: Duration, + event_bus: Arc>, +) { + tokio::time::sleep(duration).await; + event_bus + .lock() + .unwrap() + .resolve_request(request_id, Err(RequestError::Timeout)); +} + async fn run_match(router: PlayerRouter, pool: ConnectionPool) { let conn = pool.get().await.unwrap(); From ff061f2a7a0e3a62792ffcef8f2cd3ec6ddc5710 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Tue, 7 Jun 2022 19:12:49 +0200 Subject: [PATCH 08/15] timeout when player never connects --- planetwars-server/src/modules/bot_api.rs | 32 +++++++++++++++++------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/planetwars-server/src/modules/bot_api.rs b/planetwars-server/src/modules/bot_api.rs index f5aae20..2fffc79 100644 --- a/planetwars-server/src/modules/bot_api.rs +++ b/planetwars-server/src/modules/bot_api.rs @@ -48,7 +48,7 @@ impl PlayerRouter { routing_table.insert(player_id, entry); } - fn get(&self, player_id: &str) -> Option { + fn take(&self, player_id: &str) -> Option { // TODO: this design does not allow for reconnects. Is this desired? let mut routing_table = self.routing_table.lock().unwrap(); routing_table.remove(player_id) @@ -75,7 +75,7 @@ impl pb::bot_api_service_server::BotApiService for BotApiServer { let sync_data = self .router - .get(player_id_str) + .take(player_id_str) .ok_or_else(|| Status::not_found("player_id not found"))?; let stream = req.into_inner(); @@ -106,21 +106,35 @@ impl runner::BotSpec for RemoteBotSpec { ) -> Box { let (tx, rx) = oneshot::channel(); let (server_msg_snd, server_msg_recv) = mpsc::unbounded_channel(); + let player_key = "test_player".to_string(); self.router.put( - "test_player".to_string(), + player_key.clone(), SyncThingData { tx, server_messages: server_msg_recv, }, ); - let client_messages = rx.await.unwrap(); - tokio::spawn(handle_bot_messages( - player_id, - event_bus.clone(), - client_messages, - )); + let fut = tokio::time::timeout(Duration::from_secs(10), rx); + match fut.await { + Ok(Ok(client_messages)) => { + // let client_messages = rx.await.unwrap(); + tokio::spawn(handle_bot_messages( + player_id, + event_bus.clone(), + client_messages, + )); + } + _ => { + // ensure router cleanup + self.router.take(&player_key); + } + }; + // If the player did not connect, the receiving half of `sender` + // will be dropped here, resulting in a time-out for every turn. + // This is fine for now, but + // TODO: provide a formal mechanism for player startup failure Box::new(RemoteBotHandle { sender: server_msg_snd, player_id, From 028d4a99e4033f9289239600c0dd6ec499a99c04 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Tue, 7 Jun 2022 20:16:42 +0200 Subject: [PATCH 09/15] run bot process in client --- planetwars-client/Cargo.toml | 3 +++ planetwars-client/simplebot.toml | 2 ++ planetwars-client/src/main.rs | 29 +++++++++++++++++++++++------ 3 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 planetwars-client/simplebot.toml diff --git a/planetwars-client/Cargo.toml b/planetwars-client/Cargo.toml index 52c3c64..9c68391 100644 --- a/planetwars-client/Cargo.toml +++ b/planetwars-client/Cargo.toml @@ -10,6 +10,9 @@ tokio = { version = "1.15", features = ["full"] } tokio-stream = "0.1.9" prost = "0.10" tonic = "0.7.2" +serde = { version = "1.0", features = ["derive"] } +toml = "0.5" +planetwars-matchrunner = { path = "../planetwars-matchrunner" } [build-dependencies] tonic-build = "0.7.2" diff --git a/planetwars-client/simplebot.toml b/planetwars-client/simplebot.toml new file mode 100644 index 0000000..dfee25c --- /dev/null +++ b/planetwars-client/simplebot.toml @@ -0,0 +1,2 @@ +name = "simplebot" +command = ["python", "../simplebot/simplebot.py"] diff --git a/planetwars-client/src/main.rs b/planetwars-client/src/main.rs index 0fbcdb2..8840a89 100644 --- a/planetwars-client/src/main.rs +++ b/planetwars-client/src/main.rs @@ -3,13 +3,25 @@ pub mod pb { } use pb::bot_api_service_client::BotApiServiceClient; -use tokio_stream::wrappers::UnboundedReceiverStream; - +use planetwars_matchrunner::bot_runner::Bot; +use serde::Deserialize; +use std::path::PathBuf; use tokio::sync::mpsc; +use tokio_stream::wrappers::UnboundedReceiverStream; use tonic::{metadata::MetadataValue, transport::Channel, Request}; +#[derive(Deserialize)] +struct BotConfig { + #[allow(dead_code)] + name: String, + command: Vec, +} + #[tokio::main] async fn main() { + let content = std::fs::read_to_string("simplebot.toml").unwrap(); + let bot_config: BotConfig = toml::from_str(&content).unwrap(); + let channel = Channel::from_static("http://localhost:50051") .connect() .await @@ -21,6 +33,12 @@ async fn main() { Ok(req) }); + let mut bot_process = Bot { + working_dir: PathBuf::from("."), + argv: bot_config.command, + } + .spawn_process(); + let (tx, rx) = mpsc::unbounded_channel(); let mut stream = client .connect_bot(UnboundedReceiverStream::new(rx)) @@ -28,12 +46,11 @@ async fn main() { .unwrap() .into_inner(); while let Some(message) = stream.message().await.unwrap() { - let state = String::from_utf8(message.content).unwrap(); - println!("{}", state); - let response = r#"{ moves: [] }"#; + let state = std::str::from_utf8(&message.content).unwrap(); + let moves = bot_process.communicate(&message.content).await.unwrap(); tx.send(pb::PlayerRequestResponse { request_id: message.request_id, - content: response.as_bytes().to_vec(), + content: moves.as_bytes().to_vec(), }) .unwrap(); } From 1b2472fbfc876c3f8b6cf5dd6164308123fed133 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Wed, 8 Jun 2022 22:37:38 +0200 Subject: [PATCH 10/15] implement grpc match creation PoC --- planetwars-server/src/modules/bot_api.rs | 86 +++++++++++++++--------- proto/bot_api.proto | 10 +++ 2 files changed, 64 insertions(+), 32 deletions(-) diff --git a/planetwars-server/src/modules/bot_api.rs b/planetwars-server/src/modules/bot_api.rs index 2fffc79..8aa5d29 100644 --- a/planetwars-server/src/modules/bot_api.rs +++ b/planetwars-server/src/modules/bot_api.rs @@ -19,11 +19,13 @@ use tonic::{Request, Response, Status, Streaming}; use planetwars_matchrunner as runner; use crate::db; +use crate::util::gen_alphanumeric; use crate::{ConnectionPool, MAPS_DIR, MATCHES_DIR}; use super::matches::code_bundle_to_botspec; pub struct BotApiServer { + conn_pool: ConnectionPool, router: PlayerRouter, } @@ -85,6 +87,50 @@ impl pb::bot_api_service_server::BotApiService for BotApiServer { sync_data.server_messages, ))) } + + async fn create_match( + &self, + req: Request, + ) -> Result, Status> { + // TODO: unify with matchrunner module + let conn = self.conn_pool.get().await.unwrap(); + + let match_request = req.get_ref(); + + let opponent = db::bots::find_bot_by_name(&match_request.opponent_name, &conn) + .map_err(|_| Status::not_found("opponent not found"))?; + let opponent_code_bundle = db::bots::active_code_bundle(opponent.id, &conn) + .map_err(|_| Status::not_found("opponent has no code"))?; + + let log_file_name = "remote_match.log"; + let player_key = gen_alphanumeric(32); + + let remote_bot_spec = RemoteBotSpec { + player_key: player_key.clone(), + router: self.router.clone(), + }; + + let match_config = runner::MatchConfig { + map_path: PathBuf::from(MAPS_DIR).join("hex.json"), + map_name: "hex".to_string(), + log_path: PathBuf::from(MATCHES_DIR).join(&log_file_name), + players: vec![ + runner::MatchPlayer { + bot_spec: Box::new(remote_bot_spec), + }, + runner::MatchPlayer { + bot_spec: code_bundle_to_botspec(&opponent_code_bundle), + }, + ], + }; + + tokio::spawn(runner::run_match(match_config)); + Ok(Response::new(pb::CreatedMatch { + // TODO + match_id: 0, + player_key, + })) + } } struct SyncThingData { @@ -93,6 +139,7 @@ struct SyncThingData { } struct RemoteBotSpec { + player_key: String, router: PlayerRouter, } @@ -106,9 +153,8 @@ impl runner::BotSpec for RemoteBotSpec { ) -> Box { let (tx, rx) = oneshot::channel(); let (server_msg_snd, server_msg_recv) = mpsc::unbounded_channel(); - let player_key = "test_player".to_string(); self.router.put( - player_key.clone(), + self.player_key.clone(), SyncThingData { tx, server_messages: server_msg_recv, @@ -127,7 +173,7 @@ impl runner::BotSpec for RemoteBotSpec { } _ => { // ensure router cleanup - self.router.take(&player_key); + self.router.take(&self.player_key); } }; @@ -183,6 +229,7 @@ impl PlayerHandle for RemoteBotHandle { // directly mark all requests as timed out. // TODO: create a dedicated error type for this. // should it be logged? + println!("send error: {:?}", _send_error); self.event_bus .lock() .unwrap() @@ -212,37 +259,12 @@ async fn schedule_timeout( .resolve_request(request_id, Err(RequestError::Timeout)); } -async fn run_match(router: PlayerRouter, pool: ConnectionPool) { - let conn = pool.get().await.unwrap(); - - let opponent = db::bots::find_bot_by_name("simplebot", &conn).unwrap(); - let opponent_code_bundle = db::bots::active_code_bundle(opponent.id, &conn).unwrap(); - - let log_file_name = "remote_match.log"; - - let remote_bot_spec = RemoteBotSpec { router }; - - let match_config = runner::MatchConfig { - map_path: PathBuf::from(MAPS_DIR).join("hex.json"), - map_name: "hex".to_string(), - log_path: PathBuf::from(MATCHES_DIR).join(&log_file_name), - players: vec![ - runner::MatchPlayer { - bot_spec: Box::new(remote_bot_spec), - }, - runner::MatchPlayer { - bot_spec: code_bundle_to_botspec(&opponent_code_bundle), - }, - ], - }; - - runner::run_match(match_config).await; -} - pub async fn run_bot_api(pool: ConnectionPool) { let router = PlayerRouter::new(); - tokio::spawn(run_match(router.clone(), pool)); - let server = BotApiServer { router }; + let server = BotApiServer { + router, + conn_pool: pool.clone(), + }; let addr = SocketAddr::from(([127, 0, 0, 1], 50051)); Server::builder() diff --git a/proto/bot_api.proto b/proto/bot_api.proto index 0892270..08839f0 100644 --- a/proto/bot_api.proto +++ b/proto/bot_api.proto @@ -20,7 +20,17 @@ message PlayerRequestResponse { bytes content = 2; } +message MatchRequest { + string opponent_name = 1; +} + +message CreatedMatch { + int32 match_id = 1; + string player_key = 2; +} + service BotApiService { + rpc CreateMatch(MatchRequest) returns (CreatedMatch); // server sends requests to the player, player responds rpc ConnectBot(stream PlayerRequestResponse) returns (stream PlayerRequest); } From e3cf0df4509fe5cb0ad114040f52f61436a6663f Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Wed, 8 Jun 2022 23:24:32 +0200 Subject: [PATCH 11/15] update client to request matches --- planetwars-client/src/main.rs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/planetwars-client/src/main.rs b/planetwars-client/src/main.rs index 8840a89..9df100d 100644 --- a/planetwars-client/src/main.rs +++ b/planetwars-client/src/main.rs @@ -5,10 +5,10 @@ pub mod pb { use pb::bot_api_service_client::BotApiServiceClient; use planetwars_matchrunner::bot_runner::Bot; use serde::Deserialize; -use std::path::PathBuf; +use std::{path::PathBuf, time::Duration}; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; -use tonic::{metadata::MetadataValue, transport::Channel, Request}; +use tonic::{metadata::MetadataValue, transport::Channel, Request, Status}; #[derive(Deserialize)] struct BotConfig { @@ -27,8 +27,24 @@ async fn main() { .await .unwrap(); + let created_match = create_match(channel.clone()).await.unwrap(); + run_player(bot_config, created_match.player_key, channel).await; + tokio::time::sleep(Duration::from_secs(1)).await; +} + +async fn create_match(channel: Channel) -> Result { + let mut client = BotApiServiceClient::new(channel); + let res = client + .create_match(Request::new(pb::MatchRequest { + opponent_name: "simplebot".to_string(), + })) + .await; + res.map(|response| response.into_inner()) +} + +async fn run_player(bot_config: BotConfig, player_key: String, channel: Channel) { let mut client = BotApiServiceClient::with_interceptor(channel, |mut req: Request<()>| { - let player_id: MetadataValue<_> = "test_player".parse().unwrap(); + let player_id: MetadataValue<_> = player_key.parse().unwrap(); req.metadata_mut().insert("player_id", player_id); Ok(req) }); @@ -46,7 +62,6 @@ async fn main() { .unwrap() .into_inner(); while let Some(message) = stream.message().await.unwrap() { - let state = std::str::from_utf8(&message.content).unwrap(); let moves = bot_process.communicate(&message.content).await.unwrap(); tx.send(pb::PlayerRequestResponse { request_id: message.request_id, @@ -54,7 +69,4 @@ async fn main() { }) .unwrap(); } - std::mem::drop(tx); - // for clean exit - std::mem::drop(client); } From d1977b95c82f608bc558432cdfba8026aaf0648d Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Thu, 9 Jun 2022 20:57:45 +0200 Subject: [PATCH 12/15] consistently use player_key and player_id --- planetwars-client/src/main.rs | 4 ++-- planetwars-server/src/modules/bot_api.rs | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/planetwars-client/src/main.rs b/planetwars-client/src/main.rs index 9df100d..3ece5b3 100644 --- a/planetwars-client/src/main.rs +++ b/planetwars-client/src/main.rs @@ -44,8 +44,8 @@ async fn create_match(channel: Channel) -> Result { async fn run_player(bot_config: BotConfig, player_key: String, channel: Channel) { let mut client = BotApiServiceClient::with_interceptor(channel, |mut req: Request<()>| { - let player_id: MetadataValue<_> = player_key.parse().unwrap(); - req.metadata_mut().insert("player_id", player_id); + let player_key: MetadataValue<_> = player_key.parse().unwrap(); + req.metadata_mut().insert("player_key", player_key); Ok(req) }); diff --git a/planetwars-server/src/modules/bot_api.rs b/planetwars-server/src/modules/bot_api.rs index 8aa5d29..4eb13c1 100644 --- a/planetwars-server/src/modules/bot_api.rs +++ b/planetwars-server/src/modules/bot_api.rs @@ -45,15 +45,15 @@ impl PlayerRouter { // TODO: implement a way to expire entries impl PlayerRouter { - fn put(&self, player_id: String, entry: SyncThingData) { + fn put(&self, player_key: String, entry: SyncThingData) { let mut routing_table = self.routing_table.lock().unwrap(); - routing_table.insert(player_id, entry); + routing_table.insert(player_key, entry); } - fn take(&self, player_id: &str) -> Option { + fn take(&self, player_key: &str) -> Option { // TODO: this design does not allow for reconnects. Is this desired? let mut routing_table = self.routing_table.lock().unwrap(); - routing_table.remove(player_id) + routing_table.remove(player_key) } } @@ -66,19 +66,19 @@ impl pb::bot_api_service_server::BotApiService for BotApiServer { req: Request>, ) -> Result, Status> { // TODO: clean up errors - let player_id = req + let player_key = req .metadata() - .get("player_id") - .ok_or_else(|| Status::unauthenticated("no player_id provided"))?; + .get("player_key") + .ok_or_else(|| Status::unauthenticated("no player_key provided"))?; - let player_id_str = player_id + let player_key_str = player_key .to_str() .map_err(|_| Status::invalid_argument("unreadable string"))?; let sync_data = self .router - .take(player_id_str) - .ok_or_else(|| Status::not_found("player_id not found"))?; + .take(player_key_str) + .ok_or_else(|| Status::not_found("player_key not found"))?; let stream = req.into_inner(); From 5ee66c9c9b4156692c739a861c9cdbaf0c65aec8 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Fri, 10 Jun 2022 21:09:33 +0200 Subject: [PATCH 13/15] allow match_player code_bundle_id to be null --- .../down.sql | 1 + .../up.sql | 1 + planetwars-server/src/db/matches.rs | 23 +++++++++++++------ planetwars-server/src/routes/bots.rs | 2 +- planetwars-server/src/routes/demo.rs | 4 ++-- planetwars-server/src/routes/matches.rs | 4 ++-- planetwars-server/src/schema.rs | 2 +- 7 files changed, 24 insertions(+), 13 deletions(-) create mode 100644 planetwars-server/migrations/2022-06-10-180418_nullable_match_player_code_bundle/down.sql create mode 100644 planetwars-server/migrations/2022-06-10-180418_nullable_match_player_code_bundle/up.sql diff --git a/planetwars-server/migrations/2022-06-10-180418_nullable_match_player_code_bundle/down.sql b/planetwars-server/migrations/2022-06-10-180418_nullable_match_player_code_bundle/down.sql new file mode 100644 index 0000000..bb0b613 --- /dev/null +++ b/planetwars-server/migrations/2022-06-10-180418_nullable_match_player_code_bundle/down.sql @@ -0,0 +1 @@ +ALTER TABLE match_players ALTER COLUMN code_bundle_id SET NOT NULL; diff --git a/planetwars-server/migrations/2022-06-10-180418_nullable_match_player_code_bundle/up.sql b/planetwars-server/migrations/2022-06-10-180418_nullable_match_player_code_bundle/up.sql new file mode 100644 index 0000000..86ab65d --- /dev/null +++ b/planetwars-server/migrations/2022-06-10-180418_nullable_match_player_code_bundle/up.sql @@ -0,0 +1 @@ +ALTER TABLE match_players ALTER COLUMN code_bundle_id DROP NOT NULL; diff --git a/planetwars-server/src/db/matches.rs b/planetwars-server/src/db/matches.rs index ee25e85..6ec1389 100644 --- a/planetwars-server/src/db/matches.rs +++ b/planetwars-server/src/db/matches.rs @@ -44,7 +44,7 @@ pub struct MatchBase { pub struct MatchPlayer { pub match_id: i32, pub player_id: i32, - pub code_bundle_id: i32, + pub code_bundle_id: Option, } pub struct MatchPlayerData { @@ -92,7 +92,10 @@ pub fn list_matches(conn: &PgConnection) -> QueryResult> { let matches = matches::table.get_results::(conn)?; let match_players = MatchPlayer::belonging_to(&matches) - .inner_join(code_bundles::table) + .left_join( + code_bundles::table + .on(match_players::code_bundle_id.eq(code_bundles::id.nullable())), + ) .left_join(bots::table.on(code_bundles::bot_id.eq(bots::id.nullable()))) .load::(conn)? .grouped_by(&matches); @@ -120,7 +123,7 @@ pub struct FullMatchData { // #[primary_key(base.match_id, base::player_id)] pub struct FullMatchPlayerData { pub base: MatchPlayer, - pub code_bundle: CodeBundle, + pub code_bundle: Option, pub bot: Option, } @@ -142,7 +145,10 @@ pub fn find_match(id: i32, conn: &PgConnection) -> QueryResult { let match_base = matches::table.find(id).get_result::(conn)?; let match_players = MatchPlayer::belonging_to(&match_base) - .inner_join(code_bundles::table) + .left_join( + code_bundles::table + .on(match_players::code_bundle_id.eq(code_bundles::id.nullable())), + ) .left_join(bots::table.on(code_bundles::bot_id.eq(bots::id.nullable()))) .load::(conn)?; @@ -160,14 +166,17 @@ pub fn find_match_base(id: i32, conn: &PgConnection) -> QueryResult { } pub enum MatchResult { - Finished { winner: Option } + Finished { winner: Option }, } pub fn save_match_result(id: i32, result: MatchResult, conn: &PgConnection) -> QueryResult<()> { let MatchResult::Finished { winner } = result; diesel::update(matches::table.find(id)) - .set((matches::winner.eq(winner), matches::state.eq(MatchState::Finished))) + .set(( + matches::winner.eq(winner), + matches::state.eq(MatchState::Finished), + )) .execute(conn)?; Ok(()) -} \ No newline at end of file +} diff --git a/planetwars-server/src/routes/bots.rs b/planetwars-server/src/routes/bots.rs index 3bbaa1a..df0c4d0 100644 --- a/planetwars-server/src/routes/bots.rs +++ b/planetwars-server/src/routes/bots.rs @@ -12,7 +12,7 @@ use std::path::PathBuf; use thiserror; use crate::db::bots::{self, CodeBundle}; -use crate::db::ratings::{RankedBot, self}; +use crate::db::ratings::{self, RankedBot}; use crate::db::users::User; use crate::modules::bots::save_code_bundle; use crate::{DatabaseConnection, BOTS_DIR}; diff --git a/planetwars-server/src/routes/demo.rs b/planetwars-server/src/routes/demo.rs index 7f7ba71..3318dfd 100644 --- a/planetwars-server/src/routes/demo.rs +++ b/planetwars-server/src/routes/demo.rs @@ -58,12 +58,12 @@ pub async fn submit_bot( match_players: vec![ FullMatchPlayerData { base: match_data.match_players[0].clone(), - code_bundle: player_code_bundle, + code_bundle: Some(player_code_bundle), bot: None, }, FullMatchPlayerData { base: match_data.match_players[1].clone(), - code_bundle: opponent_code_bundle, + code_bundle: Some(opponent_code_bundle), bot: Some(opponent), }, ], diff --git a/planetwars-server/src/routes/matches.rs b/planetwars-server/src/routes/matches.rs index b61008d..7169ebe 100644 --- a/planetwars-server/src/routes/matches.rs +++ b/planetwars-server/src/routes/matches.rs @@ -107,7 +107,7 @@ pub struct ApiMatch { #[derive(Serialize, Deserialize)] pub struct ApiMatchPlayer { - code_bundle_id: i32, + code_bundle_id: Option, bot_id: Option, bot_name: Option, } @@ -127,7 +127,7 @@ pub fn match_data_to_api(data: matches::FullMatchData) -> ApiMatch { .match_players .iter() .map(|_p| ApiMatchPlayer { - code_bundle_id: _p.code_bundle.id, + code_bundle_id: _p.code_bundle.as_ref().map(|cb| cb.id), bot_id: _p.bot.as_ref().map(|b| b.id), bot_name: _p.bot.as_ref().map(|b| b.name.clone()), }) diff --git a/planetwars-server/src/schema.rs b/planetwars-server/src/schema.rs index be3e858..92acc8e 100644 --- a/planetwars-server/src/schema.rs +++ b/planetwars-server/src/schema.rs @@ -31,7 +31,7 @@ table! { match_players (match_id, player_id) { match_id -> Int4, player_id -> Int4, - code_bundle_id -> Int4, + code_bundle_id -> Nullable, } } From a3766980735851e9aa4b56a80e91c0b77cf63adb Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Fri, 10 Jun 2022 21:49:32 +0200 Subject: [PATCH 14/15] update RunMatch helper to allow for remote bots --- planetwars-server/src/db/matches.rs | 4 +- planetwars-server/src/modules/matches.rs | 50 +++++++++++++++++------- planetwars-server/src/modules/ranking.rs | 9 +++-- planetwars-server/src/routes/demo.rs | 7 +++- planetwars-server/src/routes/matches.rs | 2 +- 5 files changed, 50 insertions(+), 22 deletions(-) diff --git a/planetwars-server/src/db/matches.rs b/planetwars-server/src/db/matches.rs index 6ec1389..54fd113 100644 --- a/planetwars-server/src/db/matches.rs +++ b/planetwars-server/src/db/matches.rs @@ -25,7 +25,7 @@ pub struct NewMatchPlayer { /// player id within the match pub player_id: i32, /// id of the bot behind this player - pub code_bundle_id: i32, + pub code_bundle_id: Option, } #[derive(Queryable, Identifiable)] @@ -48,7 +48,7 @@ pub struct MatchPlayer { } pub struct MatchPlayerData { - pub code_bundle_id: i32, + pub code_bundle_id: Option, } pub fn create_match( diff --git a/planetwars-server/src/modules/matches.rs b/planetwars-server/src/modules/matches.rs index a254bac..6d9261d 100644 --- a/planetwars-server/src/modules/matches.rs +++ b/planetwars-server/src/modules/matches.rs @@ -16,32 +16,54 @@ use crate::{ const PYTHON_IMAGE: &str = "python:3.10-slim-buster"; -pub struct RunMatch<'a> { +pub struct RunMatch { log_file_name: String, - player_code_bundles: Vec<&'a db::bots::CodeBundle>, + players: Vec, match_id: Option, } -impl<'a> RunMatch<'a> { - pub fn from_players(player_code_bundles: Vec<&'a db::bots::CodeBundle>) -> Self { +pub struct MatchPlayer { + bot_spec: Box, + // meta that will be passed on to database + code_bundle_id: Option, +} + +impl MatchPlayer { + pub fn from_code_bundle(code_bundle: &db::bots::CodeBundle) -> Self { + MatchPlayer { + bot_spec: code_bundle_to_botspec(code_bundle), + code_bundle_id: Some(code_bundle.id), + } + } + + pub fn from_bot_spec(bot_spec: Box) -> Self { + MatchPlayer { + bot_spec, + code_bundle_id: None, + } + } +} + +impl RunMatch { + pub fn from_players(players: Vec) -> Self { let log_file_name = format!("{}.log", gen_alphanumeric(16)); RunMatch { log_file_name, - player_code_bundles, + players, match_id: None, } } - pub fn runner_config(&self) -> runner::MatchConfig { + pub fn into_runner_config(self) -> runner::MatchConfig { runner::MatchConfig { map_path: PathBuf::from(MAPS_DIR).join("hex.json"), map_name: "hex".to_string(), log_path: PathBuf::from(MATCHES_DIR).join(&self.log_file_name), players: self - .player_code_bundles - .iter() - .map(|b| runner::MatchPlayer { - bot_spec: code_bundle_to_botspec(b), + .players + .into_iter() + .map(|player| runner::MatchPlayer { + bot_spec: player.bot_spec, }) .collect(), } @@ -56,10 +78,10 @@ impl<'a> RunMatch<'a> { log_path: &self.log_file_name, }; let new_match_players = self - .player_code_bundles + .players .iter() - .map(|b| db::matches::MatchPlayerData { - code_bundle_id: b.id, + .map(|p| db::matches::MatchPlayerData { + code_bundle_id: p.code_bundle_id, }) .collect::>(); @@ -70,7 +92,7 @@ impl<'a> RunMatch<'a> { pub fn spawn(self, pool: ConnectionPool) -> JoinHandle { let match_id = self.match_id.expect("match must be saved before running"); - let runner_config = self.runner_config(); + let runner_config = self.into_runner_config(); tokio::spawn(run_match_task(pool, runner_config, match_id)) } } diff --git a/planetwars-server/src/modules/ranking.rs b/planetwars-server/src/modules/ranking.rs index f76fbae..d83debb 100644 --- a/planetwars-server/src/modules/ranking.rs +++ b/planetwars-server/src/modules/ranking.rs @@ -1,7 +1,7 @@ use crate::{db::bots::Bot, DbPool}; use crate::db; -use crate::modules::matches::RunMatch; +use crate::modules::matches::{MatchPlayer, RunMatch}; use rand::seq::SliceRandom; use std::time::Duration; use tokio; @@ -43,9 +43,12 @@ async fn play_ranking_match(selected_bots: Vec, db_pool: DbPool) { code_bundles.push(code_bundle); } - let code_bundle_refs = code_bundles.iter().map(|b| b).collect::>(); + let players = code_bundles + .iter() + .map(MatchPlayer::from_code_bundle) + .collect::>(); - let mut run_match = RunMatch::from_players(code_bundle_refs); + let mut run_match = RunMatch::from_players(players); run_match .store_in_database(&db_conn) .expect("could not store match in db"); diff --git a/planetwars-server/src/routes/demo.rs b/planetwars-server/src/routes/demo.rs index 3318dfd..33dc02d 100644 --- a/planetwars-server/src/routes/demo.rs +++ b/planetwars-server/src/routes/demo.rs @@ -1,7 +1,7 @@ use crate::db; use crate::db::matches::{FullMatchData, FullMatchPlayerData}; use crate::modules::bots::save_code_bundle; -use crate::modules::matches::RunMatch; +use crate::modules::matches::{MatchPlayer, RunMatch}; use crate::ConnectionPool; use axum::extract::Extension; use axum::Json; @@ -46,7 +46,10 @@ pub async fn submit_bot( // TODO: can we recover from this? .expect("could not save bot code"); - let mut run_match = RunMatch::from_players(vec![&player_code_bundle, &opponent_code_bundle]); + let mut run_match = RunMatch::from_players(vec![ + MatchPlayer::from_code_bundle(&player_code_bundle), + MatchPlayer::from_code_bundle(&opponent_code_bundle), + ]); let match_data = run_match .store_in_database(&conn) .expect("failed to save match"); diff --git a/planetwars-server/src/routes/matches.rs b/planetwars-server/src/routes/matches.rs index 7169ebe..874c775 100644 --- a/planetwars-server/src/routes/matches.rs +++ b/planetwars-server/src/routes/matches.rs @@ -61,7 +61,7 @@ pub async fn play_match( }); bot_ids.push(matches::MatchPlayerData { - code_bundle_id: code_bundle.id, + code_bundle_id: Some(code_bundle.id), }); } From 7a3b801f58752a78b65e3e7e7b998b6479f980f7 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Sat, 11 Jun 2022 17:50:44 +0200 Subject: [PATCH 15/15] use RunMatch in bot_api service --- planetwars-server/src/modules/bot_api.rs | 43 +++++++++++------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/planetwars-server/src/modules/bot_api.rs b/planetwars-server/src/modules/bot_api.rs index 4eb13c1..0ecbf71 100644 --- a/planetwars-server/src/modules/bot_api.rs +++ b/planetwars-server/src/modules/bot_api.rs @@ -4,7 +4,6 @@ pub mod pb { use std::collections::HashMap; use std::net::SocketAddr; -use std::path::PathBuf; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -20,9 +19,9 @@ use planetwars_matchrunner as runner; use crate::db; use crate::util::gen_alphanumeric; -use crate::{ConnectionPool, MAPS_DIR, MATCHES_DIR}; +use crate::ConnectionPool; -use super::matches::code_bundle_to_botspec; +use super::matches::{MatchPlayer, RunMatch}; pub struct BotApiServer { conn_pool: ConnectionPool, @@ -43,6 +42,12 @@ impl PlayerRouter { } } +impl Default for PlayerRouter { + fn default() -> Self { + Self::new() + } +} + // TODO: implement a way to expire entries impl PlayerRouter { fn put(&self, player_key: String, entry: SyncThingData) { @@ -102,37 +107,29 @@ impl pb::bot_api_service_server::BotApiService for BotApiServer { let opponent_code_bundle = db::bots::active_code_bundle(opponent.id, &conn) .map_err(|_| Status::not_found("opponent has no code"))?; - let log_file_name = "remote_match.log"; let player_key = gen_alphanumeric(32); - let remote_bot_spec = RemoteBotSpec { + let remote_bot_spec = Box::new(RemoteBotSpec { player_key: player_key.clone(), router: self.router.clone(), - }; + }); + let mut run_match = RunMatch::from_players(vec![ + MatchPlayer::from_bot_spec(remote_bot_spec), + MatchPlayer::from_code_bundle(&opponent_code_bundle), + ]); + let created_match = run_match + .store_in_database(&conn) + .expect("failed to save match"); + run_match.spawn(self.conn_pool.clone()); - let match_config = runner::MatchConfig { - map_path: PathBuf::from(MAPS_DIR).join("hex.json"), - map_name: "hex".to_string(), - log_path: PathBuf::from(MATCHES_DIR).join(&log_file_name), - players: vec![ - runner::MatchPlayer { - bot_spec: Box::new(remote_bot_spec), - }, - runner::MatchPlayer { - bot_spec: code_bundle_to_botspec(&opponent_code_bundle), - }, - ], - }; - - tokio::spawn(runner::run_match(match_config)); Ok(Response::new(pb::CreatedMatch { - // TODO - match_id: 0, + match_id: created_match.base.id, player_key, })) } } +// TODO: please rename me struct SyncThingData { tx: oneshot::Sender>, server_messages: mpsc::UnboundedReceiver>,