App Engineで60秒制限を超えそうなリクエストを処理する方法

# お題
表題について、例えば、ある画面に、ボタン押下時の条件（対象期間等）に応じて、ある程度の容量を含む情報を外部から取得してZIPに固めてクライアント側にダウンロードさせるボタンがある。
App Engineではデフォルトで採用されるスケーリング方式だと[１HTTPリクエストの処理期限は「`60秒`」](https://cloud.google.com/appengine/docs/standard/go/how-instances-are-managed?hl=ja#instance_scaling)なので、上記のように時間のかかる処理に関してはタイムアウトを起こす可能性がある。
そういう時用にApp Engineには「タスクキュー（最大10分まで可）」というものが用意されていたのだけど、どうやら今は「[Google Cloud Tasks](https://cloud.google.com/tasks/?hl=ja)」を使うようになっているらしい。
なんだけど、Cloud Tasksはまだ”ベータ版”なのでプロダクションで使うには心もとない。

じゃあ、どうしよう。
とりあえずオーダーだけをRDB等に永続化して、それをApp Engineの[cronジョブ](https://cloud.google.com/appengine/docs/standard/go/config/cron?hl=ja)で毎秒ウォッチしつつ、オーダーがあれば条件に合致する情報を取得してZIPに固めて[Google Cloud Storage](https://cloud.google.com/storage/?hl=ja)に格納。
元の画面側では、Cloud Storage上にZIPが格納されたかを定期的にポーリング（ないし、cronジョブからZIP格納完了通知を何かしらの媒体を介して取得）して監視し、できていればクライアント側にダウンロードさせる。
といった方法が考えられるけど、使うサービス増えるし、もうちょっと簡易的な方法でシンプルにやりたい。

という時に「Basicスケーリング設定の別サービスを作って特定のURLパスでリクエストが来た時だけ、別サービスにディスパッチする」という方法がとれるらしい。

![screenshot-console.cloud.google.com-2019-04-25-08-49-34-337.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/f49e3075-b47e-b08d-10ec-b66f6b5799ea.png)

# 前提
- [GCP](https://cloud.google.com/?hl=ja)は知っている。
- [Google App Engine](https://cloud.google.com/appengine/?hl=ja)は知っている。

以下は済んだ上での作業。

- GCPプロジェクトの作成
- [Cloud SDK](https://cloud.google.com/sdk/docs/)のインストールと初期化・認証
- App Engineの有効化

# 開発環境
### # OS
```
$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="18.04.2 LTS (Bionic Beaver)"
```

### # Cloud SDK
```
$ gcloud version
Google Cloud SDK 242.0.0
```

### # Golang
```
$ go version
go version go1.11.4 linux/amd64
```

# 実践
今回のソース全量は下記。
https://github.com/sky0621/try-gae-go111/tree/9da41fc38a0def2f703abaa11ebb2f074447c1d8/t01

## 必要なファイル群を用意
### HTTPリクエスト処理ロジック

```go:[main.go]
package main

import (
	"fmt"
	"net/http"
	"os"
	"time"

	"github.com/labstack/echo"
)

func main() {
	e := echo.New()
	g := e.Group("/api/v1")

	// デフォルトスケーリング設定用（ドキュメント上、１HTTPリクエストは「60秒」以内に終える必要があると記載あり。）
	g.GET("/automatic/min10", func(c echo.Context) error {
		time.Sleep(10 * time.Minute)
		return c.JSON(http.StatusOK, "OK")
	})

	// ベーシックスケーリング設定用（こちらは最大 24時間までOK）
	g.GET("/basic/min10", func(c echo.Context) error {
		time.Sleep(10 * time.Minute)
		return c.JSON(http.StatusOK, "OK")
	})

	// Google App Engineではデプロイ時にポートが決まり、環境変数「PORT」に接続ポートがセットされる。
	e.Logger.Fatal(e.Start(fmt.Sprintf(":%s", os.Getenv("PORT"))))
}
```

### デフォルトスケーリング用のサービスデプロイ設定ファイル

Goのランタイム指定さえ合っていれば、どのURLパスも受け付ける。

```yaml:[app.yaml]
runtime: go111
```

### ベーシックスケーリング用のサービスデプロイ設定ファイル

URLパスとして「`/api/v1/basic/`」を含む場合用に「`basic-service`」という名前でサービス起動させる設定。

```yaml:[app_basic_scaling.yaml]
runtime: go111

service: basic-service

handlers:
  - url: /api/v1/basic/.*
    script: auto

basic_scaling:
  max_instances: 25
```

### 指定のURLパスだけサービスをデフォルトから分ける設定ファイル

URLパスとして「`/api/v1/basic/`」を含む場合は「`basic-service`」に転送する。

```yaml:[dispatch.yaml]
dispatch:
  - url: "*/api/v1/basic/*"
    service: basic-service
```

## 各種ファイルのデプロイ実行

### デプロイ先の確認

```
$ gcloud config list
[compute]
region = asia-northeast1
zone = asia-northeast1-c
[core]
account = 【自分のアカウント】
disable_usage_reporting = False
project = 【自分のGCPプロジェクトID】
```

### デフォルトサービスのデプロイ

```
$ gcloud app deploy app.yaml 
Services to deploy:

descriptor:      [/home/sky0621/work/src/go111/src/github.com/sky0621/try-gae-go111/t01/app.yaml]
source:          [/home/sky0621/work/src/go111/src/github.com/sky0621/try-gae-go111/t01]
target project:  [【自分のGCPプロジェクトID】]
target service:  [default]
target version:  [20190425t091940]
target url:      [http://【自分のGCPプロジェクトID】.appspot.com]


Do you want to continue (Y/n)?  y

Beginning deployment of service [default]...
Created .gcloudignore file. See `gcloud topic gcloudignore` for details.
╔════════════════════════════════════════════════════════════╗
╠═ Uploading 7 files to Google Cloud Storage                ═╣
╚════════════════════════════════════════════════════════════╝
File upload done.
Updating service [default]...done.                                                                                                                                                                           
Setting traffic split for service [default]...done.                                                                                                                                                          
Deployed service [default] to [http://【自分のGCPプロジェクトID】.appspot.com]
```

### ベーシックスケーリングサービスのデプロイ

```
$ gcloud app deploy app_basic_scaling.yaml 
Services to deploy:

descriptor:      [/home/sky0621/work/src/go111/src/github.com/sky0621/try-gae-go111/t01/app_basic_scaling.yaml]
source:          [/home/sky0621/work/src/go111/src/github.com/sky0621/try-gae-go111/t01]
target project:  [【自分のGCPプロジェクトID】]
target service:  [basic-service]
target version:  [20190425t092233]
target url:      [http://basic-service.【自分のGCPプロジェクトID】.appspot.com]


Do you want to continue (Y/n)?  y

Beginning deployment of service [basic-service]...
╔════════════════════════════════════════════════════════════╗
╠═ Uploading 0 files to Google Cloud Storage                ═╣
╚════════════════════════════════════════════════════════════╝
File upload done.
Updating service [basic-service]...done.                                                                                                                                                                     
Setting traffic split for service [basic-service]...⠹                                                                                                                                                        
Setting traffic split for service [basic-service]...⠼                                                                                                                                                        
Setting traffic split for service [basic-service]...⠶                                                                                                                                                        
Setting traffic split for service [basic-service]...done.                                                                                                                                                    
Deployed service [basic-service] to [http://basic-service.【自分のGCPプロジェクトID】.appspot.com]
```

### ディスパッチ設定のデプロイ

```
$ gcloud app deploy dispatch.yaml 
Configurations to update:

descriptor:      [/home/sky0621/work/src/go111/src/github.com/sky0621/try-gae-go111/t01/dispatch.yaml]
type:            [routing rules]
target project:  [【自分のGCPプロジェクトID】]


Do you want to continue (Y/n)?  y

Updating config [dispatch]...done.                                                                                                                                                                           

Custom routings have been updated.
```

## デプロイ結果の確認

デフォルトとベーシックそれぞれのサービスが出来てる。

![screenshot-console.cloud.google.com-2019-04-25-09-27-14-670.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/3364673d-f5f3-7a95-3fa2-9cbcd1a14907.png)

## 動作検証

### デフォルトサービスにアクセス

レスポンスがタイムアウトを表しているのかが判然とはしないが、”OK"は返っていない。

```
$ curl -m 900 -X GET \
>   https://【自分のGCPプロジェクトID】.appspot.com/api/v1/automatic/min10 \
>   -H 'Content-Type: application/json' \
>   -H 'cache-control: no-cache'

<html><head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>500 Server Error</title>
</head>
<body text=#000000 bgcolor=#ffffff>
<h1>Error: Server Error</h1>
<h2>The server encountered an error and could not complete your request.<p>Please try again in 30 seconds.</h2>
<h2></h2>
</body></html>
```


### ベーシックスケーリングサービスにアクセス

デフォルトサービスと同じ 10分 のスリープを設定した結果、"OK"が返った。

```
$ curl -m 900 -X GET \
>   https://【自分のGCPプロジェクトID】.appspot.com/api/v1/basic/min10 \
>   -H 'Content-Type: application/json' \
>   -H 'cache-control: no-cache'
"OK"
```

# まとめ
うまくいった。
ただ、今回のソースは、１ファイルにどちらのサービス用のロジックも書いていて、それを「デフォルトサービス」と「ベーシックスケーリングサービス」それぞれに同じものをデプロイしている。
なので、本来は、デフォルトサービス用とベーシックスケーリングサービス用とで、それぞれに必要なURLパス処理ロジックだけが含まれるようにした方がいい。