Vercelには、関数の実行時間に上限があります。Vercelを利用して、待ち合わせ駅を自動提案するサービスを開発していますが、サーバー側で最寄り駅を探し、それから経路を計算する、データベースに対する複数回の入出力を行うといった一連の処理に時間的なコストが発生します。関数の実行時間の制限を知らないまま複雑な実装をしていて、ローカル開発環境では正常に動作したので、デプロイしたところ504エラー(Gateway Timeout)になってしまいました。
まず最初に、処理の途中結果をデータベースに保存することでサーバーの処理を分割し、再帰的に実行する方法を試みましたが、呼び出している関数の実行時間が制限に引っかかってしまいました。素朴に考えて、最寄り駅が表示されたら経路検索を行うボタンを表示させて、ユーザーがアクションを順番に実行する方式へ変更しました。このアプローチでは、処理が完全に分離されており、各アクションは制限時間内に無事に完了しましたが、本質的には不必要な操作が追加され、ユーザー体験が損なわれる結果となりました。
ここまで来ると当たり前だと思われるかもしれませんが、レスポンスがあるごとにページからサーバーに次の処理を伝えるリクエストを自動で送信すれば良いことに気がつきました。リクエストボディに指定するアクション名をパラメータに含めてサーバーに送信することで、各アクションに分割されて順番に処理されます。これで、サーバー側の実装もシンプルになり、いるかもしれないユーザー体験も向上しました。
もし、既知のベストプラクティスがあれば、それを参考にしてください。
サーバー側の実装
サーバー側では、各アクションに対応する関数を定義し、リクエストボディからservice_id
とitem_id
を取得して、処理の途中結果をデータベースとやり取りして全体の処理を行います。コードはイメージで実際の物とは異なります。そのままでは動作しません。
// サーバー側の関数(+server.ts)
async function handleAction(request: Request) {
const { action, service_id, item_id } = await request.json(); // リクエストボディからアクション名とパラメータを取得
switch (action) {// 処理を分岐するためではなく、アクション名を指定するため
case 'actionOne':
return handleActionOne(service_id, item_id);
case 'actionTwo':
return handleActionTwo(service_id, item_id);
case 'actionThree':
return handleActionThree(service_id, item_id);
case 'actionFour':
return handleActionFour(service_id, item_id);
case 'actionFive':
return handleActionFive(service_id, item_id);
case 'actionSix':
return handleActionSix(service_id, item_id);
default:
return { status: 400, body: { message: 'Invalid action' } };
}
}
// 各アクションの実装
async function handleActionOne(service_id, item_id) {
// 処理を実行(途中結果はデータベースに保存)
return { status: 200, body: { message: 'Action One completed' } };
}
async function handleActionTwo(service_id, item_id) {
// 処理を実行(handleActionOneの結果を利用し、途中結果はデータベースに保存)
return { status: 200, body: { message: 'Action Two completed' } };
}
async function handleActionThree(service_id, item_id) {
// 処理を実行(外部のAPIを呼び出すかもしれないが、レスポンスが10秒以内で返ってくるならOK)
return { status: 200, body: { message: 'Action Three completed' } };
}
async function handleActionFour(service_id, item_id) {
// 処理を実行
return { status: 200, body: { message: 'Action Four completed' } };
}
async function handleActionFive(service_id, item_id) {
// 処理を実行
return { status: 200, body: { message: 'Action Five completed' } };
}
async function handleActionSix(service_id, item_id) {
// 処理を実行
const finalValue = "最終的な値"; // 最終的な値を設定
return { status: 200, body: { finalValue: finalValue, message: "処理が完了しました" } };
}
2. ページ側の実装
次に、ページ側の実装を紹介します。各アクションに対応する関数が呼び出され、前の処理が完了した後に次の処理が実行されます。リクエストボディにアクション名や必要なパラメータを含めることで、サーバー側でどのアクションを実行するかを指定します。やはり、コードはイメージで実際のものとは異なります。
// ページ側の実装(+page.svelte)
<script lang="ts">
import { writable } from 'svelte/store';
import { onMount } from 'svelte';
import { page } from '$app/stores';
const isLoading = writable(false);
const actionResponse = writable(null);
let service_id: string;
let item_id: string;
$: {
service_id = $page.params.service_id;
item_id = $page.params.item_id;
}
onMount(async () => {
await performActionOne();
});
async function performActionOne() {
const response = await fetch(``, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'actionOne', service_id: service_id, item_id: item_id }) // アクション名とパラメータを指定
});
if (response.ok) {
const data = await response.json();
actionResponse.set(data.message);
await performActionTwo(); // アクション2を呼び出す
}
}
async function performActionTwo() {
const response = await fetch(``, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'actionTwo', service_id: service_id, item_id: item_id }) // アクション名とパラメータを指定
});
if (response.ok) {
const data = await response.json();
actionResponse.set(data.message);
await performActionThree(); // アクション3を呼び出す
}
}
async function performActionThree() {
const response = await fetch(``, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'actionThree', service_id: service_id, item_id: item_id }) // アクション名とパラメータを指定
});
if (response.ok) {
const data = await response.json();
actionResponse.set(data.message);
await performActionFour(); // アクション4を呼び出す
}
}
async function performActionFour() {
const response = await fetch(``, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'actionFour', service_id: service_id, item_id: item_id }) // アクション名とパラメータを指定
});
if (response.ok) {
const data = await response.json();
actionResponse.set(data.message);
await performActionFive(); // アクション5を呼び出す
}
}
async function performActionFive() {
const response = await fetch(``, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'actionFive', service_id: service_id, item_id: item_id }) // アクション名とパラメータを指定
});
if (response.ok) {
const data = await response.json();
actionResponse.set(data.message);
await performActionSix(); // アクション6を呼び出す
}
}
async function performActionSix() {
const response = await fetch(``, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'actionSix', service_id: service_id, item_id: item_id }) // アクション名とパラメータを指定
});
if (response.ok) {
const data = await response.json();
actionResponse.set(data.finalValue); // 最終的な値を受け取る
}
}
</script>
<main>
<h1>最終結果</h1>
<p>{$actionResponse}</p>
</main>
説明とまとめ
この方法により、Vercelの関数の実行時間の制限を回避し、効率的に実装を追加できます。各アクションを分割することで、可読性も向上します。関数が分離されていることで、新たに処理を実装したらcaseを追加するだけでよく、実装もシンプルになります。ひょっとすると、これがサーバー側の標準的な書き方で、関数のTimeoutエラーには遭遇しないのが当たり前だったりするかもしれません。
あるいは、Vercelの設定の工夫か、他のベストプラクティスが存在するかもしれないので、もしよかったらコメントをお願いします。