ばとらの部屋

Maximum AtCoder Leaderboardを作りました

作成日 2024年10月30日 / 最終更新日 2025年1月29日

サイトは こちら から見ることができます。

※現在サイトに欠陥があったため非公開

ソースコードはGitHubリポジトリから見ることができます。


hero

きっかけ

埼玉大学プログラミングサークル「Maximum」では、活動の1つとして競技プログラミングがあり、部内メンバーはAtCoderのコンテストに積極的に参加しています。

そこで、メンバーにもっと競争力を持ってもらうために、部内の月間ランキングなんてあったらいいな~と思い開発に取り掛かりました。

技術スタック

フロントエンド

  • JavaScript (React)

バックエンド

  • Node.js (Hono.js)

デプロイ

  • Cloudflare Pages (フロントエンド)
  • Cloudflare Workers (バックエンド)

実装

実装方法は以下の図のようになります。どうやらAtCoderは、各ユーザーのリンクの末尾に/history/jsonを追加することで、これまでに参加したコンテストの詳細やレートの変動などの情報がjson形式で受け取ることができるので、これをプロキシサーバーを介して(CORSの都合上)fetchをすることでAPIとして過去の成績を取得することができます。

部員のユーザー情報を取得する流れ

フロントエンドの処理は省きます

エラーハンドリング

適切なリクエストを送信するために、フロントから送られてきたユーザーのリストusersに対してエラーハンドリングを行います。

if (!users) return c.text("Bad Request", 400);
if (!Array.isArray(users)) return c.text("Bad Request", 400);
if (!users.every((user) => typeof user === "string"))
  return c.text("Bad Request", 400);
  • そもそもusersがundefinedの場合
  • usersが配列ではない場合
  • 要素が文字列ではない場合

このような場合にはBad Requestを返却することにします。

ユーザー毎にAPIを叩く

各ユーザーに対する処理の全体は以下のようになります。

const userData = [];
for (const user of users) {
const cache = await c.env.USER_CACHE.get(user);
if (cache) {
    const { data, timestamp } = JSON.parse(cache);
    if (Date.now() - timestamp < 1000 * 60 * 60) {
    userData.push(data);
    continue;
    }
}
const res = await fetch(`https://atcoder.jp/users/${user}/history/json`);
const data = await res.json();
userData.push(data);
await c.env.USER_CACHE.put(user, JSON.stringify({
    data,
    timestamp: Date.now(),
}));
await new Promise((resolve) => setTimeout(resolve, 1000));
}

各リクエストに対して1000msの遅延をしています。

キャッシュの確認

Cloudflare Workersのキャッシュ先としてKVへの保存を選びました。詳しくは以下のサイトをご覧ください。

動的コンテツをエッジのKVにキャッシュする - ゆーすけべー日記

Web APIのパフォーマンス向上に「Dynamic Content Storing = DCS」という戦略を考えている。 Web APIに限らず、サーバーサイドで動的に作られるコンテンツ全てへ適応できるものである。 本番環境で運用したわけではない

https://yusukebe.com/posts/2022/dcs/

ユーザーに対してキャッシュが存在するかをKV USER_CACHEを確認します。

const cache = await c.env.USER_CACHE.get(user);

キャッシュが存在するときは、有効期限を確認して期限内であればfetchをせずにキャッシュからデータを取ってきてそのままリストuserDataへ挿入します。

if (cache) {
    const { data, timestamp } = JSON.parse(cache);
    if (Date.now() - timestamp < 1000 * 60 * 60) {
    userData.push(data);
    continue;
    }
}

KVにキャッシュする

キャッシュが存在しない或いは有効期限が切れているときは、KVに新たにキャッシュを保存します。

await c.env.USER_CACHE.put(user, JSON.stringify({
    data,
    timestamp: Date.now(),
}));

このようにユーザーに対してデータとキャッシュの有効期限を持たせます。