Challenges / The concert that oversold

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)A 30-second run, scored against 4 goals
Start this challengeIt runs in your browser. No account, nothing to install.

The setup

Ticket BuyersRequest Generator
Ticket BalancerHTTP Load Balancer
Box OfficeInstance Group · 1 instance
TicketsTable (DynamoDB)

Box Office sells the concert's 500 tickets. Five seconds in, it scales out to three instances to keep up with the buyers.

Sell every ticket exactly once, however many instances are running. Tickets is a table every instance can reach.

The run

Each goal is judged over its own stretch of the run rather than the whole of it.

0 s30 s
  • 5 s · Box Office scales out to three instances

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.

Box Office / server.js
import express from 'express';
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({});

// The tickets this process has left to sell. A variable lives in the memory of the process
// that declared it, and starts again at 500 whenever a process starts
let remaining = 500;

// 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 buys a ticket
putMetric('tickets sold', 0, 'Count');

const app = express();
app.use(express.json());

// The load balancer asks for this before it sends any buyers 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('/tickets', async (req, res) => {
  const { buyer = 'nobody' } = req.body;

  // Sold out is an answer, not a failure, so it is a 409 rather than an error
  if (remaining === 0) {
    res.status(409).json({ status: 'sold out' });
    return;
  }
  remaining -= 1;

  // Every ticket sold is recorded, keyed by who bought it
  await dynamodb.send(
    new PutItemCommand({
      TableName: tableName,
      Item: { id: { S: `ticket-${buyer}` }, buyer: { S: buyer } }
    })
  );

  putMetric('tickets sold', 1, 'Count');
  res.status(201).json({ status: 'sold' });
});

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

30s run · 2 goals

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.

HTTP Load Balancer, Instance Group, Table (DynamoDB)

Usually solved with: Queue (SQS), Function (Lambda)

+
30s run · 4 goals