Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,26 @@ 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,
see their respective `README.md` files.

To run tests against a DynamoDB instance running [locally](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html), make sure
you got [`docker`](https://docs.docker.com/engine/install/) and
[`AWS CLI`](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions) installed, then hit:

```console
$ cd server
$ make dynamodb
$ make test/e2e
```

Now, to develop against a local instance of DynamoDB, hit:

```console
$ make run
```

You can also spin a [Web UI](https://github.com/aaronshaf/dynamodb-admin?tab=readme-ov-file)
for your local Dynamodb with:

```console
$ make dynamodb/admin
```
3 changes: 2 additions & 1 deletion server/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/target
/target
/dynamodb-data
116 changes: 116 additions & 0 deletions server/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
DYNAMODB_CONTAINER_NAME=dynamodb-local
DYNAMODB_ADMIN_CONTAINER_NAME=dynamodb-admin
ENDPOINT_URL=http://localhost:8000

.PHONY: fmt
fmt:
cargo fmt

.PHONY: check
check:
cargo fmt --check
cargo clippy --all-features
cargo d --no-deps --all-features

.PHONY: run
run:
USE_DYNAMODB=1 AWS_ENDPOINT_URL=${ENDPOINT_URL} cargo run

.PHONY: test/e2e
test/e2e:
AWS_ENDPOINT_URL=${ENDPOINT_URL} cargo t -- --ignored

.PHONY: dynamodb
.ONESHELL: dynamodb
dynamodb:
Comment thread
jonhoo marked this conversation as resolved.
Outdated
@docker ps | grep ${DYNAMODB_CONTAINER_NAME} > /dev/null && \
echo "Already running. Use 'make dynamodb/kill' first." && \
exit 0

echo "🖴 Preparing volumes for DynamoDB..."
@rm -rf dynamodb-data
@mkdir dynamodb-data

echo "🚀 Spinning up a container with DynamoDB..."
@(docker run --rm -d \
-v ./dynamodb-data:/home/dynamodblocal/data \
-p 127.0.0.1:8000:8000 \
-w /home/dynamodblocal \
--name ${DYNAMODB_CONTAINER_NAME} \
amazon/dynamodb-local:latest \
-jar DynamoDBLocal.jar -sharedDb -dbPath ./data) > /dev/null

while ! (aws dynamodb list-tables --endpoint-url ${ENDPOINT_URL} > /dev/null 2>&1); do
echo "⏳ Waiting for the database to start accepting connections..."
done

echo "🗒️ Creating 'events' table..."
@aws dynamodb create-table \
--table-name events \
--attribute-definitions AttributeName=id,AttributeType=S \
--key-schema AttributeName=id,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--endpoint-url ${ENDPOINT_URL} > /dev/null
@aws dynamodb update-time-to-live \
--table-name events \
--time-to-live-specification Enabled=true,AttributeName=expire \
--endpoint-url ${ENDPOINT_URL} > /dev/null

echo "🗒️ Creating 'questions' table and 🚄 GSI..."
@aws dynamodb create-table \
--table-name questions \
--attribute-definitions AttributeName=id,AttributeType=S \
AttributeName=eid,AttributeType=S \
AttributeName=votes,AttributeType=N \
--key-schema AttributeName=id,KeyType=HASH \
--global-secondary-indexes 'IndexName=top,KeySchema=[{AttributeName=eid,KeyType=HASH},{AttributeName=votes,KeyType=RANGE}],Projection={ProjectionType=INCLUDE,NonKeyAttributes=[answered,hidden]}' \
--billing-mode PAY_PER_REQUEST \
--endpoint-url ${ENDPOINT_URL} > /dev/null
@aws dynamodb update-time-to-live \
--table-name questions \
--time-to-live-specification Enabled=true,AttributeName=expire \
--endpoint-url ${ENDPOINT_URL} > /dev/null

echo "✅ Done!"
echo "💡To get details on a table, run 'make dynamodb/describe/<table_name>'"

.PHONY: dynamodb/admin
dynamodb/admin:
@docker ps | grep ${DYNAMODB_ADMIN_CONTAINER_NAME} > /dev/null && \
echo "Already running. Use 'make dynamodb/admin/kill' first." && \
exit 0

echo "🚀 Spinning up a container with DynamoDB Admin..."
@(docker run -d --rm --net host --name ${DYNAMODB_ADMIN_CONTAINER_NAME} aaronshaf/dynamodb-admin) > /dev/null
echo "🔎 DynamoDB Admin is available at http://localhost:8001"

.PHONY: dynamodb/admin/kill
dynamodb/admin/kill:
@docker ps | grep ${DYNAMODB_ADMIN_CONTAINER_NAME} > /dev/null && \
docker stop ${DYNAMODB_ADMIN_CONTAINER_NAME} > /dev/null && \
echo "🧹 Done!" && exit 0
echo "Container '${DYNAMODB_ADMIN_CONTAINER_NAME}' not found.

.PHONY: dynamodb/list
dynamodb/list:
@aws dynamodb list-tables --endpoint-url ${ENDPOINT_URL} | jq .TableNames

.PHONY: dynamodb/describe/events
dynamodb/describe/events:
@aws dynamodb describe-table --table-name events --endpoint-url ${ENDPOINT_URL} | jq .Table

.PHONY: dynamodb/describe/questions
dynamodb/describe/questions:
@aws dynamodb describe-table --table-name questions --endpoint-url ${ENDPOINT_URL} | jq .Table

.PHONY: dynamodb/describe/gsi
dynamodb/describe/questions/gsi:
@aws dynamodb describe-table --table-name questions --endpoint-url ${ENDPOINT_URL} | jq .Table.GlobalSecondaryIndexes

.PHONY: dynamodb/kill
.ONESHELL: dynamodb/kill
dynamodb/kill:
@docker ps | grep ${DYNAMODB_CONTAINER_NAME} > /dev/null && \
docker stop ${DYNAMODB_CONTAINER_NAME} > /dev/null && \
echo "🧹 Done!" && exit 0
echo "Container '${DYNAMODB_CONTAINER_NAME}' not found."
127 changes: 67 additions & 60 deletions server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,71 +136,78 @@ async fn main() -> Result<(), Error> {

#[cfg(debug_assertions)]
let backend = {
use rand::prelude::SliceRandom;
use serde::Deserialize;
use std::time::Duration;
// To be able to develop and test against a local DynamoDB instance
// USE_DYNAMODB=1 AWS_ENDPOINT_URL=http://localhost:8000 cargo run
if std::env::var_os("USE_DYNAMODB").is_some() {
let config = aws_config::load_from_env().await;
Backend::Dynamo(aws_sdk_dynamodb::Client::new(&config))
} else {
Comment thread
jonhoo marked this conversation as resolved.
Outdated
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,
}
#[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<LiveAskQuestion> = 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,
},
)
.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 = 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()));
if answered {
q.insert("answered", to_dynamo_timestamp(SystemTime::now()));
let mut state = Local::default();
let seed: Vec<LiveAskQuestion> = 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,
},
)
.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 = 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()));
if answered {
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);
}
q.insert("hidden", AttributeValue::Bool(hidden));
q.insert("when", AttributeValue::N(created.to_string()));
qids.push(qid);
}
let cheat = state.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;
}
});
state
}
let cheat = state.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;
}
});
state
};
#[cfg(not(debug_assertions))]
let backend = {
Expand Down