Challenges / Signups grind to a halt
Signups grind to a halt
The signup API hashes every password and stores it while the caller waits. Keep signups answering when the traffic climbs.
The setup
Signup API hashes every signup and writes it to the table before it answers. Signup Balancer spreads the traffic over however many instances it is set to run.
The usual answer is a queue with a function behind it: the API hands the signup over and answers, and the function does the hashing and the write. Anything that answers as quickly and stores the same thing will do.
Move the hashing wherever you like, but leave what it computes alone. The goals compare the hashes you stored against the ones this code produces.
The run
Each goal is judged over its own stretch of the run rather than the whole of it.
- 5 s · Signups climb to four a second
- Every signup is answered, at least 15 every 5 seconds0 s–end
- Signups are answered in under 100 ms on average10 s–end
- Signups are stored in AccountsAt the end
- Stored passwords are hashed the way the starting code doesAt the end
The code
Ordinary Node against the real AWS SDK, running as real processes in the tab. You can edit any of it once the challenge is open.
import express from 'express';
import { createHash, scryptSync } from 'node:crypto';
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
// Glass Garden gives every instance its own PORT, because they all share one machine here.
// In a real deployment they would each be on their own machine, and could all listen on the same port.
const port = Number(process.env.PORT);
if (!port) {
throw new Error('PORT is not set');
}
// Set by the table this node is connected to. The endpoint and the credentials the SDK
// needs to reach the local AWS region arrive in the environment too, which is why the
// client below takes no arguments at all. You can see every one of them in the Config tab
const tableName = process.env.DYNAMODB_TABLE;
if (!tableName) {
throw new Error('DYNAMODB_TABLE is not set. Connect this instance group to one table node.');
}
const dynamodb = new DynamoDBClient({});
// A password hash is meant to be expensive to compute: that is the whole defence if this
// table is ever stolen, because it is what makes guessing a password slow. p is the cost
// knob, and scryptSync is synchronous, so this instance answers nobody while one of these
// runs. The goals compare the hash this cost produces, so try doubling it on a canvas of
// your own and watch what it does to the response time on the generator's chart
const COST = { N: 16384, r: 8, p: 8 };
// Embedded Metric Format: the shape CloudWatch extracts metrics from a log line, and how
// anything running here puts a number on its own Metrics tab
function putMetric(name, value, unit) {
console.log(
JSON.stringify({
_aws: {
Timestamp: Date.now(),
CloudWatchMetrics: [
{ Namespace: 'glass-garden', Dimensions: [[]], Metrics: [{ Name: name, Unit: unit }] }
]
},
[name]: value
})
);
}
// Once at startup, so the name is in the Metrics tab before anybody signs up
putMetric('signups', 0, 'Count');
const app = express();
app.use(express.json());
// The load balancer asks for this before it sends any signups here, the way an ALB checks a
// target group. An instance that stops answering it is taken out of the rotation
app.get('/', (req, res) => {
res.type('text/plain').send('ok\n');
});
app.post('/signup', async (req, res) => {
const { email = 'nobody@example.com', password = 'hunter2' } = req.body;
// The caller waits for every line of this. Ask what they are actually waiting for: the
// signup is decided the moment the request arrives, and nothing needs the stored hash
// until this person next logs in. Work nobody is waiting for does not belong here
// A salt is what stops two people who chose the same password having the same stored
// hash, so a stolen table cannot be cracked all at once. A real store keeps a random
// one per account and saves it alongside; deriving it from the email instead makes the
// hash recomputable, which is the only reason this challenge can check what you stored.
// Move this code, but leave what it computes alone: the goals compare the exact bytes
const salt = createHash('sha256').update(email).digest();
const hash = scryptSync(password, salt, 64, COST);
await dynamodb.send(
new PutItemCommand({
TableName: tableName,
Item: {
email: { S: email },
hash: { S: hash.toString('base64') }
}
})
);
putMetric('signups', 1, 'Count');
res.status(201).json({ status: 'signed up' });
});
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});More challenges
Your first challenge
Nothing is sending the app traffic, and its code fails on every request. Wire it up, fix one line, and watch a run score itself.
Instance Group
The concert that oversold
Box Office counts the tickets it has left. Keep the concert from selling more than it has when Box Office scales out.
HTTP Load Balancer, Instance Group, Table (DynamoDB)