RenderComのスピンダウン回避の為にAndroidを使用した話

# [Render.com](https://render.com/)とは?
[Render.com](https://render.com/)は雑に言うと[Heroku](https://jp.heroku.com/)みたいな物です。

Herokuは無料枠が消えましたがRender.comはまだできます。

(ここからは詳細な話。趣旨とは離れるので興味のない方はスキップしてください。)

Renderは5つのサービスを提供しています。
- ありふれたウェブサービス
- 静的サイト
- プライベートサービス
- バックグラウンドワーカー
- Cronジョブ

また、下記の物も使用できます。
- PostgreSQL
- Redis

## 無料枠について
一部のサービスのインスタンスは無料で作れます
- webサービス
- PostgreSQL
- Redis

が、制限として下記の物があります。
- 15分アクセスが無いと停止します(スピンダウン)。停止時にアクセスされると1分ぐらいかけて再起動されます
    - つまり15分に1回はアクセスすればよい
- 毎月750時間までしか起動できません
    - 24時間*31日=744時間なので、単一のサービスであれば問題ない。複数のサービスをしたいなら課金するか別アカウントを作成する必要がある
- Postgresqlは30日でアクセスできなくなります
- Postgresqlは突然メンテナンスに入り一時的にアクセスできなくなる可能性があります
- Postgresqlは突然再起動されることがあります

等、色々な制限があります。
詳細は[Deploy for Free](https://docs.render.com/free)のページをご覧ください。

また、他の有料プランについては [Pricing](https://render.com/pricing)のページをご覧ください

# スピンダウンを回避する
無料枠を使うにあたり、15分アクセスが無いときにスピンダウンされる問題があります。これを回避する方法としていくつかの手段があります

- [UptimeRobotを使う](https://qiita.com/ppputtyo/items/2ab44303dcfabeb7b23e)
- [CloudFunctionsを使う](https://zenn.dev/yutasb/articles/cb5b58e1ab6de8)
- [Google App Scriptを使う](https://zenn.dev/no215/articles/ff4f1670c080e4) [別記事1](https://zenn.dev/sdb_blog/articles/002-render-okosu)

自分はUptimeRobotを使用していましたが、何故か落ちる...
(GETじゃなくてHEADだったのが悪いのかな...?)

無料サービスを便利に使うために別の無料サービスに負荷をかけるのはちょっと気が引けるのもあり、Androidでアクセスするようにしました。

# 回避アプリ
RaspBerry PiとかでこんなPythonのコード
```
import requests
import time
while True:
  requests.get("hogehoge.com")
  time.sleep(60*13)
```
を書いて動かすでも良いのですが、ぶっちゃけこれだけの為にRaspBerryPi買うのはもったいないので、Androidを使用することにしました。

## 使用する機能
- WebView
- ForeGround Service

### アプリ起動中
アプリが起動している状態で、アプリを見ている際にはMainActivityのWebView上で指定されたURLを読み込みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3838545/5cdfa652-9216-5581-ff48-7c288080c3f8.png)

`onCreate` の時にここら辺を全部設定します。
```
private var _binding: ActivityMainBinding? = null
private val binding: ActivityMainBinding get() = _binding!!
private val handler = Handler(Looper.getMainLooper())
private val reloadInterval = 300000L
private val sdf = SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.JAPAN).apply {
    timeZone = TimeZone.getTimeZone("Asia/Tokyo")
}
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    _binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)

    setupWebViews()
    startReloading()

    binding.button.setOnClickListener {
        update()
    }
}

private fun setupWebViews() {
    binding.wb1.loadUrl("https://google.com")
}
private fun startReloading() {
    val reloadRunnable = object : Runnable {
        override fun run() {
            update()
            handler.postDelayed(this, reloadInterval)
        }
    }
    handler.post(reloadRunnable)
}

private fun reloadWebViews() {
    binding.wb1.reload()
    binding.wb2.reload()
    binding.wb3.reload()
}
```
最初にURL読み込んで、Handlerを使って5分毎にリロードをしています。
また、リロードボタンを押すと即時にリロードします。

最後に読み込みを行った時刻が表示される仕組みになっています。

### アプリがバックに居る時
Foreground Serviceを起動します。
```
override fun onResume() {
    super.onResume()
    val intent = Intent(this@MainActivity, BackGroundRequestService::class.java)
    stopService(intent)
}

override fun onPause() {
    super.onPause()
    val intent = Intent(this@MainActivity, BackGroundRequestService::class.java)
    startService(intent)
}


override fun onStart() {
    super.onStart()
    val intent = Intent(this@MainActivity, BackGroundRequestService::class.java)
    stopService(intent)
}

override fun onStop() {
    super.onStop()
    val intent = Intent(this@MainActivity, BackGroundRequestService::class.java)
    startService(intent)
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3838545/c6e24908-9862-18b7-11ad-12892e4859c4.png)

このように表示されます。
処理自体はMainActivityとほぼ同じなので、ほとんど説明を省きます。
reloadの部分を、okHttpClientでアクセスを行うに変更しているだけです。

```
private fun makeNetworkRequest(urlString: String) {
    CoroutineScope(Dispatchers.IO).launch {
        val client = OkHttpClient()
        val request = Request.Builder()
            .url(urlString)
            .build()
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                Log.w("HTTP Request", "Failed to execute request", e)
            }

            override fun onResponse(call: Call, response: Response) {
                Log.w("HTTP Request", "Success to execute request")
            }
        })
    }
}
```

# 分からないところ
今回、サービス周りをどのライフサイクルに紐づければ良いかぶっちゃけよくわからなかったので適当に付けた。

Android公式のActivity Lifecycleの図的に、onPauseとonStopでサービスを開始して、onStartとonResumeで停止すればいいか！と思いこのような実装にした。

(今考えるとonStartの時別に停止しなくて良いか?)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3838545/87f5afc7-6343-4251-9a4a-7c7785e1f139.png)

# まとめ
スピンダウンしないためにAndroidで5分に1回HTTPリクエストを送信するようにしました。

レポジトリ
https://github.com/holndal/RenderComAliver