SEEDS Creator's Blog

Kotlin/JSのAWS Lambda関数でPromiseを使うようにする

f:id:seeds-std:20191010160615p:plain
こんにちは, Web事業部の西村です
私の前回の記事 では dyanmic の利用をなくすようにしました
そして今回は, Callbackの呼び出しをやめて, JavaScriptの非同期処理( Promise )になるよう変更したいと思います
また, Promiseスタンダード なものと Coroutines のものの2種類がありますので, この記事ではその両方を紹介したいと思います

目次

過去の記事

なぜ非同期に?

非同期処理することにより複数の処理を同時に実行することができます
例えば, 複数のファイルを受け取り, S3に保存する といったことを考えてみます
非同期処理ではない場合, 1ずつしかファイルのアップロードができません
非同期処理の場合, 複数のファイルを同時にアップロードできるようになるので時間の短縮が図れます

注意事項

この記事では Promise の詳しい解説は行いません
詳しく知りたい方は下記の記事を参考にしてください
qiita.com

開発環境

この記事では下記の環境で開発を行っています

  • AdoptOpenJDK 1.8.0_222-b10
  • IntelliJ IDEA Community 2019.2.2
  • Kotlin 1.3.50

プロジェクト

前回の記事 まで作成したプロジェクトを利用します
プロジェクトの構成は下記のようになっています

KotlinLambda
├─.gradle/
├─.idea/
├─build/
├─gradle/
├─src/
│ └─main/
│   └─kotlin/
│     └─jp.co.seeds_std.lambda.kotlin/
│       └─handler.kt
├─build.gradle.kts
├─compress.zip
├─gradlew
├─gradlew.bat
└─settings.gradle.kts

また, handler.ktは下記のようになっています

package jp.co.seeds_std.lambda.kotlin

import kotlin.js.Json
import kotlin.js.Promise
import kotlin.js.json

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json, callback: (Error?, Response?) -> Unit) {
    val response = Response(body = "Hello from Kotlin Class Lambda!")
    callback(null, response)
}

data class Response(
    val statusCode: Int = 200,
    val body: String = "{}",
    val isBase64Encoded: Boolean = false,
    val headers: Json = json(),
    val multiValueHeaders: Json = json()
)

スタンダードなPromise

まずはkotlin-stdlibに実装されている Promise を利用してみます

関数の書き換え

handler 関数を変更します

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json, callback: (Error?, Response?) -> Unit) {
    val response = Response(body = "Hello from Kotlin Class Lambda!")
    callback(null, response)
}

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = Promise<Response> { executor, reject ->
    executor(Response(body = "Hello from Kotlin Async Lambda!"))
}
  • executor は処理成功時に実行する関数で, 引数はジェネリクスで指定したクラス (この記事では Response ) となります
  • reject は処理失敗時に実行する関数で, 引数は Throwable クラスのオブジェクトとなります

動作確認

関数をデプロイし, API GatewayのURLにアクセスし Hello from Kotlin Async Lambda! と表示されれば成功です

もう少し恩恵を受けてみる(スタンダード)

今度はNode.jsの setTimeout を利用して本当に非同期に処理されているかを確認したいと思います
その前に, Kotlin/JSにはNode.jsの setTimeout は定義されていないので追加します
今回追加する場所はコードの最下部に追加します

handler.kt

external fun setTimeout(callback: dynamic, delay: Int, vararg args: Any?): Int
external fun clearTimeout(timeout: Int)
  • external : 外部の関数/変数を利用するために記述します。トランスパイルした場合にそのまま残るようになります
  • callback の部分が dynamic となっていますが, 関数の引数が固定値ではないためやむを得ず dynamic としています

続いて handler 関数を変更します

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = Promise<Response> { executor, reject ->
    val results = (1..10).map {
        Promise<Int> { childExecutor, _ ->
            setTimeout({childExecutor(it)}, 1000)
        }
    }
    Promise.all(results.toTypedArray()).then {
        executor(Response(body = "Result: ${it.sum()} - Standard"))
    }
}

このコードでは 1秒待ってから数字を返す というコードを10回繰り返すものとなっています

動作確認

関数をデプロイし, API GatewayのURLにアクセスし約1秒後に Result: 55 - Standard と表示されれば成功です
※初回の実行では2-3秒ほどかかる場合もあります

通常の処理であれば 1秒待って数字を返す を10回行えば10秒かかるはずですが, 非同期に実行されているため約1秒で完了します

CoroutinesのPromise

標準実装の Promise では見にくく感じませんでしたか?(私は少なからず見にくいなと感じました)
非同期処理を書くのであれば, Node.jsみたいに async で書きたいところです
そこで使うのが kotlinx.coroutines です

依存関係を追加する

kotlinx.coroutines は外部ライブラリなので build.gradle.kts に依存関係を追加する必要があります

build.gradle.kts

implementation(kotlin("stdlib-js"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.3.2") // 追加

コードを書き換える

まずはただの文字列を返すように handelr 関数を変更します

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
    return@promise Response(body = "Hello from Kotlin Coroutines Lambda!")
}

先ほどのコードと比較するとかなり単調なものになりました
※エラーを返す際は throw NullPointerException() のように throw するだけとなります

動作確認

関数をデプロイし, API GatewayのURLにアクセスし Hello from Kotlin Coroutines Lambda! と表示されれば成功です

もう少し恩恵を受けてみる(Coroutines)

先ほどと同じ関数を実装してみます

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
    val tasks = (1..10).map {
        async {
            delay(1000)
            it
        }
    }
    val results = tasks.awaitAll()
    return@promise Response(body = "Result: ${results.sum()} - Coroutines")
}

先ほどとは異なり, CoroutineScope 内で実行できるため, async 関数を利用できます

動作確認

関数をデプロイし, API GatewayのURLにアクセスし約1秒後に Result: 55 - Coroutines と表示されれば成功です
※初回の実行では2-3秒ほどかかる場合もあります

最後に

いかがでしたでしょうか
非同期処理が行えるとよりできることの幅が増えるかと思います
また、Coroutinesの Promise と標準実装の Promise は互換性があるため外部ライブラリが非同期処理を行う場合にも使え,
よりKotlinらしい書き方ができるようになっていくと思います
次回は kotlinx.serialization を用いてJsonのレスポンスを行う記事を書きたいと思います
ここまで読んでいただきありございました

Kotlin/JSのAWS Lambda関数でJsonを返してみる