From 78797fde6d1ee2730a35da10590a7cedf2f26442 Mon Sep 17 00:00:00 2001 From: Pavel Mikhalkevich Date: Sat, 28 Dec 2024 14:05:56 +0400 Subject: [PATCH 01/10] Add seeding to dynamodb local arm in main --- server/src/ask.rs | 14 +--- server/src/main.rs | 176 +++++++++++++++++++++++++++++++++++++++++---- server/src/new.rs | 11 +-- 3 files changed, 168 insertions(+), 33 deletions(-) diff --git a/server/src/ask.rs b/server/src/ask.rs index 4878075..6c25f51 100644 --- a/server/src/ask.rs +++ b/server/src/ask.rs @@ -1,5 +1,5 @@ use super::{Backend, Local}; -use crate::to_dynamo_timestamp; +use crate::{to_dynamo_timestamp, QUESTIONS_TTL}; use aws_sdk_dynamodb::{ error::SdkError, operation::put_item::{PutItemError, PutItemOutput}, @@ -9,17 +9,12 @@ use axum::extract::{Path, State}; use axum::response::Json; use http::StatusCode; use serde::Deserialize; -use std::{ - collections::HashMap, - time::{Duration, SystemTime}, -}; +use std::{collections::HashMap, time::SystemTime}; use ulid::Ulid; #[allow(unused_imports)] use tracing::{debug, error, info, trace, warn}; -const QUESTIONS_EXPIRE_AFTER_DAYS: u64 = 30; - impl Backend { pub(super) async fn ask( &self, @@ -35,10 +30,7 @@ impl Backend { ("when", to_dynamo_timestamp(SystemTime::now())), ( "expire", - to_dynamo_timestamp( - SystemTime::now() - + Duration::from_secs(QUESTIONS_EXPIRE_AFTER_DAYS * 24 * 60 * 60), - ), + to_dynamo_timestamp(SystemTime::now() + QUESTIONS_TTL), ), ("hidden", AttributeValue::Bool(false)), ]; diff --git a/server/src/main.rs b/server/src/main.rs index d8ac3ca..0ceb25a 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -6,7 +6,7 @@ use axum::Router; use http::StatusCode; use http_body_util::BodyExt; use lambda_http::Error; -use std::time::SystemTime; +use std::time::{Duration, SystemTime}; use std::{ collections::HashMap, future::Future, @@ -19,12 +19,32 @@ use tower_service::Service; use tracing_subscriber::EnvFilter; use ulid::Ulid; +const QUESTIONS_EXPIRE_AFTER_DAYS: u64 = 30; +const QUESTIONS_TTL: Duration = Duration::from_secs(QUESTIONS_EXPIRE_AFTER_DAYS * 24 * 60 * 60); + +const EVENTS_EXPIRE_AFTER_DAYS: u64 = 60; +const EVENTS_TTL: Duration = Duration::from_secs(EVENTS_EXPIRE_AFTER_DAYS * 24 * 60 * 60); + #[allow(unused_imports)] use tracing::{debug, error, info, trace, warn}; #[cfg(debug_assertions)] const SEED: &str = include_str!("test.json"); +#[cfg(debug_assertions)] +use serde::Deserialize; + +#[cfg(debug_assertions)] +#[derive(Deserialize)] +struct LiveAskQuestion { + likes: usize, + text: String, + hidden: bool, + answered: bool, + #[serde(rename = "createTimeUnix")] + created: usize, +} + #[derive(Clone, Debug)] #[allow(dead_code)] enum Backend { @@ -171,23 +191,151 @@ async fn main() -> Result<(), Error> { let backend = Backend::dynamo().await; #[cfg(debug_assertions)] let backend = if std::env::var_os("USE_DYNAMODB").is_some() { - Backend::dynamo().await + use aws_sdk_dynamodb::types::{PutRequest, WriteRequest}; + use rand::prelude::SliceRandom; + + let mut backend = Backend::dynamo().await; + let qids: Vec = if let Backend::Dynamo(ref mut client) = backend { + info!("going to seed test event"); + let seed: Vec = serde_json::from_str(SEED).unwrap(); + let seed_e = Ulid::from_string("00000000000000000000000000").unwrap(); + match client + .put_item() + .table_name("events") + .condition_expression("attribute_not_exists(id)") + .item("id", AttributeValue::S(seed_e.to_string())) + .item("secret", AttributeValue::S("secret".into())) + .item("when", to_dynamo_timestamp(SystemTime::now())) + .item( + "expire", + to_dynamo_timestamp(SystemTime::now() + EVENTS_TTL), + ) + .send() + .await + { + Err(ref error @ SdkError::ServiceError(ref e)) => { + if e.err().is_conditional_check_failed_exception() { + warn!("test event is already there, skipping seeding questions"); + } else { + panic!("failed to seed test event {:?}", error) + } + } + Err(e) => panic!("failed to seed test event {:?}", e), + Ok(_) => { + info!("successfully registered test event, going to seed questions now"); + // DynamoDB supports batch write operations with `25` as max batch size + // https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html + for chunk in seed.chunks(25) { + client + .batch_write_item() + .request_items( + "questions", + chunk + .iter() + .map( + |LiveAskQuestion { + likes, + text, + hidden, + answered, + created, + }| { + let mut item = HashMap::from([ + ( + "id".to_string(), + AttributeValue::S( + ulid::Ulid::new().to_string(), + ), + ), + ( + "eid".to_string(), + AttributeValue::S(seed_e.to_string()), + ), + ( + "votes".to_string(), + AttributeValue::N(likes.to_string()), + ), + ( + "text".to_string(), + AttributeValue::S(text.clone()), + ), + ( + "when".to_string(), + AttributeValue::N(created.to_string()), + ), + ( + "expire".to_string(), + to_dynamo_timestamp( + SystemTime::now() + QUESTIONS_TTL, + ), + ), + ( + "hidden".to_string(), + AttributeValue::Bool(*hidden), + ), + ]); + if *answered { + item.insert( + "answered".to_string(), + to_dynamo_timestamp(SystemTime::now()), + ); + } + WriteRequest::builder() + .put_request( + PutRequest::builder() + .set_item(Some(item)) + .build() + .expect("request to have been built ok"), + ) + .build() + }, + ) + .collect::>(), + ) + .send() + .await + .expect("batch to have been written ok"); + } + info!("successfully registered questions"); + } + } + + client + .query() + .table_name("questions") + .index_name("top") + .key_condition_expression("eid = :eid") + .expression_attribute_values(":eid", AttributeValue::S(seed_e.to_string())) + .send() + .await + .expect("scanned index ok") + .items() + .iter() + .filter_map(|item| { + let id = item + .get("id") + .expect("id is in projection") + .as_s() + .expect("id is of type string"); + ulid::Ulid::from_string(id).ok() + }) + .collect() + } else { + unreachable!() + }; + let cheat = backend.clone(); + tokio::spawn(async move { + loop { + tokio::time::sleep(Duration::from_secs(1)).await; + let qid = qids.choose(&mut rand::thread_rng()).unwrap(); + let _ = cheat.vote(qid, vote::UpDown::Up).await; + } + }); + backend } else { use rand::prelude::SliceRandom; - use serde::Deserialize; use std::time::Duration; - #[cfg(debug_assertions)] - #[derive(Deserialize)] - struct LiveAskQuestion { - likes: usize, - text: String, - hidden: bool, - answered: bool, - #[serde(rename = "createTimeUnix")] - created: usize, - } - let mut state = Local::default(); let seed: Vec = serde_json::from_str(SEED).unwrap(); let seed_e = "00000000000000000000000000"; diff --git a/server/src/new.rs b/server/src/new.rs index 0722392..8e77328 100644 --- a/server/src/new.rs +++ b/server/src/new.rs @@ -1,4 +1,4 @@ -use crate::to_dynamo_timestamp; +use crate::{to_dynamo_timestamp, EVENTS_TTL}; use super::{Backend, Local}; use aws_sdk_dynamodb::{ @@ -11,14 +11,12 @@ use axum::response::Json; use http::StatusCode; use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; -use std::time::{Duration, SystemTime}; +use std::time::SystemTime; use ulid::Ulid; #[allow(unused_imports)] use tracing::{debug, error, info, trace, warn}; -const EVENTS_EXPIRE_AFTER_DAYS: u64 = 60; - impl Backend { #[allow(clippy::wrong_self_convention)] #[allow(clippy::new_ret_no_self)] @@ -37,10 +35,7 @@ impl Backend { .item("when", to_dynamo_timestamp(SystemTime::now())) .item( "expire", - to_dynamo_timestamp( - SystemTime::now() - + Duration::from_secs(EVENTS_EXPIRE_AFTER_DAYS * 24 * 60 * 60), - ), + to_dynamo_timestamp(SystemTime::now() + EVENTS_TTL), ) .send() .await From cfb3e4b748ca2de201dd90e208a70b6aedc3563e Mon Sep 17 00:00:00 2001 From: Pavel Mikhalkevich Date: Sat, 28 Dec 2024 19:40:23 +0400 Subject: [PATCH 02/10] Re-use votes autogeneration logic --- server/src/main.rs | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/server/src/main.rs b/server/src/main.rs index 0ceb25a..ea9cb89 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -190,9 +190,8 @@ async fn main() -> Result<(), Error> { #[cfg(not(debug_assertions))] let backend = Backend::dynamo().await; #[cfg(debug_assertions)] - let backend = if std::env::var_os("USE_DYNAMODB").is_some() { + let (backend, qids) = if std::env::var_os("USE_DYNAMODB").is_some() { use aws_sdk_dynamodb::types::{PutRequest, WriteRequest}; - use rand::prelude::SliceRandom; let mut backend = Backend::dynamo().await; let qids: Vec = if let Backend::Dynamo(ref mut client) = backend { @@ -299,7 +298,8 @@ async fn main() -> Result<(), Error> { info!("successfully registered questions"); } } - + // let's collect ids of the questions related to the test event, + // we can then use them to auto-generate user votes over time client .query() .table_name("questions") @@ -323,19 +323,8 @@ async fn main() -> Result<(), Error> { } else { unreachable!() }; - let cheat = backend.clone(); - tokio::spawn(async move { - loop { - tokio::time::sleep(Duration::from_secs(1)).await; - let qid = qids.choose(&mut rand::thread_rng()).unwrap(); - let _ = cheat.vote(qid, vote::UpDown::Up).await; - } - }); - backend + (backend, qids) } else { - use rand::prelude::SliceRandom; - use std::time::Duration; - let mut state = Local::default(); let seed: Vec = serde_json::from_str(SEED).unwrap(); let seed_e = "00000000000000000000000000"; @@ -377,16 +366,22 @@ async fn main() -> Result<(), Error> { qids.push(qid); } } - let cheat = state.clone(); + (state, qids) + }; + + // to aid in development, auto-generate user votes over time + #[cfg(debug_assertions)] + { + use rand::prelude::SliceRandom; + let backend = backend.clone(); tokio::spawn(async move { loop { tokio::time::sleep(Duration::from_secs(1)).await; let qid = qids.choose(&mut rand::thread_rng()).unwrap(); - let _ = cheat.vote(qid, vote::UpDown::Up).await; + let _ = backend.vote(qid, vote::UpDown::Up).await; } }); - state - }; + } let app = Router::new() .route("/api/event", post(new::new)) From 377a8a8ae591e31fe91f882fa715b9a5f00cbbe7 Mon Sep 17 00:00:00 2001 From: Pavel Mikhalkevich Date: Sat, 28 Dec 2024 22:47:44 +0400 Subject: [PATCH 03/10] Update test event url in docs --- CONTRIBUTING.md | 2 +- server/src/main.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ff634de..97d0a8d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,7 +25,7 @@ auto-update. If you modify files under `server/`, you'll have to re-run Note that when run this way, to aid in development, the server will auto-populate an event with a set of questions from a past live Q&A session I ran at -. +. It will also auto-generate user votes over time for the questions there. If you're curious about the technologies used in the server and client, diff --git a/server/src/main.rs b/server/src/main.rs index ea9cb89..cb706f7 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -375,8 +375,10 @@ async fn main() -> Result<(), Error> { use rand::prelude::SliceRandom; let backend = backend.clone(); tokio::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(1)); + interval.tick().await; loop { - tokio::time::sleep(Duration::from_secs(1)).await; + interval.tick().await; let qid = qids.choose(&mut rand::thread_rng()).unwrap(); let _ = backend.vote(qid, vote::UpDown::Up).await; } From 2d105090b012a1693017f68903af8de90c834f61 Mon Sep 17 00:00:00 2001 From: Pavel Mikhalkevich Date: Sun, 29 Dec 2024 22:51:03 +0400 Subject: [PATCH 04/10] Use let-else in main --- server/src/main.rs | 242 ++++++++++++++++++++++----------------------- 1 file changed, 117 insertions(+), 125 deletions(-) diff --git a/server/src/main.rs b/server/src/main.rs index cb706f7..0b9b3c9 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -194,135 +194,127 @@ async fn main() -> Result<(), Error> { use aws_sdk_dynamodb::types::{PutRequest, WriteRequest}; let mut backend = Backend::dynamo().await; - let qids: Vec = if let Backend::Dynamo(ref mut client) = backend { - info!("going to seed test event"); - let seed: Vec = serde_json::from_str(SEED).unwrap(); - let seed_e = Ulid::from_string("00000000000000000000000000").unwrap(); - match client - .put_item() - .table_name("events") - .condition_expression("attribute_not_exists(id)") - .item("id", AttributeValue::S(seed_e.to_string())) - .item("secret", AttributeValue::S("secret".into())) - .item("when", to_dynamo_timestamp(SystemTime::now())) - .item( - "expire", - to_dynamo_timestamp(SystemTime::now() + EVENTS_TTL), - ) - .send() - .await - { - Err(ref error @ SdkError::ServiceError(ref e)) => { - if e.err().is_conditional_check_failed_exception() { - warn!("test event is already there, skipping seeding questions"); - } else { - panic!("failed to seed test event {:?}", error) - } + let Backend::Dynamo(ref mut client) = backend else { + unreachable!() + }; + info!("going to seed test event"); + let seed: Vec = serde_json::from_str(SEED).unwrap(); + let seed_e = Ulid::from_string("00000000000000000000000000").unwrap(); + match client + .put_item() + .table_name("events") + .condition_expression("attribute_not_exists(id)") + .item("id", AttributeValue::S(seed_e.to_string())) + .item("secret", AttributeValue::S("secret".into())) + .item("when", to_dynamo_timestamp(SystemTime::now())) + .item( + "expire", + to_dynamo_timestamp(SystemTime::now() + EVENTS_TTL), + ) + .send() + .await + { + Err(ref error @ SdkError::ServiceError(ref e)) => { + if e.err().is_conditional_check_failed_exception() { + warn!("test event is already there, skipping seeding questions"); + } else { + panic!("failed to seed test event {:?}", error) } - Err(e) => panic!("failed to seed test event {:?}", e), - Ok(_) => { - info!("successfully registered test event, going to seed questions now"); - // DynamoDB supports batch write operations with `25` as max batch size - // https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html - for chunk in seed.chunks(25) { - client - .batch_write_item() - .request_items( - "questions", - chunk - .iter() - .map( - |LiveAskQuestion { - likes, - text, - hidden, - answered, - created, - }| { - let mut item = HashMap::from([ - ( - "id".to_string(), - AttributeValue::S( - ulid::Ulid::new().to_string(), - ), - ), - ( - "eid".to_string(), - AttributeValue::S(seed_e.to_string()), - ), - ( - "votes".to_string(), - AttributeValue::N(likes.to_string()), - ), - ( - "text".to_string(), - AttributeValue::S(text.clone()), - ), - ( - "when".to_string(), - AttributeValue::N(created.to_string()), - ), - ( - "expire".to_string(), - to_dynamo_timestamp( - SystemTime::now() + QUESTIONS_TTL, - ), - ), - ( - "hidden".to_string(), - AttributeValue::Bool(*hidden), + } + Err(e) => panic!("failed to seed test event {:?}", e), + Ok(_) => { + info!("successfully registered test event, going to seed questions now"); + // DynamoDB supports batch write operations with `25` as max batch size + // https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html + for chunk in seed.chunks(25) { + client + .batch_write_item() + .request_items( + "questions", + chunk + .iter() + .map( + |LiveAskQuestion { + likes, + text, + hidden, + answered, + created, + }| { + let mut item = HashMap::from([ + ( + "id".to_string(), + AttributeValue::S(ulid::Ulid::new().to_string()), + ), + ( + "eid".to_string(), + AttributeValue::S(seed_e.to_string()), + ), + ( + "votes".to_string(), + AttributeValue::N(likes.to_string()), + ), + ("text".to_string(), AttributeValue::S(text.clone())), + ( + "when".to_string(), + AttributeValue::N(created.to_string()), + ), + ( + "expire".to_string(), + to_dynamo_timestamp( + SystemTime::now() + QUESTIONS_TTL, ), - ]); - if *answered { - item.insert( - "answered".to_string(), - to_dynamo_timestamp(SystemTime::now()), - ); - } - WriteRequest::builder() - .put_request( - PutRequest::builder() - .set_item(Some(item)) - .build() - .expect("request to have been built ok"), - ) - .build() - }, - ) - .collect::>(), - ) - .send() - .await - .expect("batch to have been written ok"); - } - info!("successfully registered questions"); + ), + ("hidden".to_string(), AttributeValue::Bool(*hidden)), + ]); + if *answered { + item.insert( + "answered".to_string(), + to_dynamo_timestamp(SystemTime::now()), + ); + } + WriteRequest::builder() + .put_request( + PutRequest::builder() + .set_item(Some(item)) + .build() + .expect("request to have been built ok"), + ) + .build() + }, + ) + .collect::>(), + ) + .send() + .await + .expect("batch to have been written ok"); } + info!("successfully registered questions"); } - // let's collect ids of the questions related to the test event, - // we can then use them to auto-generate user votes over time - client - .query() - .table_name("questions") - .index_name("top") - .key_condition_expression("eid = :eid") - .expression_attribute_values(":eid", AttributeValue::S(seed_e.to_string())) - .send() - .await - .expect("scanned index ok") - .items() - .iter() - .filter_map(|item| { - let id = item - .get("id") - .expect("id is in projection") - .as_s() - .expect("id is of type string"); - ulid::Ulid::from_string(id).ok() - }) - .collect() - } else { - unreachable!() - }; + } + // let's collect ids of the questions related to the test event, + // we can then use them to auto-generate user votes over time + let qids = client + .query() + .table_name("questions") + .index_name("top") + .key_condition_expression("eid = :eid") + .expression_attribute_values(":eid", AttributeValue::S(seed_e.to_string())) + .send() + .await + .expect("scanned index ok") + .items() + .iter() + .filter_map(|item| { + let id = item + .get("id") + .expect("id is in projection") + .as_s() + .expect("id is of type string"); + ulid::Ulid::from_string(id).ok() + }) + .collect(); + (backend, qids) } else { let mut state = Local::default(); From 9d15443f3fa4d69fb5a91cc6190b714c05f04660 Mon Sep 17 00:00:00 2001 From: Pavel Mikhalkevich Date: Mon, 30 Dec 2024 00:19:38 +0400 Subject: [PATCH 05/10] Put seeding logic to dedicated fn --- server/src/main.rs | 397 ++++++++++++++++++++++++--------------------- 1 file changed, 210 insertions(+), 187 deletions(-) diff --git a/server/src/main.rs b/server/src/main.rs index 0b9b3c9..9023758 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -31,20 +31,6 @@ use tracing::{debug, error, info, trace, warn}; #[cfg(debug_assertions)] const SEED: &str = include_str!("test.json"); -#[cfg(debug_assertions)] -use serde::Deserialize; - -#[cfg(debug_assertions)] -#[derive(Deserialize)] -struct LiveAskQuestion { - likes: usize, - text: String, - hidden: bool, - answered: bool, - #[serde(rename = "createTimeUnix")] - created: usize, -} - #[derive(Clone, Debug)] #[allow(dead_code)] enum Backend { @@ -53,7 +39,7 @@ enum Backend { } impl Backend { - #[cfg(test)] + #[cfg(debug_assertions)] async fn local() -> Self { Backend::Local(Arc::new(Mutex::new(Local::default()))) } @@ -181,201 +167,238 @@ fn mint_service_error(e: E) -> SdkError { ) } -#[tokio::main] -async fn main() -> Result<(), Error> { - tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .without_time(/* cloudwatch does that */).init(); +/// Seed the database. +/// +/// This will register a test event (with id `00000000000000000000000000`) and +/// a number of questions for it in the database, whether it's an in-memory [`Local`] +/// database or a local instance of DynamoDB). Note that in the latter case +/// we are checking if the test event is already there, and - if so - we are _not_ seeding +/// the questions. This is to avoid creating duplicated questions when re-running the app. +/// And this is not an issue of course when running against our in-memory [`Local`] database. +/// +/// The returned vector contains IDs of the questions related to the test event. +#[cfg(debug_assertions)] +async fn seed(backend: &mut Backend) -> Vec { + #[derive(serde::Deserialize)] + struct LiveAskQuestion { + likes: usize, + text: String, + hidden: bool, + answered: bool, + #[serde(rename = "createTimeUnix")] + created: usize, + } - #[cfg(not(debug_assertions))] - let backend = Backend::dynamo().await; - #[cfg(debug_assertions)] - let (backend, qids) = if std::env::var_os("USE_DYNAMODB").is_some() { - use aws_sdk_dynamodb::types::{PutRequest, WriteRequest}; + let seed: Vec = serde_json::from_str(SEED).unwrap(); + let seed_e = Ulid::from_string("00000000000000000000000000").unwrap(); - let mut backend = Backend::dynamo().await; - let Backend::Dynamo(ref mut client) = backend else { - unreachable!() - }; - info!("going to seed test event"); - let seed: Vec = serde_json::from_str(SEED).unwrap(); - let seed_e = Ulid::from_string("00000000000000000000000000").unwrap(); - match client - .put_item() - .table_name("events") - .condition_expression("attribute_not_exists(id)") - .item("id", AttributeValue::S(seed_e.to_string())) - .item("secret", AttributeValue::S("secret".into())) - .item("when", to_dynamo_timestamp(SystemTime::now())) - .item( - "expire", - to_dynamo_timestamp(SystemTime::now() + EVENTS_TTL), - ) - .send() - .await - { - Err(ref error @ SdkError::ServiceError(ref e)) => { - if e.err().is_conditional_check_failed_exception() { - warn!("test event is already there, skipping seeding questions"); - } else { - panic!("failed to seed test event {:?}", error) + match backend { + Backend::Dynamo(ref mut client) => { + use aws_sdk_dynamodb::types::{PutRequest, WriteRequest}; + + info!("going to seed test event"); + match client + .put_item() + .table_name("events") + .condition_expression("attribute_not_exists(id)") + .item("id", AttributeValue::S(seed_e.to_string())) + .item("secret", AttributeValue::S("secret".into())) + .item("when", to_dynamo_timestamp(SystemTime::now())) + .item( + "expire", + to_dynamo_timestamp(SystemTime::now() + EVENTS_TTL), + ) + .send() + .await + { + Err(ref error @ SdkError::ServiceError(ref e)) => { + if e.err().is_conditional_check_failed_exception() { + warn!("test event is already there, skipping seeding questions"); + } else { + panic!("failed to seed test event {:?}", error) + } } - } - Err(e) => panic!("failed to seed test event {:?}", e), - Ok(_) => { - info!("successfully registered test event, going to seed questions now"); - // DynamoDB supports batch write operations with `25` as max batch size - // https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html - for chunk in seed.chunks(25) { - client - .batch_write_item() - .request_items( - "questions", - chunk - .iter() - .map( - |LiveAskQuestion { - likes, - text, - hidden, - answered, - created, - }| { - let mut item = HashMap::from([ - ( - "id".to_string(), - AttributeValue::S(ulid::Ulid::new().to_string()), - ), - ( - "eid".to_string(), - AttributeValue::S(seed_e.to_string()), - ), - ( - "votes".to_string(), - AttributeValue::N(likes.to_string()), - ), - ("text".to_string(), AttributeValue::S(text.clone())), - ( - "when".to_string(), - AttributeValue::N(created.to_string()), - ), - ( - "expire".to_string(), - to_dynamo_timestamp( - SystemTime::now() + QUESTIONS_TTL, + Err(e) => panic!("failed to seed test event {:?}", e), + Ok(_) => { + info!("successfully registered test event, going to seed questions now"); + // DynamoDB supports batch write operations with `25` as max batch size + // https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html + for chunk in seed.chunks(25) { + client + .batch_write_item() + .request_items( + "questions", + chunk + .iter() + .map( + |LiveAskQuestion { + likes, + text, + hidden, + answered, + created, + }| { + let mut item = HashMap::from([ + ( + "id".to_string(), + AttributeValue::S( + ulid::Ulid::new().to_string(), + ), + ), + ( + "eid".to_string(), + AttributeValue::S(seed_e.to_string()), + ), + ( + "votes".to_string(), + AttributeValue::N(likes.to_string()), + ), + ( + "text".to_string(), + AttributeValue::S(text.clone()), + ), + ( + "when".to_string(), + AttributeValue::N(created.to_string()), ), - ), - ("hidden".to_string(), AttributeValue::Bool(*hidden)), - ]); - if *answered { - item.insert( - "answered".to_string(), - to_dynamo_timestamp(SystemTime::now()), - ); - } - WriteRequest::builder() - .put_request( - PutRequest::builder() - .set_item(Some(item)) - .build() - .expect("request to have been built ok"), - ) - .build() - }, - ) - .collect::>(), - ) - .send() - .await - .expect("batch to have been written ok"); + ( + "expire".to_string(), + to_dynamo_timestamp( + SystemTime::now() + QUESTIONS_TTL, + ), + ), + ( + "hidden".to_string(), + AttributeValue::Bool(*hidden), + ), + ]); + if *answered { + item.insert( + "answered".to_string(), + to_dynamo_timestamp(SystemTime::now()), + ); + } + WriteRequest::builder() + .put_request( + PutRequest::builder() + .set_item(Some(item)) + .build() + .expect("request to have been built ok"), + ) + .build() + }, + ) + .collect::>(), + ) + .send() + .await + .expect("batch to have been written ok"); + } + info!("successfully registered questions"); } - info!("successfully registered questions"); } - } - // let's collect ids of the questions related to the test event, - // we can then use them to auto-generate user votes over time - let qids = client - .query() - .table_name("questions") - .index_name("top") - .key_condition_expression("eid = :eid") - .expression_attribute_values(":eid", AttributeValue::S(seed_e.to_string())) - .send() - .await - .expect("scanned index ok") - .items() - .iter() - .filter_map(|item| { - let id = item - .get("id") - .expect("id is in projection") - .as_s() - .expect("id is of type string"); - ulid::Ulid::from_string(id).ok() - }) - .collect(); - - (backend, qids) - } else { - let mut state = Local::default(); - let seed: Vec = serde_json::from_str(SEED).unwrap(); - let seed_e = "00000000000000000000000000"; - let seed_e = Ulid::from_string(seed_e).unwrap(); - state.events.insert(seed_e, String::from("secret")); - state.questions_by_eid.insert(seed_e, Vec::new()); - let mut state = Backend::Local(Arc::new(Mutex::new(state))); - let mut qs = Vec::new(); - for q in seed { - let qid = ulid::Ulid::new(); - state - .ask( - &seed_e, - &qid, - ask::Question { - body: q.text, - asker: None, - }, - ) + // let's collect ids of the questions related to the test event, + // we can then use them to auto-generate user votes over time + let qids = client + .query() + .table_name("questions") + .index_name("top") + .key_condition_expression("eid = :eid") + .expression_attribute_values(":eid", AttributeValue::S(seed_e.to_string())) + .send() .await - .unwrap(); - qs.push((qid, q.created, q.likes, q.hidden, q.answered)); + .expect("scanned index ok") + .items() + .iter() + .filter_map(|item| { + let id = item + .get("id") + .expect("id is in projection") + .as_s() + .expect("id is of type string"); + ulid::Ulid::from_string(id).ok() + }) + .collect(); + + qids } - let mut qids = Vec::new(); - { - let Backend::Local(ref mut state): Backend = state else { - unreachable!(); - }; - let state = Arc::get_mut(state).unwrap(); - let state = Mutex::get_mut(state).unwrap(); - for (qid, created, votes, hidden, answered) in qs { - let q = state.questions.get_mut(&qid).unwrap(); - q.insert("votes", AttributeValue::N(votes.to_string())); + Backend::Local(ref mut local) => { + let mut state = local.lock().unwrap(); + info!("going to seed test event"); + state.events.insert(seed_e, String::from("secret")); + + info!("successfully registered test event, going to seed questions now"); + let mut qids = Vec::new(); + for LiveAskQuestion { + likes, + text, + created, + hidden, + answered, + } in seed + { + let qid = ulid::Ulid::new(); + let mut item = HashMap::from([ + ("id", AttributeValue::S(qid.to_string())), + ("eid", AttributeValue::S(seed_e.to_string())), + ("votes", AttributeValue::N(likes.to_string())), + ("text", AttributeValue::S(text.clone())), + ("when", AttributeValue::N(created.to_string())), + ( + "expire", + to_dynamo_timestamp(SystemTime::now() + QUESTIONS_TTL), + ), + ("hidden", AttributeValue::Bool(hidden)), + ]); if answered { - q.insert("answered", to_dynamo_timestamp(SystemTime::now())); - } - q.insert("hidden", AttributeValue::Bool(hidden)); - q.insert("when", AttributeValue::N(created.to_string())); + item.insert("answered", to_dynamo_timestamp(SystemTime::now())); + }; + state.questions.insert(qid, item); qids.push(qid); } + state.questions_by_eid.insert(seed_e, qids.clone()); + info!("successfully registered questions"); + + qids } - (state, qids) - }; + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .without_time(/* cloudwatch does that */).init(); + + #[cfg(not(debug_assertions))] + let backend = Backend::dynamo().await; - // to aid in development, auto-generate user votes over time #[cfg(debug_assertions)] - { + let backend = { use rand::prelude::SliceRandom; - let backend = backend.clone(); + + let backend = if std::env::var_os("USE_DYNAMODB").is_some() { + Backend::dynamo().await + } else { + Backend::local().await + }; + + // to aid in development, seed the backend with a test event and related + // questions, and auto-generate user votes over time + let mut cheat = backend.clone(); + let qids = seed(&mut cheat).await; tokio::spawn(async move { let mut interval = tokio::time::interval(Duration::from_secs(1)); interval.tick().await; loop { interval.tick().await; let qid = qids.choose(&mut rand::thread_rng()).unwrap(); - let _ = backend.vote(qid, vote::UpDown::Up).await; + let _ = cheat.vote(qid, vote::UpDown::Up).await; } }); - } + + backend + }; let app = Router::new() .route("/api/event", post(new::new)) From 529d83d2b6a615cc5ba18fff995e0240eef0a73f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavie=C5=82=20Michalkievi=C4=8D?= Date: Mon, 30 Dec 2024 17:36:47 +0500 Subject: [PATCH 06/10] Update server/src/main.rs Co-authored-by: Jon Gjengset --- server/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main.rs b/server/src/main.rs index 9023758..01c4612 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -171,7 +171,7 @@ fn mint_service_error(e: E) -> SdkError { /// /// This will register a test event (with id `00000000000000000000000000`) and /// a number of questions for it in the database, whether it's an in-memory [`Local`] -/// database or a local instance of DynamoDB). Note that in the latter case +/// database or a local instance of DynamoDB. Note that in the latter case /// we are checking if the test event is already there, and - if so - we are _not_ seeding /// the questions. This is to avoid creating duplicated questions when re-running the app. /// And this is not an issue of course when running against our in-memory [`Local`] database. From d8b028448b9435130e12c14ab3095ff1cba1c5ff Mon Sep 17 00:00:00 2001 From: Pavel Mikhalkevich Date: Thu, 2 Jan 2025 12:26:34 +0400 Subject: [PATCH 07/10] Restore local seed arm: ask + update --- server/src/main.rs | 70 ++++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/server/src/main.rs b/server/src/main.rs index 01c4612..e2e9a81 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -39,7 +39,6 @@ enum Backend { } impl Backend { - #[cfg(debug_assertions)] async fn local() -> Self { Backend::Local(Arc::new(Mutex::new(Local::default()))) } @@ -193,10 +192,13 @@ async fn seed(backend: &mut Backend) -> Vec { let seed_e = Ulid::from_string("00000000000000000000000000").unwrap(); match backend { - Backend::Dynamo(ref mut client) => { + backend_dynamo @ Backend::Dynamo(_) => { use aws_sdk_dynamodb::types::{PutRequest, WriteRequest}; info!("going to seed test event"); + let Backend::Dynamo(ref mut client) = backend_dynamo else { + unreachable!() + }; match client .put_item() .table_name("events") @@ -322,41 +324,43 @@ async fn seed(backend: &mut Backend) -> Vec { qids } - Backend::Local(ref mut local) => { - let mut state = local.lock().unwrap(); + backend_local @ Backend::Local(_) => { info!("going to seed test event"); - state.events.insert(seed_e, String::from("secret")); + backend_local.new(&seed_e, "secret").await.unwrap(); info!("successfully registered test event, going to seed questions now"); - let mut qids = Vec::new(); - for LiveAskQuestion { - likes, - text, - created, - hidden, - answered, - } in seed - { + let mut qs = Vec::new(); + for q in seed { let qid = ulid::Ulid::new(); - let mut item = HashMap::from([ - ("id", AttributeValue::S(qid.to_string())), - ("eid", AttributeValue::S(seed_e.to_string())), - ("votes", AttributeValue::N(likes.to_string())), - ("text", AttributeValue::S(text.clone())), - ("when", AttributeValue::N(created.to_string())), - ( - "expire", - to_dynamo_timestamp(SystemTime::now() + QUESTIONS_TTL), - ), - ("hidden", AttributeValue::Bool(hidden)), - ]); + backend_local + .ask( + &seed_e, + &qid, + ask::Question { + body: q.text, + asker: None, + }, + ) + .await + .unwrap(); + qs.push((qid, q.created, q.likes, q.hidden, q.answered)); + } + let mut qids = Vec::new(); + let Backend::Local(ref mut state) = backend_local else { + unreachable!(); + }; + let state = Arc::get_mut(state).unwrap(); + let state = Mutex::get_mut(state).unwrap(); + for (qid, created, votes, hidden, answered) in qs { + let q = state.questions.get_mut(&qid).unwrap(); + q.insert("votes", AttributeValue::N(votes.to_string())); if answered { - item.insert("answered", to_dynamo_timestamp(SystemTime::now())); - }; - state.questions.insert(qid, item); + q.insert("answered", to_dynamo_timestamp(SystemTime::now())); + } + q.insert("hidden", AttributeValue::Bool(hidden)); + q.insert("when", AttributeValue::N(created.to_string())); qids.push(qid); } - state.questions_by_eid.insert(seed_e, qids.clone()); info!("successfully registered questions"); qids @@ -377,7 +381,7 @@ async fn main() -> Result<(), Error> { let backend = { use rand::prelude::SliceRandom; - let backend = if std::env::var_os("USE_DYNAMODB").is_some() { + let mut backend = if std::env::var_os("USE_DYNAMODB").is_some() { Backend::dynamo().await } else { Backend::local().await @@ -385,8 +389,8 @@ async fn main() -> Result<(), Error> { // to aid in development, seed the backend with a test event and related // questions, and auto-generate user votes over time - let mut cheat = backend.clone(); - let qids = seed(&mut cheat).await; + let qids = seed(&mut backend).await; + let cheat = backend.clone(); tokio::spawn(async move { let mut interval = tokio::time::interval(Duration::from_secs(1)); interval.tick().await; From 1501e7729145b3eb81f712c5386c4e1fa86cebda Mon Sep 17 00:00:00 2001 From: Pavel Mikhalkevich Date: Thu, 2 Jan 2025 12:31:43 +0400 Subject: [PATCH 08/10] Add not on tracing subscriber --- server/src/main.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/main.rs b/server/src/main.rs index e2e9a81..03b9d9d 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -372,6 +372,9 @@ async fn seed(backend: &mut Backend) -> Vec { async fn main() -> Result<(), Error> { tracing_subscriber::fmt() .with_env_filter(EnvFilter::from_default_env()) + // TODO: we may _not_ want `without_time` when deploying + // TODO: on non-Lambda runtimes; this can be addressed as + // TODO: part of https://github.com/jonhoo/wewerewondering/issues/202 .without_time(/* cloudwatch does that */).init(); #[cfg(not(debug_assertions))] From ea6582ace52762546df43557279e20a1afdea9a6 Mon Sep 17 00:00:00 2001 From: Pavel Mikhalkevich Date: Thu, 2 Jan 2025 20:25:00 +0400 Subject: [PATCH 09/10] Use Backend::ask in dynamodb arm in main --- server/src/main.rs | 171 +++++++++++++++++---------------------------- 1 file changed, 66 insertions(+), 105 deletions(-) diff --git a/server/src/main.rs b/server/src/main.rs index 03b9d9d..b8fd8de 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -190,126 +190,85 @@ async fn seed(backend: &mut Backend) -> Vec { let seed: Vec = serde_json::from_str(SEED).unwrap(); let seed_e = Ulid::from_string("00000000000000000000000000").unwrap(); + let seed_e_secret = "secret"; match backend { backend_dynamo @ Backend::Dynamo(_) => { - use aws_sdk_dynamodb::types::{PutRequest, WriteRequest}; + use aws_sdk_dynamodb::types::BatchStatementRequest; info!("going to seed test event"); - let Backend::Dynamo(ref mut client) = backend_dynamo else { - unreachable!() - }; - match client - .put_item() - .table_name("events") - .condition_expression("attribute_not_exists(id)") - .item("id", AttributeValue::S(seed_e.to_string())) - .item("secret", AttributeValue::S("secret".into())) - .item("when", to_dynamo_timestamp(SystemTime::now())) - .item( - "expire", - to_dynamo_timestamp(SystemTime::now() + EVENTS_TTL), - ) - .send() - .await - { - Err(ref error @ SdkError::ServiceError(ref e)) => { - if e.err().is_conditional_check_failed_exception() { - warn!("test event is already there, skipping seeding questions"); - } else { - panic!("failed to seed test event {:?}", error) - } + match backend_dynamo.event(&seed_e).await.unwrap() { + output if output.item().is_some() => { + warn!("test event is already there, skipping seeding questions"); } - Err(e) => panic!("failed to seed test event {:?}", e), - Ok(_) => { + _ => { + backend_dynamo.new(&seed_e, seed_e_secret).await.unwrap(); info!("successfully registered test event, going to seed questions now"); - // DynamoDB supports batch write operations with `25` as max batch size - // https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html - for chunk in seed.chunks(25) { - client - .batch_write_item() - .request_items( - "questions", - chunk - .iter() - .map( - |LiveAskQuestion { - likes, - text, - hidden, - answered, - created, - }| { - let mut item = HashMap::from([ - ( - "id".to_string(), - AttributeValue::S( - ulid::Ulid::new().to_string(), - ), - ), - ( - "eid".to_string(), - AttributeValue::S(seed_e.to_string()), - ), - ( - "votes".to_string(), - AttributeValue::N(likes.to_string()), - ), - ( - "text".to_string(), - AttributeValue::S(text.clone()), - ), - ( - "when".to_string(), - AttributeValue::N(created.to_string()), - ), - ( - "expire".to_string(), - to_dynamo_timestamp( - SystemTime::now() + QUESTIONS_TTL, - ), - ), - ( - "hidden".to_string(), - AttributeValue::Bool(*hidden), - ), - ]); - if *answered { - item.insert( - "answered".to_string(), - to_dynamo_timestamp(SystemTime::now()), - ); - } - WriteRequest::builder() - .put_request( - PutRequest::builder() - .set_item(Some(item)) - .build() - .expect("request to have been built ok"), - ) - .build() - }, - ) - .collect::>(), + + let mut qs = Vec::new(); + for q in seed { + let qid = ulid::Ulid::new(); + backend_dynamo + .ask( + &seed_e, + &qid, + ask::Question { + body: q.text, + asker: None, + }, ) + .await + .unwrap(); + qs.push((qid, q.created, q.likes, q.hidden, q.answered)); + } + + let Backend::Dynamo(ref mut client) = backend_dynamo else { + unreachable!(); + }; + // DynamoDB supports batch operations using PartiQL syntax with `25` as max batch size + // https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchExecuteStatement.html + for chunk in qs.chunks(25) { + let batch_update = chunk + .iter() + .map(|(qid, created, votes, hidden, answered)| { + let builder = BatchStatementRequest::builder(); + let builder = if *answered { + builder.statement( + // numerous words are reserved in the DynamoDB engine (e.g. Key, Id, When) and + // should be qouted; we are quoting all of our attrs to avoid possible collisions + r#"UPDATE "questions" SET "answered"=? SET "votes"=? SET "when"=? SET "hidden"=? WHERE "id"=?"#, + ) + .parameters(to_dynamo_timestamp(SystemTime::now())) // answered + } else { + builder.statement( + r#"UPDATE "questions" SET "votes"=? SET "when"=? SET "hidden"=? WHERE "id"=?"#, + ) + }; + builder + .parameters(AttributeValue::N(votes.to_string())) // votes + .parameters(AttributeValue::N(created.to_string())) // when + .parameters(AttributeValue::Bool(*hidden)) // hidden + .parameters(AttributeValue::S(qid.to_string())) // id + .build() + .unwrap() + }) + .collect::>(); + client + .batch_execute_statement() + .set_statements(Some(batch_update)) .send() .await .expect("batch to have been written ok"); } - info!("successfully registered questions"); } } + info!("successfully registered questions"); // let's collect ids of the questions related to the test event, // we can then use them to auto-generate user votes over time - let qids = client - .query() - .table_name("questions") - .index_name("top") - .key_condition_expression("eid = :eid") - .expression_attribute_values(":eid", AttributeValue::S(seed_e.to_string())) - .send() + let qids = backend_dynamo + .list(&seed_e, true) .await - .expect("scanned index ok") + .expect("scenned index ok") .items() .iter() .filter_map(|item| { @@ -326,7 +285,7 @@ async fn seed(backend: &mut Backend) -> Vec { } backend_local @ Backend::Local(_) => { info!("going to seed test event"); - backend_local.new(&seed_e, "secret").await.unwrap(); + backend_local.new(&seed_e, seed_e_secret).await.unwrap(); info!("successfully registered test event, going to seed questions now"); let mut qs = Vec::new(); @@ -399,7 +358,9 @@ async fn main() -> Result<(), Error> { interval.tick().await; loop { interval.tick().await; - let qid = qids.choose(&mut rand::thread_rng()).unwrap(); + let qid = qids + .choose(&mut rand::thread_rng()) + .expect("there _are_ some questions for our test event"); let _ = cheat.vote(qid, vote::UpDown::Up).await; } }); From 765139414ed784414f3f8ae77d93b0433a5da4fa Mon Sep 17 00:00:00 2001 From: Pavel Mikhalkevich Date: Sun, 5 Jan 2025 15:25:35 +0400 Subject: [PATCH 10/10] Re-use Backend::event, Backend::ask, Backend::list in seed --- server/src/main.rs | 150 ++++++++++++++++++--------------------------- 1 file changed, 58 insertions(+), 92 deletions(-) diff --git a/server/src/main.rs b/server/src/main.rs index b8fd8de..6aee941 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -192,39 +192,35 @@ async fn seed(backend: &mut Backend) -> Vec { let seed_e = Ulid::from_string("00000000000000000000000000").unwrap(); let seed_e_secret = "secret"; - match backend { - backend_dynamo @ Backend::Dynamo(_) => { - use aws_sdk_dynamodb::types::BatchStatementRequest; - - info!("going to seed test event"); - match backend_dynamo.event(&seed_e).await.unwrap() { - output if output.item().is_some() => { - warn!("test event is already there, skipping seeding questions"); - } - _ => { - backend_dynamo.new(&seed_e, seed_e_secret).await.unwrap(); - info!("successfully registered test event, going to seed questions now"); - - let mut qs = Vec::new(); - for q in seed { - let qid = ulid::Ulid::new(); - backend_dynamo - .ask( - &seed_e, - &qid, - ask::Question { - body: q.text, - asker: None, - }, - ) - .await - .unwrap(); - qs.push((qid, q.created, q.likes, q.hidden, q.answered)); - } - - let Backend::Dynamo(ref mut client) = backend_dynamo else { - unreachable!(); - }; + info!("going to seed test event"); + match backend.event(&seed_e).await.unwrap() { + output if output.item().is_some() => { + warn!("test event is already there, skipping seeding questions"); + } + _ => { + backend.new(&seed_e, seed_e_secret).await.unwrap(); + info!("successfully registered test event, going to seed questions now"); + // first create questions ... + let mut qs = Vec::new(); + for q in seed { + let qid = ulid::Ulid::new(); + backend + .ask( + &seed_e, + &qid, + ask::Question { + body: q.text, + asker: None, + }, + ) + .await + .unwrap(); + qs.push((qid, q.created, q.likes, q.hidden, q.answered)); + } + // ... then set the vote count + answered/hidden flags + match backend { + Backend::Dynamo(ref mut client) => { + use aws_sdk_dynamodb::types::BatchStatementRequest; // DynamoDB supports batch operations using PartiQL syntax with `25` as max batch size // https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchExecuteStatement.html for chunk in qs.chunks(25) { @@ -261,70 +257,40 @@ async fn seed(backend: &mut Backend) -> Vec { .expect("batch to have been written ok"); } } - } - info!("successfully registered questions"); - // let's collect ids of the questions related to the test event, - // we can then use them to auto-generate user votes over time - let qids = backend_dynamo - .list(&seed_e, true) - .await - .expect("scenned index ok") - .items() - .iter() - .filter_map(|item| { - let id = item - .get("id") - .expect("id is in projection") - .as_s() - .expect("id is of type string"); - ulid::Ulid::from_string(id).ok() - }) - .collect(); - - qids - } - backend_local @ Backend::Local(_) => { - info!("going to seed test event"); - backend_local.new(&seed_e, seed_e_secret).await.unwrap(); - - info!("successfully registered test event, going to seed questions now"); - let mut qs = Vec::new(); - for q in seed { - let qid = ulid::Ulid::new(); - backend_local - .ask( - &seed_e, - &qid, - ask::Question { - body: q.text, - asker: None, - }, - ) - .await - .unwrap(); - qs.push((qid, q.created, q.likes, q.hidden, q.answered)); - } - let mut qids = Vec::new(); - let Backend::Local(ref mut state) = backend_local else { - unreachable!(); - }; - let state = Arc::get_mut(state).unwrap(); - let state = Mutex::get_mut(state).unwrap(); - for (qid, created, votes, hidden, answered) in qs { - let q = state.questions.get_mut(&qid).unwrap(); - q.insert("votes", AttributeValue::N(votes.to_string())); - if answered { - q.insert("answered", to_dynamo_timestamp(SystemTime::now())); + Backend::Local(ref mut state) => { + let state = Arc::get_mut(state).unwrap(); + let state = Mutex::get_mut(state).unwrap(); + for (qid, created, votes, hidden, answered) in qs { + let q = state.questions.get_mut(&qid).unwrap(); + q.insert("votes", AttributeValue::N(votes.to_string())); + if answered { + q.insert("answered", to_dynamo_timestamp(SystemTime::now())); + } + q.insert("hidden", AttributeValue::Bool(hidden)); + q.insert("when", AttributeValue::N(created.to_string())); + } } - q.insert("hidden", AttributeValue::Bool(hidden)); - q.insert("when", AttributeValue::N(created.to_string())); - qids.push(qid); } info!("successfully registered questions"); - - qids } } + // let's collect ids of the questions related to the test event, + // we can then use them to auto-generate user votes over time + backend + .list(&seed_e, true) + .await + .expect("scenned index ok") + .items() + .iter() + .filter_map(|item| { + let id = item + .get("id") + .expect("id is in projection") + .as_s() + .expect("id is of type string"); + ulid::Ulid::from_string(id).ok() + }) + .collect() } #[tokio::main]