• Stars
    star
    154
  • Rank 242,095 (Top 5 %)
  • Language
    Rust
  • Created about 2 years ago
  • Updated 5 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

wewerewondering.com

If you want to contribute, see CONTRIBUTING.md.

This is the code that fuels https://wewerewondering.com/, a website that is aimed at facilitating live Q&A sessions. To use it, just go to that URL and click "Create Event". Then, click "Share Event" and share the URL that just got copied to your clipboard to anyone you want to be able to ask questions. You'll see them come in live in the host view. You can share the host view by copy-pasting the URL in your browser address bar.

What it provides:

  • Zero-hassle experience for you and your audience.
  • Audience question voting.
  • List of already-answered questions.
  • Ability to hide questions.

What it doesn't provide:

  • Protection against malicious double-voting.
  • Live question feed for the audience (it is ~10s out-of-date).
  • Long-lived Q&A sessions -- questions go away after 30 days.

If you're curious about the technology behind the site, it's all run on AWS. Here's the rough architecture behind the scenes:

Account.

I've set up an AWS Organization for my personal AWS account. In that organization, I've created a dedicated AWS account that holds all the infrastructure for wewerewondering.com. That way, at least in theory, it's cleanly separated from everything else, and could even be given away to elsewhere should that become relevant.

Domain.

The domain is registered with Hover, my registrar of choice for no particularly good reason. The nameservers are set to point at Route 53, which hold a single public hosted zone. It has MX records and SPF pointing to ImprovMX (which is great btw), A and AAAA records that use "aliasing" to point at the CloudFront distribution for the site (see below). Finally, it has a CNAME record used for domain verification for AWS Certificate Manager.

The process for setting up the cert was a little weird. First, the certificate must be in us-east-1 to work with CloudFront for reasons. Second, the CNAME record for domain verification wasn't auto-added. Instead, I had to go into the Certificate Manager control panel for the domain, and click a button named "Create records in Route 53". Not too bad, but wasn't immediately obvious. Once I did that though, verification went through just fine.

CDN.

The main entry point for the site is AWS CloudFront. I have a single "distribution", and the Route 53 A/AAAA entries are pointed at that one distribution's CloudFront domain name. The distribution also has wewerewondering.com configured as an alternate domain name, and is configured to use the Certificate Manager domain from earlier and the most up-to-date TLS configuration. The distribution has "standard logging" (to S3) enabled for now, and has a "default root object" of index.html (more on that later).

CloudFront ties "behaviors" to "origins". Behaviors are ~= routes and origins are ~= backends. There are two behaviors: the default route and the /api route. There are two origins: S3 and API Gateway. You get three internet points if you can guess which behavior connects to which origin.

Static components. The default route (behavior) is set up to send requests to the S3 origin, which in turn just points at an S3 bucket that holds the output of building the stuff in client/. The behavior redirects HTTP to HTTPS, only allows GET and HEAD requests, and uses the CachingOptimized caching policy which basically means it has a long default timeout (1 day) and compression enabled. In S3, I've specifically overridden the "metadata" for index.html to set cache-control to max-age=300 since it gets updated in-place (the assets/ files have hashes in their names and can be cached forever). In addition, it has the SecurityHeaderPolicy response header policy to set X-Frame-Options and friends.

There's one non-obvious trick in use here to make the single-page app approach work with "pretty" URLs that don't involve #. Ultimately we want URLs that the single-page app handles to all be routed to index.html rather than try to request, say, /event/foo from S3. There are multiple ways to achieve this. The one I went with was to define a CloudFront function to rewrite request URLs that I then associate with the "Viewer request" hook. It looks like this:

function handler(event) {
    var req = event.request;
    if (req.uri.startsWith('/event/')) {
        req.uri = '/index.html';
    }
    return req;
}

I did it this way rather than using a custom error response because that also rewrites 404 errors from the API origin, which I don't want. Not to mention I wanted unhandled URLs to still give 404s. And I didn't want to use S3 Static Web Hosting (which allows you to set up conditional redirects) because then CloudFront can't access S3 "natively" and will instead redirect to the bucket and require it to be publicly accessible.

Another modification I made to the defaults was to slightly modify the S3 bucket policy compared to the one CloudFlare recommends in order to allow LIST requests so that you get 404s instead of 403s. The part of the policy I used was:

"Action": [
	"s3:GetObject",
	"s3:ListBucket"
],
"Resource": [
	"arn:aws:s3:::wewerewondering-static",
	"arn:aws:s3:::wewerewondering-static/*"
],

The /api endpoints. The behavior for the /api URLs is defined for the path /api/*, is configured to allow all HTTP methods but only HTTPS, and also uses the SecurityHeaderPolicy response header policy. For caching, I created my own policy that is basically CachingOptimized but has a default TTL of 1s, because if I fail to set a cache header I'd rather things mostly keep working rather than everything looking like nothing updates.

The origin for /api is a custom origin that holds the "Invoke URL" of the API Gateway API (and requires HTTPS). Which brings us to:

The API.

As previously mentioned, the API is a single AWS Lambda backed by the Lambda Rust Runtime (see server/ for more details). But, it's hosted through AWS' API Gateway service, mostly because it gives me throttling, metrics, and logging out of the box. For more elaborate services I'm sure the multi-stage and authorization bits come in handy too, but I haven't made use of any of that. The site also uses the HTTP API configuration because it's a) cheaper, b) simpler to set up, and c) worked out of the box with the Lambda Rust Runtime, which the REST API stuff didn't (for me at least). There are other differences, but none that seemed compelling for this site's use-case.

All of the routes supported by the API implementation (in server/) are registered in API Gateway and are all pointed at the same Lambda. This has the nice benefit that other routes won't even invoke the Lambda, which (I assume) is cheaper. I've set up the $default stage to have fairly conservative throttling (for now) just to avoid any surprise jumps in cost. It also has "Access logging" set up.

One thing noting about using API Gateway with the HTTP API is that the automatic dashboard it adds to CloudWatch doesn't work because it expects the metrics from the REST API, which are named differently from the ones used by the HTTP API. The (annoying) fix was to copy the automatic dashboard over into a new (custom) dashboard and edit the source for every widget to replace

"ApiName", "wewerewondering"

with

"ApiId", "<the ID of the API Gateway API>"

and replace the metric names by the correct ones.

The Lambda itself is mostly just what cargo lambda deploy sets up, though I've specifically add RUST_LOG as an environment variable to get more verbose logs (for now). It's also set up to log to CloudWatch, which I think happened more or less automatically. Crucially though, the IAM role used to execute the Lambda is also granted read/write (but not delete/admin) access to the database, like so:

{
    "Sid": "VisualEditor0",
    "Effect": "Allow",
    "Action": [
        "dynamodb:BatchGetItem",
        "dynamodb:PutItem",
        "dynamodb:GetItem",
        "dynamodb:Scan",
        "dynamodb:Query",
        "dynamodb:UpdateItem"
    ],
    "Resource": [
        "arn:aws:dynamodb:*:<account id>:table/events",
        "arn:aws:dynamodb:*:<account id>:table/questions",
        "arn:aws:dynamodb:*:<account id>:table/questions/index/top"
    ]
}

The database.

The site uses DynamoDB as its storage backend, because frankly, that's all it needs. And it's fairly fast and cheap if you can get away with its limited feature set. There are two tables, events and questions, both of which are set up to use on-demand provisioning. events just holds the ULID of an event, which is also the partition key (DynamoDB doesn't have auto-increment integer primary keys because they don't scale), the event's secret key, and its creation and auto-deletion timestamp. questions has:

  • the question ULID (as the partition key)
  • the event ULID
  • the question text
  • the question author (if given)
  • the number of votes
  • whether the question is answered
  • whether the question is hidden
  • creation and auto-deletion timestamps

The ULIDs, the timestamps, and the question text + author never change This is why the API to look up event info and question texts/authors is separated from looking up vote counts -- the former can have much longer cache time.

To allow querying questions for a given event and receive them in sorted order, questions also has a global secondary index called top whose partition key is the event ULID and sort key votes. That index also projects out the "answered" and "hidden" fields so that a single query to that index gives all the mutable state for an event's question list (and can thus be queried with a single DynamoDB call by the Lambda).

Metrics and Logging.


Scaling further.

Currently, everything is in us-east-1. That's sad. CDN helps (potentially a lot), but mainly for guests, and not when voting. It's mostly because DynamoDB [global tables] do reconciliation-by-overwrite, which doesn't work very well for counters. Could make it store every vote separately and do a count, but that's sad. Alternatively, if we assume that most guests are near the host, we could:

  1. Make events a global table (but not questions).
  2. Have a separate questions in each region.
  3. Add a region column to events which is set to the region that hosts the Lambda that serves the "create event" request.
  4. Update the server code to always access questions in the region of the associated event.

We'd probably need to tweak CloudFlare (and maybe Route 53?) a little bit to make it to do geo-aware routing, but I think that's a thing it supports.


Notes for me

To deploy server:

cd server
cargo lambda build --release --arm64
cargo lambda deploy --env-var RUST_LOG=info,tower_http=debug,wewerewondering_api=trace --profile qa

To deploy client:

cd client
npm run build
aws --profile qa s3 sync --delete dist/ s3://wewerewondering-static

More Repositories

1

left-right

A lock-free, read-optimized, concurrency primitive.
Rust
1,940
star
2

inferno

A Rust port of FlameGraph
Rust
1,642
star
3

fantoccini

A high-level API for programmatically interacting with web pages through WebDriver.
Rust
1,085
star
4

configs

My configuration files
Vim Script
968
star
5

bus

Efficient, lock-free, bounded Rust broadcast channel
Rust
668
star
6

flurry

A port of Java's ConcurrentHashMap to Rust
Rust
516
star
7

rust-tcp

A learning experience in implementing TCP in Rust
Rust
488
star
8

rust-imap

IMAP client library for Rust
Rust
477
star
9

evmap

A lock-free, eventually consistent, concurrent multi-value map.
Rust
459
star
10

drwmutex

Distributed RWMutex in Go
Go
323
star
11

rust-ci-conf

Collection of CI configuration files for Rust projects
278
star
12

rustengan

https://fly.io/dist-sys/ in Rust
Rust
260
star
13

roget

Wordle Solver inspired by 3blue1brown
Rust
224
star
14

haphazard

Hazard pointers in Rust.
Rust
193
star
15

griddle

A HashMap variant that spreads resize load across inserts
Rust
188
star
16

stream-cancel

A Rust library for interrupting asynchronous streams.
Rust
155
star
17

tsunami

Rust crate for running one-off cloud jobs
Rust
154
star
18

faktory-rs

Rust bindings for Faktory clients and workers
Rust
149
star
19

rust-for-rustaceans.com

Source for https://rust-for-rustaceans.com/
CSS
146
star
20

buzz

A simple system tray application for notifying about unseen e-mail
Rust
129
star
21

volley

Volley is a benchmarking tool for measuring the performance of server networking stacks.
C
121
star
22

msql-srv

Bindings for writing a server that can act as MySQL/MariaDB
Rust
116
star
23

proximity-sort

Simple command-line utility for sorting inputs by proximity to a path argument
Rust
116
star
24

bustle

A benchmarking harness for concurrent key-value collections
Rust
114
star
25

tracing-timing

Inter-event timing metrics on top of tracing.
Rust
112
star
26

stuck

Rust
107
star
27

atone

A `VecDeque` (and `Vec`) variant that spreads resize load across pushes.
Rust
106
star
28

rust-ibverbs

Bindings for RDMA ibverbs through rdma-core
Rust
106
star
29

thesis

My PhD thesis (eventually)
TeX
93
star
30

openssh-rs

Scriptable SSH through OpenSSH in Rust
Rust
92
star
31

superimposer

Python
85
star
32

udp-over-tcp

A command-line tool for tunneling UDP datagrams over TCP.
Rust
84
star
33

tetris-tutorial

From rags to riches; building Tetris with no programming experience.
JavaScript
78
star
34

codecrafters-bittorrent-rust

Rust
78
star
35

orst

Sorting algorithms in Rust
Rust
76
star
36

async-ssh

High-level Rust library for asynchronous SSH connections
Rust
74
star
37

codecrafters-git-rust

Rust
73
star
38

pthread_pool

A simple implementation of thread pooling for C/C++ using POSIX threads
C
71
star
39

arrav

A sentinel-based, heapless, `Vec`-like type.
Rust
68
star
40

streamsh

Download online video streams using shell
Shell
67
star
41

trawler

Workload generator that emulates the traffic pattern of lobste.rs
Rust
66
star
42

async-bincode

Asynchronous access to a bincode-encoded item stream.
Rust
66
star
43

sento

A lock-free, append-only atomic pool.
Rust
65
star
44

hashbag

An unordered multiset/bag implementation backed by HashMap
Rust
64
star
45

cucache

Fast PUT/GET/DELETE in-memory key-value store for lookaside caching
Go
63
star
46

wp2ghost

Convert WordPress XML exports to Ghost JSON import files
JavaScript
58
star
47

vast-vmap

JavaScript library for IAB VAST + VMAP
JavaScript
56
star
48

lox

https://app.codecrafters.io/courses/interpreter
Rust
55
star
49

vote.rs

Simple website for doing multi-round ranked choice voting
Rust
52
star
50

tokio-io-pool

An I/O-oriented tokio runtime thread pool
Rust
48
star
51

hurdles

Rust library providing a counter-based thread barrier
Rust
46
star
52

ordsearch

A Rust data structure for efficient lower-bound lookups
Rust
42
star
53

arccstr

Thread-safe, reference-counted null-terminated immutable Rust strings.
Rust
41
star
54

cliff

Find the load at which a benchmark falls over.
Rust
36
star
55

curb

Run a process on a particular subset of the available hardware.
Rust
35
star
56

bystander

Rust
30
star
57

shortcut

Rust crate providing an indexed, queryable column-based storage system
Rust
30
star
58

hasmail

Simple tray icon for detecting new email on IMAP servers
Go
29
star
59

ornithology

A tool that parses your Twitter archive and highlights interesting data from it.
Rust
28
star
60

rust-zipf

Rust implementation of a fast, bounded, Zipf-distributed random number generator
Rust
26
star
61

minion

Rust crate for managing cancellable services
Rust
24
star
62

cargo-index-transit

A package for common types for Cargo index interactions, and conversion between them.
Rust
24
star
63

streamunordered

A version for futures::stream::FuturesUnordered that multiplexes Streams
Rust
24
star
64

icebreaker

Web app that allows students to ask real-time, anonymous questions during class
Go
23
star
65

mktrayicon

Create system tray icons by writing to a pipe
C
22
star
66

thesquareplanet.com

My homepage
HTML
20
star
67

rust-basic-hashmap

Let's build a HashMap
Rust
20
star
68

obs-do

CLI for common OBS operations while streaming using WebSocket
Rust
20
star
69

gladia-captions

Rust
20
star
70

PHP-Browser

A PHP class to allow scripts to access online resources hidden behind login-forms and other user-interaction measures
PHP
19
star
71

keybase-chat-notifier

Simple desktop notifier for keybase chat
Rust
19
star
72

tokio-byteorder

Asynchronous adapter for byteorder
Rust
19
star
73

python-agenda

Python module for pretty task logging
Python
18
star
74

async-lease

An asynchronous leased value
Rust
17
star
75

yambar-hyprland-wses

Rust
17
star
76

tally

time's prettier cousin
Rust
15
star
77

strawpoll

Rust
15
star
78

async-option

Asynchronous Arc<Mutex<Option<T>>>
Rust
14
star
79

indexmap-amortized

bluss/IndexMap with amortized resizes
Rust
14
star
80

tokio-os-timer

Timer facilities for Tokio based on OS-level primitives.
Rust
14
star
81

cloud-arch

Script for generating Arch Linux cloud images
Shell
13
star
82

cryptsetup-gui

Simple GUI for unlocking cryptsetup volumes
C
13
star
83

guardian

Owned mutex guards for refcounted mutexes.
Rust
12
star
84

rust-at-sunrise

Tool for posting daily updates on the latest available Rust Nightly
Rust
11
star
85

experiment

A tool for running concurrent multi-configuration experiments.
Ruby
11
star
86

mio-pool

A pool of workers operating on a single set of mio connections
Rust
10
star
87

rust-agenda

Simple, pretty CLI progress tracker
Rust
10
star
88

songtext

Simple bash script for retrieving song lyrics.
Shell
10
star
89

skyline

Various implementations of tower::Service
Rust
9
star
90

hanabot

Hanabi Slack bot
Rust
8
star
91

cleaver

Data-flow distribution analyzer
Rust
8
star
92

repackage

A terrible and nerve-inducing tool to rename a crate in a .crate file
Rust
7
star
93

simio

I/O Automata Simulator
Python
7
star
94

cargo-http-bench

Benchmarking suite for Cargo's experimental HTTP registry implementation
Shell
7
star
95

go-iprof

Simple go library for concurrent instrumented profiling.
Go
7
star
96

incomplete

Provides incomplete!(), a compile-time checked version of Rust's unimplemented!() macro
Rust
7
star
97

rucache

Fast PUT/GET/DELETE in-memory key-value store for lookaside caching -- Rust prototype
Rust
6
star
98

throttled-reader

An io::Read proxy that limits calls to read()
Rust
6
star
99

ff-jump-to-tab

A (subjectively) better Ctrl+# experience for Firefox
JavaScript
6
star
100

snavi

Sane navigation enhancement with Javascript
JavaScript
6
star