【Kubernetes】KubernetesのCronJobのcronスケジュールはどう管理されているのか?
はじめに
本記事は Kuberenetes Advent Calendar 2019の17日目の記事です。 Kubernetesのリソースの中に、定期的に揮発性のあるジョブを生み出すCronJobについての記事です。
本題
cronjobは以下のようなマニフェストで定義されます。
apiVersion: batch/v1beta1 kind: CronJob metadata: name: hello spec: schedule: "*/1 * * * *" jobTemplate: spec: template: spec: containers: - name: hello image: busybox args: - /bin/sh - -c - date; echo Hello from the Kubernetes cluster restartPolicy: OnFailure
Running Automated Tasks with a CronJob - Kubernetes
さて、このcronjobの schedule: "*/1 * * * *"
が今回のブログの主役です。
このスケジュールは一体どう管理されているんでしょうか?パースして・・・どうしてるんだろう?気になりませんか?
今回はこの文字列がどうやって管理されて、ジョブの生成に繋がっているかに焦点をおきます。
仮説は以下です。
①Kubernetesからglibcのlibcron的な何かを叩いている
②Kubernetes上で文字列をパースして独自実装されている
③第三者のなんかのライブラリで間接的に使っている
多分cronjobというやつもdeploymentと同じでコントローラがあると思っていて、 そいつがなんやかんやして定期的にjobをリソースを生み出している機構があるのでしょう。 そこまでいけたらゴールです。 さて、見ていきますか。
ソースを読む。
もう最初からソース見た方が早いよねってことで。
func startCronJobController(ctx ControllerContext) (http.Handler, bool, error) { if !ctx.AvailableResources[schema.GroupVersionResource{Group: "batch", Version: "v1beta1", Resource: "cronjobs"}] { return nil, false, nil } cjc, err := cronjob.NewController( ctx.ClientBuilder.ClientOrDie("cronjob-controller"), ) if err != nil { return nil, true, fmt.Errorf("error creating CronJob controller: %v", err) } go cjc.Run(ctx.Stop) return nil, true, nil }
うん、、、どうやら予想通り cronjob-controller
という奴がいそうで、 "k8s.io/kubernetes/pkg/controller/cronjob"
を見ていけば何かがわかりそう。
ここにあるようだ→ https://github.com/kubernetes/kubernetes/blob/1514bb2141/pkg/controller/cronjob/controller.go
よく見てみると、わずか数個しか関数がないのがわかります。
func NewController(kubeClient clientset.Interface) (*Controller, error) func (jm *Controller) Run(stopCh <-chan struct{}) func (jm *Controller) syncAll() func cleanupFinishedJobs(sj *batchv1beta1.CronJob, js []batchv1.Job, jc jobControlInterface, sjc sjControlInterface, recorder record.EventRecorder) func removeOldestJobs(sj *batchv1beta1.CronJob, js []batchv1.Job, jc jobControlInterface, maxJobs int32, recorder record.EventRecorder) func syncOne(sj *batchv1beta1.CronJob, js []batchv1.Job, now time.Time, jc jobControlInterface, sjc sjControlInterface, recorder record.EventRecorder) func deleteJob(sj *batchv1beta1.CronJob, job *batchv1.Job, jc jobControlInterface, recorder record.EventRecorder) func getRef(object runtime.Object) (*v1.ObjectReference, error)
cronjobを使ったことあるひとは関数名だけで動作が手に取ったようにわかるかもしれません。
知らんけど多分、 syncAll
syncOne
がjobを生み出している関数だってことくらいはわかりました。
syncAll
こいつは単純で、 クラスター内のcronjobをリソースを持ってきて、それをfor文でぶん回して syncOne
を叩いてるだけのようです。
func (jm *Controller) syncAll() { // snip for _, sj := range sjs { syncOne(&sj, jobsBySj[sj.UID], time.Now(), jm.jobControl, jm.sjControl, jm.recorder) cleanupFinishedJobs(&sj, jobsBySj[sj.UID], jm.jobControl, jm.sjControl, jm.recorder) } // snip }
syncOne
コメントアウトに注目
// syncOne reconciles a CronJob with a list of any Jobs that it created. // All known jobs created by "sj" should be included in "js". // The current time is passed in to facilitate testing. // It has no receiver, to facilitate testing. func syncOne(sj *batchv1beta1.CronJob, js []batchv1.Job, now time.Time, jc jobControlInterface, sjc sjControlInterface, recorder record.EventRecorder) {
// syncOne reconciles a CronJob with a list of any Jobs that it created. // All known jobs created by "sj" should be included in "js". // The current time is passed in to facilitate testing. // It has no receiver, to facilitate testing.
syncOneは、CronJobと、作成したジョブのリストを照合します。 「sj」によって作成されたすべての既知のジョブは、「js」に含める必要があります。テストを容易にするために、現在の時刻が渡されます。テストを容易にするための受信機はありません。
結構重要なのはなんと、この now time.Time
という引数はテストのために渡されているということが書かれていますが、
多分関数の中で time.Now()するとテストの時に照合しにくくなってしまうから引数で渡そう的なアイディアで、実処理にも使われてはいると推測します。
するすると見ていくと、実にあやしいコードを見つけます。
times, err := getRecentUnmetScheduleTimes(*sj, now) if err != nil { recorder.Eventf(sj, v1.EventTypeWarning, "FailedNeedsStart", "Cannot determine if job needs to be started: %v", err) klog.Errorf("Cannot determine if %s needs to be started: %v", nameForLog, err) return } // TODO: handle multiple unmet start times, from oldest to newest, updating status as needed. if len(times) == 0 { klog.V(4).Infof("No unmet start times for %s", nameForLog) return } if len(times) > 1 { klog.V(4).Infof("Multiple unmet start times for %s so only starting last one", nameForLog) }
うんーどうやらここっぽいねえ。その後の処理を見ても、どうやらこの times
というリストが結構参照されているので重要な奴だそうだ。
どう生み出されているのか見ていこう。
(ちなみにsjは*batchv1beta1.CronJobなので、cronjobリソースそのものだ、なので尚更あやしい)
getRecentUnmetScheduleTimes
この子はさっき見ていた controller.go
にはなかった。
同パッケージ内の utils.go
に入っている。
// getRecentUnmetScheduleTimes gets a slice of times (from oldest to latest) that have passed when a Job should have started but did not. // // If there are too many (>100) unstarted times, just give up and return an empty slice. // If there were missed times prior to the last known start time, then those are not returned. func getRecentUnmetScheduleTimes(sj batchv1beta1.CronJob, now time.Time) ([]time.Time, error) { starts := []time.Time{} sched, err := cron.ParseStandard(sj.Spec.Schedule) if err != nil { return starts, fmt.Errorf("Unparseable schedule: %s : %s", sj.Spec.Schedule, err) } var earliestTime time.Time if sj.Status.LastScheduleTime != nil { earliestTime = sj.Status.LastScheduleTime.Time } else { // If none found, then this is either a recently created scheduledJob, // or the active/completed info was somehow lost (contract for status // in kubernetes says it may need to be recreated), or that we have // started a job, but have not noticed it yet (distributed systems can // have arbitrary delays). In any case, use the creation time of the // CronJob as last known start time. earliestTime = sj.ObjectMeta.CreationTimestamp.Time } if sj.Spec.StartingDeadlineSeconds != nil { // Controller is not going to schedule anything below this point schedulingDeadline := now.Add(-time.Second * time.Duration(*sj.Spec.StartingDeadlineSeconds)) if schedulingDeadline.After(earliestTime) { earliestTime = schedulingDeadline } } if earliestTime.After(now) { return []time.Time{}, nil } for t := sched.Next(earliestTime); !t.After(now); t = sched.Next(t) { starts = append(starts, t) // An object might miss several starts. For example, if // controller gets wedged on friday at 5:01pm when everyone has // gone home, and someone comes in on tuesday AM and discovers // the problem and restarts the controller, then all the hourly // jobs, more than 80 of them for one hourly scheduledJob, should // all start running with no further intervention (if the scheduledJob // allows concurrency and late starts). // // However, if there is a bug somewhere, or incorrect clock // on controller's server or apiservers (for setting creationTimestamp) // then there could be so many missed start times (it could be off // by decades or more), that it would eat up all the CPU and memory // of this controller. In that case, we want to not try to list // all the missed start times. // // I've somewhat arbitrarily picked 100, as more than 80, // but less than "lots". if len(starts) > 100 { // We can't get the most recent times so just return an empty slice return []time.Time{}, fmt.Errorf("too many missed start time (> 100). Set or decrease .spec.startingDeadlineSeconds or check clock skew") } } return starts, nil }
コメントアウトから
getRecentUnmetScheduleTimes gets a slice of times (from oldest to latest) that have passed when a Job should have started but did not.
getRecentUnmetScheduleTimesは、ジョブが開始されるべきであるが、開始されなかったときに経過した時間のスライス(最も古いものから最新のものまで)を取得します。
ほほう。先ほどの読み出し元の関数と見比べてみても、この getRecentUnmetScheduleTimes
が返す []time.Time
を元に、 スケジュール時間がすぎて開始されるべきジョブ
を見極めるようです。
っと言っていると・・・おっっ!!!
"github.com/robfig/cron"
sched, err := cron.ParseStandard(sj.Spec.Schedule)
おおー!これが答えみたいなもんです・・・!なんと、予想の ③第三者のなんかのライブラリで間接的に使っている
でした・・・結構意外でしょう?取れ高ですね。
さて、最後にこの関数をみていきましょう
(めっちゃスターついてますやん)
cron.ParseStandard
// ParseStandard returns a new crontab schedule representing the given // standardSpec (https://en.wikipedia.org/wiki/Cron). It requires 5 entries // representing: minute, hour, day of month, month and day of week, in that // order. It returns a descriptive error if the spec is not valid. // // It accepts // - Standard crontab specs, e.g. "* * * * ?" // - Descriptors, e.g. "@midnight", "@every 1h30m" func ParseStandard(standardSpec string) (Schedule, error) { return standardParser.Parse(standardSpec) } // Parse returns a new crontab schedule representing the given spec. // It returns a descriptive error if the spec is not valid. // It accepts crontab specs and features configured by NewParser. func (p Parser) Parse(spec string) (Schedule, error) { if len(spec) == 0 { return nil, fmt.Errorf("empty spec string") } // Extract timezone if present var loc = time.Local if strings.HasPrefix(spec, "TZ=") || strings.HasPrefix(spec, "CRON_TZ=") { var err error i := strings.Index(spec, " ") eq := strings.Index(spec, "=") if loc, err = time.LoadLocation(spec[eq+1 : i]); err != nil { return nil, fmt.Errorf("provided bad location %s: %v", spec[eq+1:i], err) } spec = strings.TrimSpace(spec[i:]) } // Handle named schedules (descriptors), if configured if strings.HasPrefix(spec, "@") { if p.options&Descriptor == 0 { return nil, fmt.Errorf("parser does not accept descriptors: %v", spec) } return parseDescriptor(spec, loc) } // Split on whitespace. fields := strings.Fields(spec) // Validate & fill in any omitted or optional fields var err error fields, err = normalizeFields(fields, p.options) if err != nil { return nil, err } field := func(field string, r bounds) uint64 { if err != nil { return 0 } var bits uint64 bits, err = getField(field, r) return bits } var ( second = field(fields[0], seconds) minute = field(fields[1], minutes) hour = field(fields[2], hours) dayofmonth = field(fields[3], dom) month = field(fields[4], months) dayofweek = field(fields[5], dow) ) if err != nil { return nil, err } return &SpecSchedule{ Second: second, Minute: minute, Hour: hour, Dom: dayofmonth, Month: month, Dow: dayofweek, Location: loc, }, nil }
https://github.com/robfig/cron/blob/e843a09e5b2db454d77aad25b1660173445fb2fc/parser.go
ParseStandard(standardSpec string)
引数が文字列なので間違って無さそうですが、何よりその処理中が興味深いですね・・・つまり構文解析は自前で実装してるわけです。 使い方も間違って無さそう
func TestStandardSpecSchedule(t *testing.T) { entries := []struct { expr string expected Schedule err string }{ { expr: "5 * * * *", expected: &SpecSchedule{1 << seconds.min, 1 << 5, all(hours), all(dom), all(months), all(dow), time.Local}, }, { expr: "@every 5m", expected: ConstantDelaySchedule{time.Duration(5) * time.Minute}, }, { expr: "5 j * * *", err: "failed to parse int from", }, { expr: "* * * *", err: "expected exactly 5 fields", }, } for _, c := range entries { actual, err := ParseStandard(c.expr) if len(c.err) != 0 && (err == nil || !strings.Contains(err.Error(), c.err)) { t.Errorf("%s => expected %v, got %v", c.expr, c.err, err) } if len(c.err) == 0 && err != nil { t.Errorf("%s => unexpected error %v", c.expr, err) } if !reflect.DeepEqual(actual, c.expected) { t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual) } } }
https://github.com/robfig/cron/blob/master/parser_test.go#L315-L351
結論
ということで、KubernetesのCronJobのcronスケジュールは https://github.com/robfig/cron
によってパースしたのちに、現在時刻から比較してジョブを生成する工程に入る、でした。
ちょっぴり考察すると、libcronみたいなCのライブラリとかを使っちゃうとKuberentesのコントローラ自体の依存が増えてしまうので、
今回使われた robfig/cron
のようなgo言語のみでcron文字列をパースできるライブラリを使ったんじゃないかなーと思ったりラジバンダリ😪
おまけ
このcronjobのスケジューラのライブラリのなかで、使われた ParseStandard
ですが、これは、
以下の二つのページを参照してくれよとREADME書いてあるので、何のフォーマットが対応しているかは以下を参照してください。
https://en.wikipedia.org/wiki/Cron
[http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/tutorial-lesson-06.html
【kotlin】kotlinで書いたスクリプトを叩きたい人生だった
はじめに
本記事は Kotlin Advent Calendar 2019の9日目の記事です。
この記事で書くこと
kotlinのプロジェクトを管理していると、 いわゆる本体のアプリケーション以外にも、 DBのマイグレーションやちょっとしたデプロイスクリプトみたいな処理を書いて、 その1枚ペラの.ktファイルだけを実行したい時があったりします。
それらをコンパイルして javaで実行してもいいですが、いちいちそんなことで jarファイルを作りたくないですし、 javaのclassPath指定とかめっちゃ面倒です。
そんな時どうすればいいのか、代表的な三つの方法を書いていきます。
- ktsファイルを書いて実行する。
- kotlinで書いてgradleで叩く。
- kscriptを使う。
①ktsファイルを書いて実行する。
王道です。
println("Hello World!")
と適当な Main.kts
を書いて、以下のように実行します。
kotlinc -script Main.kts
うーん簡単に叩けるねえ。 しかしこれは本当に簡単なスクリプトの場合はで、ライブラリを使うとちと面倒で、
https://blog.jetbrains.com/kotlin/2018/09/kotlin-1-3-rc-is-here-migrate-your-coroutines/#scripting
kotlinの13.0RCから入った次のようなannotationで依存関係を記載することなどで実行可能です。
以下のような sample.main.kts
を作り、
@file:Repository("https://jcenter.bintray.com") @file:DependsOn("org.jetbrains.kotlinx:kotlinx-html-jvm:0.6.11") import kotlinx.html.* import kotlinx.html.stream.* print(createHTML().html { body { h1 { +"Hello, World!" } } })
以下のように実行します。
kotlinc -cp <path/to/kotlin-main-kts.jar> -script sample.main.kts
②kotlinで書いてgradleで叩く。
「え?.ktsファイルで書かないの?」って感じですが、たまにはpureなkotlinを実行したい時もあります。 一番簡単なのが、gradleのなかで JavaExecなtask として定義してしまうことです。
package nnao4.task import kotlinx.html.* import kotlinx.html.stream.* fun main() { print(createHTML().html { body { h1 { +"Hello, World!" } } }) }
sourceSets { main.kotlin.srcDirs = main.java.srcDirs = ['src/main/kotlin'] } task runTask(type: JavaExec) { main ='nnao4.task.MainKt' classpath = sourceSets.main.runtimeClasspath }
これだけでもなんと、依存モジュールも併せて通常通りコンパイルするので実行できてしまいます。 .ktsでは書きにくいスクリプトもこれで簡単に実行できるようにできますね。
gradle runTask
③kscriptを使う。
出ました外部ライブラリ。
どうやら kotlinc
のラッパーのようで、
以下が特徴的な機能だそうです。
In particular this wrapper around kotlinc adds
- Compiled script caching (using md5 checksums)
- Dependency declarations using gradle-style resource locators and automatic dependency resolution with jcabi-aether
- More options to provide scripts including interpreter mode, reading from stdin, local files or URLs
- Embedded configuration for Kotlin runtime options
- Support library to ease the writing of Kotlin scriptlets
- Deploy scripts as stand-alone binaries
高性能でテンション上がりますね😊使っていきましょう。
sdkmanを使えばinstallは一撃
sdk install kscript
さて肝心の使いっぷりですが、割と衝撃を受けます。
#!/usr/bin/env kscript println("Hello from Kotlin!") for (arg in args) { println("arg: $arg") }
え・・・そういうこと・・・!? 実行ファイルから実行できるの!?( ´ ▽ ` ;)
ということは標準出力からも原理的に可能ですね、
kscript 'println("hello world")'
まじか・・・ちゅご・・・。 しかも衝撃的なことに、依存モジュールも対応しちゃってます!
#!/usr/bin/env kscript //DEPS com.offbytwo:docopt:0.6.0.20150202,log4j:log4j:1.2.14 import org.docopt.Docopt import java.util.* val usage = """ Use this cool tool to do cool stuff Usage: cooltool.kts [options] <igenome> <fastq_files>... Options: --gtf <gtfFile> Custom gtf file instead of igenome bundled copy --pc-only Use protein coding genes only for mapping and quantification """ val doArgs = Docopt(usage).parse(args.toList()) println("Hello from Kotlin!") println("Parsed script arguments are: \n" + doArgs)
当然、↑の方でも書いたようなアノテーションで依存性解決をすることもできます。 便利ですね(^▽^)ktsとしてjavaモジュール使ってなんか適当な関数発火するならこっちでいいのかもしれません。
おわりに
サーバサイドkotlinは使い勝手がよく、AndroidとJavaの資産を両方に使える器用なやつです。 サーバサイドとしてはバッチ処理なども任せることも今後も多くなってくるでしょう。 そんな時に上記の方法が頭に残っていると便利かもしれません。
【kotlin】kotlin製ORM Exposed小技集
はじめに
本記事は Kotlin Advent Calendar 2019の8日目の記事です。 Jetbrain社製のkotlinで書かれたORMについて話していきます。
Exposedの軽い紹介
exposedでは以下のようにクエリを書くことができます
object program : IntIdTable() { val name = varchar("name", 255) val event_id = (entityId("event_id", event ) references event.id) val start_at = datetime("start_at") val end_at = datetime("end_at") val created = datetime("created") val updated = datetime("updated") }
program
.select { (program.start_at lessEq now) and (program.end_at greaterEq now) }
.count()
// SELECT count(*) FROM `program` WHERE `start_at` <= @now AND @now <= `end_at`
kotlinのinline関数なども相まって、 kotlinのコードに関わらず、もとのSQLにとても似ているコードを書くことができます。
可読性がとてもよく、最低限のSQLのクエリも揃っていますが、 jooq
や Hibernate
に比べるとsyntaxにまだまだ不揃いなだったり、情報がたらない時があります。
そんな漢字の、僕が初めて使ったときに困ったところと、それを切り抜ける技を紹介していきます。
ただ、困ったらまずは以下のgithubのwikiをみましょう。
https://github.com/JetBrains/Exposed/wiki
やっていき
そもそもSELECT文で取り出したEntityをData Classに射影するのどうするの?
これは意外とREADMEとかパッと見で書いてなくて困りました。
// Exposed は↓のように ` IntIdTable()` や ` UUIDTable()` を継承したobjectを定義してそれをスキーマ定義とします。 object question : IntIdTable() { val name = varchar("name", 255).default("anonymous") val created = datetime("created") } data class Question( val id: EntityID<Int>, val name: String, val created: Date ) fun findByID(id: Int): Question?= transaction { question .select { question.id eq id } .firstOrNull() // ??? }
困りました、 question.select { question.id eq id }.firstOrNull()
だけだと戻り値が ResultRow?
ですね。
ここから Question
に戻したいわけです。
さてこの ResultRow
ですが、
class ResultRow(internal val fieldIndex: Map<Expression<*>, Int>) = // 実装
コードを見てみると見てみると、ハッシュマップになっていて、キーを与えると取り出せる構造になっているようです。
このキーが、 object question : IntIdTable()
でORMとして定義したテーブルのプロパティと対応しているので、それをあてこんで引き出す事となります。
つまり以下のような関数と定義してやって
fun ResultRow.toQuestion(): Question = Question( this[question.id], this[question.name], this[question.created], )
さっきの findById
関数にこれをつなげればいいということになります。
fun findByID(id: Int): Question?= transaction { question .select { question.id eq id } .firstOrNull()?.let { it.toQuestion() } }
iterableな戻り値だった場合は、
fun findByID(id: Int): List<Question>= transaction { question .selectAll() .map { it.toQuestion() } }
と書けます、どちらも可読性が高くていい感じですね。
WHERE句を繰り返しANDで繋げるのをkotlinでやりたい
Exposedでwhere句を書く時には以下のように書くのが基本です。
question .select { question.id eq 1 } // SELECT * FROM question WHERE `id` = 1
しかし、WHERE句が WHERE id = 1 AND id = 2 AND id = 3
だと大変です。
当然このDSL記法だと書ききれませんし、
もしこれらを生SQLでやろうとすると1番目の条件だけ WHERE
で書いて、残りは AND
でやらないといけないので面倒ですね。
それにkotlinで書ければ単純にコード内の型同士で値比較できるので、なるべくkotlinで書きたいです。
Exposedには andWhere
という便利なメソッドがあります。
val ids = listOf(1, 2, 3) question .selectAll() .also { prepare -> ids.map { prepare.andWhere { question.id eq it } } } // SELECT * FROM question WHERE `id` = 1 AND `id` = 2 AND `id` = 3
(あくまで例なので、こういう場合普通 IN句でやるだろっていうのは置いておいてくださいw)
非常に便利ですね、このようにkotlinの得意なfunctionalな書きっぷりとExposedは非常に相性がいいです。
Exposedで生SQLを扱う
そもそもExposedで生SQLをどう扱うんだって話です。
できれば、 SELECT...
みたいなSQL文の文字列からSQL文にして実行したいですね。
実はしれっとFAQにあります。
https://github.com/JetBrains/Exposed/wiki/FAQ#q-is-it-possible-to-use-native-sql--sql-as-a-string
import java.sql.ResultSet import org.jetbrains.exposed.sql.transactions.TransactionManager fun <T : Any> String.execAndMap(transform: (ResultSet) -> T): List<T> { val result = arrayListOf<T>() TransactionManager.current().exec(this) { rs -> while (rs.next()) { result += transform(rs) } } return result }
こんな感じでやれば、MySQLのクエリの結果をバイナリとして受け取ることができます。
・・・ java.sql.ResultSet
ってなんでしょ?
javaの昔からあるインターフェースのようで、JDBCから汎用的なSQL結果からカラム名ごとの値のゲッターを実装しているクラスのようです。 これはこれで便利なので特にラップせず使ってもいいと思います。
さてサンプルコードです。
class Question(f1: ByteArray, f2: String, f3: Date) { val question_id: UUID val name: String val created: Date init { question_id = UUID.nameUUIDFromBytes(f1) name = f2 created = f3 } } // 実際のコード fun findAll(): QuestionAggrList = transaction { QuestionList( "SELECT * FROM question".execAndMap { rs -> Question( rs.getBytes("id"), rs.getString("name"), rs.getDate("created"), ) } ) }
resultSetのゲッターは getDate(String!): Date!
のようにnullableを強制展開する戻り値を持っているので、本番で使う場合は runCatching()
などを使って安全に値を取り出すことをお勧めします。
UNION ALLやりたい
残念ながら201912月現在 Exposed
には UNION ALLに該当するDAL、DSLは存在しません。
さてUNION ALLですが、要は、 縦結合
です。
分析用途や正規化されてないテーブル同士を1つのテーブルとして使いたい時に重宝します。
さてどうするかというと prepare
と 生SQL実行
を両用する方法があります。
先ほどの Question
テーブルに TodoQuestion
テーブルと DoneQuestion
テーブルがあって、
これらを UNION ALLしたいとします。
QuestionList( "$done_question_sql UNION ALL $todo_question_sql ORDER BY created DESC " .execAndMap { rs -> Question( rs.getBytes("id"), rs.getString("name"), rs.getDate("created"), ) } )
なんかこんな感じで TodoQuestion
テーブルと DoneQuestion
テーブル を持ってくるSQL文を、
$todo_question_sql
と $done_question_sql
に代入すれば良さそうです。
しかし、
val done_question_sql =
done_question
.selectAll()
こんなのをやってしまうと、この時点でクエリは発火されてしまいます。
できればせめてこの DoneQuestion
テーブル のSQLだけ文字列で欲しいです。
そんな時は prepareSQL
メソッドをつけてあげると解決します。
val done_question_sql = done_question .selectAll() .prepareSQL(QueryBuilder(false))
こうすることで、 発火されず、そのままクエリを文字列として持ってくることができます。 これで、
val done_question_sql = done_question .selectAll() .prepareSQL(QueryBuilder(false)) val todo_question_sql = todo_question .selectAll() .prepareSQL(QueryBuilder(false)) val result = QuestionList( "$done_question_sql UNION ALL $todo_question_sql ORDER BY created DESC " .execAndMap { rs -> Question( rs.getBytes("id"), rs.getString("name"), rs.getDate("created"), ) } )
なるべくkotlinでコード書きながら、Exposedにないsyntaxで書くことができました。
最後に
実際 他にJava製のORMがある中、pure kotlinのチャレンジングなORMであるExposedですが、 そのふんkotlinの柔軟なsyntaxとcoroutineにより、従来にないORM体験をすることできます。
ただ、まだまだ他の jooq
や Hibernate
などのORMに比べると些か実装が足らない部分もあります。
そんなところは思い切ってPRを出すか、生SQLなどを組み合わせてできるんだということを忘れなければ、
十分使っていけるORMだなと思っております。
kotlinのコードにReturn Resultを組み込む
はじめに
本記事は Kotlin Advent Calendar 2019の1日目の記事です。 今回は前回当該ブログでも紹介しました、https://github.com/michaelbull/kotlin-resultについて事例をつけてご紹介させていただければと思います。
michaelbull/kotlin-resultの概要
このgithubのイントロダクションが全てなのですが、
The Result monad has two subtypes, Ok
representing success and containing a value, and Err , representing failure and containing an error. Scott Wlaschin's article on Railway Oriented Programming is a great introduction to the benefits of modelling operations using the Result type.
Mappings are available on the wiki to assist those with experience using the Result type in other languages:
書いてある通りで、kotlinに実装されてないResultモナドをライブラリによって実装し、安全かつ強力に「結果」を取り扱おうというライブラリです。
Result
は特に Rust
Scala
Haskell
といった言語を扱った人がいれば、その強力さたるや実感できると思います。
ちなみ「どう強力なのか?」をKotlin抜きで俯瞰して見たい方は以下のサイトにある動画を見ると非常にわかりやすいです。
一応、kotlinにも Result
Class は存在するのですが、これは例外を処理するためのResultで戻り値の型として利用できません。
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/index.html
このため、今回紹介する michaelbull/kotlin-result
は便宜上、 Return Result
として題名には書かせていただきました。
どんなものなの
fun returnResult(): Result<String, Error> = TODO() fun basicResult() = returnResult() .mapBoth( success = { str -> println(str) }, failure = { err -> println(err) } )
上記に書いたちょっぴり抽象的な概念とは思えぬ、非常にシンプルな動きをします。
つまり、 Result
というのは ジェネリクスClass
ってだけで、Ok
と Err
の2種類の data class
を返すだけです。
この記事ではこのライブラリが特に強力な「エラーハンドルのスマート化」に注目し、
結果を返す動作
を michaelbull/kotlin-result
で記述する事で以下の事が達成できます。
- 結果(OkかErr)によって処理をスマートに変える
- nullableな結果に対するエラーハンドリングもスマートに
- エラーに共通処理を実装してコードを簡素化に
- 例えthorwableな結果も安全に受け取ってエラーハンドルできる
さて、それぞれのユースケースをコードと共に見ていきましょう。
結果(OkかErr)によって処理をスマートに変える
最大の特徴な訳ですが、例えばwebサーバ(今回はサーバにktorを使ったとします)で Result
クラスに生えている mapBoth()
を使って、
処理が成功したら 200 OK
、失敗したら500 Internal Server Error
を返す例です.
(おそらく本当に作る場合は EntityList, Error
は何かレスポンス用にJSONとかに変換したりの処理が入るでしょうが、一旦略します。 )
fun dbAction(): Result<EntityList, Error> = // DBの処理 fun Route.exampleController() { route("/example") { get { dbAction() .mapBoth( success = { entityList -> call.respond(HttpStatusCode.OK, EntityList) }, failure = { error -> call.respond(HttpStatusCode.InternalServerError, error.toString()) } ) } } }
このように、非常にシンプルで可読性の高く、安全なコードを書くことができます。
mapBoth()
は、
inline fun <V, E, U> Result<V, E>.mapBoth(success: (V) -> U, failure: (E) -> U): U { return when (this) { is Ok -> success(value) is Err -> failure(error) } }
のようなインライン関数で、レシーバの Result
が Ok
か Err
かどちらかの data class
かで処理を分けます。
見ていただければわかりますが、 dbActionが成功したら何をするか、失敗したら何をするかが1発でわかりますね。
これにより、例えば 成功する動作の場合にentityListがnullかどうか
や 失敗する動作の方で謝ってentityListを参照してしまう
みたいなことが起きなくなります(nullable型だらけで全部チェックしなきゃ、みたいな事も無くなります)。
また、この Result
はfunctional programmingの必殺奥義 flatmap
を使うことが可能です。
fun createEntity(value: String): Result<Entity, Error> = entityRepository.insert(value) .flatMap { id -> // entityRepository.insert(value)が戻り値 `Ok` なら次に進む bindEntityRepository.create(id) .flatMap { // bindEntityRepository.create(id)が戻り値 `Ok` なら次に進む entityRepository.findById(id) .flatMap { dao -> // entityRepository.findById(id)が戻り値 `Ok` なら次に進む Ok(Entity( dao.id, dao.name, dao.created, )) } } }
上記のように、いくつかの処理があった時、 成功した時だけ次の処理に言って、失敗したらearlyReturnして失敗値を返したい
というニーズを満たすことができ、いわゆる コールバック地獄
を回避してスマートに処理を書くことができます。
余談ですが、おそらく michaelbull/kotlin-result
の作者さんはRustが好きらしく、
inline infix fun <V, E, U> Result<V, E>.flatMap(transform: (V) -> Result<U, E>): Result<U, E> { return andThen(transform) } inline infix fun <V, E, U> Result<V, E>.andThen(transform: (V) -> Result<U, E>): Result<U, E> { return when (this) { is Ok -> transform(value) is Err -> this } }
Rustで使われる andThen
も flatMap
も同じ処理を行いますし、なんなら flatMap
は andThen
のコールバックみたいです 😆
nullableな結果に対するエラーハンドリングもスマートに
michaelbull/kotlin-result
には toResultOr
というResult
クラスがもちメソッドがあり、このメソッドは kotlinの特徴的実装である、 nullable
に対応します。
inline infix fun <V, E> V?.toResultOr(error: () -> E): Result<V, E> { return when (this) { null -> Err(error()) else -> Ok(this) } }
処理の見て通りで、nullならErrorを返すラムダを書いて、そうでなければそのまま Ok
クラスで包んで返す処理とします。
fun dbAction(): Entity? = // DBの処理 fun findById(key: Int): Result<Count, EnumError> = dbAction() .toResultOr { EnumError.NotFoundEntityFailure.withLog() } .flatMap { Ok(Entity) }
これにより、kotlinならではの nullableな戻り値も Result
型と表現でき、 1つ前の項目で言ったような flatmap
のコンビネータに kotlinのnullableを組むこむことができます。
エラーに共通処理を実装してコードを簡素化に
こちらはどちらかというとライブラリを応用したテクニックになります。
当然? Kotlin
はGoのような error
型のようなものは無いので、以下のように自分で作る必要があります。
なるべく typealiasより自分で data class
や enum class
を作った方が型の制約的な意味で安全です。
data class SimpleError( val reason: String, ) enum class EnumError { ArrayIndexOutOfBoundsFailure, ParseParamaterFailure, ParseRequestFailure, MismatchDataStoreFailure, CouldNotCreateEntityFailure, NotFoundEntityFailure; }
加えて、例えば このエラーが発生したらロギングしたい
通知したい
などのニーズがあった場合、
メソッドを生やしてしまいます。
僕がある現場で使ってるところは、以下のように生やして、スタックトレースを繋げて出すようにしています。
fun EnumError.withLog(reason: String = this.name): EnumError { var stackTrace = "" Thread.currentThread().stackTrace.forEach { stackTrace += it } return this.also { KotlinLogging.logger {}.error("$reason - $stackTrace") } }
これが何かいいというと、アプリ全体のコードを表現した際、必ず例外処理に michaelbull/kotlin-result
で結果を表現するときにエラーを定義することになります。なので、エラーの定義部分で忘れずにロギングすることができるというわけです。
fun findById(key: Int): Result<Count, EnumError> = RedisContext.zscore(globalJedisPool.resource, CountListKey, key.toString()) .toResultOr { EnumError.NotFoundEntityFailure.withLog() } .flatMap { Ok(Count( CountRow( key.toString(), it ) )) }
もちろん、「え、絶対ロギングするし、そんな毎回 .withLog()
したくないんだが」って場合は、
Err
と表現するクラスのコンストラクタ処理のところにロギングの処理を書いていただければいいと思います。
例えthorwableな結果も安全に受け取ってエラーハンドルできる
さて最後、普通にkotlinを書いていても、AWSなどのJava SDKを利用したときに、どうしても 例外(JavaのException)
が帰ってくる可能性があります。せっかくここまで細かく例外処理を 型
として処理できるようにしたのに、こいつが混ざると台無しです。
もちろん、こいつにも対応できます、でもこれは kotlin標準のResult
で、ですが(笑)
fun createSubList(rowList: List<Int>): Result<List<Int>, EnumError> = runCatching { rowList.subList(0, 10) }.fold( onSuccess = { Ok(it) }, onFailure = { Err(EnumError.ArrayIndexOutOfBoundsFailure.withLog(it.toString())) } )
まず Listクラスについているような、 subList
モジュールは、正しく無い引数を入れると境界線例外を起こすことはご存知かと思います。
kotlinの場合、runCatching
.fold
を使うことによって、例外をおこしたときに安全に例外を取り出すことができます。
public inline fun <T, R> T.runCatching(block: T.() -> R): Result<R> { return try { Result.success(block()) } catch (e: Throwable) { Result.failure(e) } }
public inline fun <R, T> Result<T>.fold( onSuccess: (value: T) -> R, onFailure: (exception: Throwable) -> R ): R { contract { callsInPlace(onSuccess, InvocationKind.AT_MOST_ONCE) callsInPlace(onFailure, InvocationKind.AT_MOST_ONCE) } return when (val exception = exceptionOrNull()) { null -> onSuccess(value as T) else -> onFailure(exception) } }
この上記の Result
はkotlin謹製の Result
なので注意を・・・。
これらを使えば、あとは例外だったとき、ではなかったときに分けて Ok
Err
それぞれの data class
に包めば例外も型の世界で処理していくことができます。
おわりに
僕の運営している勉強会の方で使用しているアプリで、全面的に michaelbull/kotlin-result
を使用しているので、
「実際webアプリで使った場合どうなるんだろう?」と思った方はぜひご覧になってみてください。
【Kotlin】kotlinでmichaelbull/kotlin-resultを使うススメ
関数型プログラミングによくあるResultって便利だよね
例えばRustでは標準で搭載されているFunctionalなエラーハンドリングの表現の一つで、
fn hoge() -> Result<String, Error> { match huga() { 0 -> Ok("OK".to_string()) _ -> Err(Error("error occured")) } }
みたいな感じにやると、受け取る側は例えば返り値をnullチェックなどをするまでもなく、 String
か Error
が帰ってきた方を仕分けすれば良くなる。
他にも flatmap
Either
とかイケてるメソッドと組み合わせればコードが綺麗にまとまるとかもあるが後で後述する。
Rustの仕組みとしては、
pub enum Result<T, E> { /// Contains the success value #[stable(feature = "rust1", since = "1.0.0")] Ok(#[stable(feature = "rust1", since = "1.0.0")] T), /// Contains the error value #[stable(feature = "rust1", since = "1.0.0")] Err(#[stable(feature = "rust1", since = "1.0.0")] E), }
一方Kotlinでは...
一応実装はあるんですが、
inline class Result<out T> : Serializable
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/index.html
A discriminated union that encapsulates successful outcome with a value of type T or a failure with an arbitrary Throwable exception.
うーん、KotlinのResultはどっちかというと、例外がスローされるかもしれない処理を runCatching
関数でラップして、明示的に処理を分けましょうみたいな感じで、いわゆる 戻り値Result
ではない感じです。
runCatching {
call.receive<AnswerRequest>() //ここが例外投げられるかもな処理
}
.onSuccess { call.respond(HttpStatusCode.OK) }
.onFailure { call.respond(HttpStatusCode.BadRequest, Error) }
誤解なきよう、これはこれで便利な訳です
それなら使おうライブラリ
今回はこちらです
このライブラリを使うことでkotlinコード全体を一気にモナモナさせていくことが可能です。。。
実装は以下
/** * [Result] is a type that represents either success ([Ok]) or failure ([Err]). * * - Elm: [Result](http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Result) * - Haskell: [Data.Either](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Either.html) * - Rust: [Result](https://doc.rust-lang.org/std/result/enum.Result.html) */ sealed class Result<out V, out E> { companion object { /** * Invokes a [function] and wraps it in a [Result], returning an [Err] * if an [Exception] was thrown, otherwise [Ok]. */ inline fun <V> of(function: () -> V): Result<V, Exception> { return try { Ok(function.invoke()) } catch (ex: Exception) { Err(ex) } } } } /** * Represents a successful [Result], containing a [value]. */ data class Ok<out V>(val value: V) : Result<V, Nothing>() /** * Represents a failed [Result], containing an [error]. */ data class Err<out E>(val error: E) : Result<Nothing, E>()
面白いところですが、 kotlinの enum class
を使っていません。kotlinのenum classはジェネリクスみたいにタイプパラメータを持てなかったり、他のクラスみたいに
パラメータでコンストラクタする訳ではないとか理由はあるでしょう。
enum class Result<V, E>(ok: V, err: E) {} // こんなのとかできない enum class Color(val rgb: Int) { RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF) } // あくまで最初から中身は決まっていて、rgbパラメータで初期化するとかの意味ではない。
このmichaelbull/kotlin-resultのライブラリは、単純にenumではなくclassとdata classの組み合わせですが、とても上手く動きます。
使いっぷり
fun returnResult(): Result<String, Error> = TODO() fun test() = returnResult() .mapBoth( success = { str -> println(str) }, failure = { err -> println(err) } )
michaelbull/kotlin-resultのResult型には様々なメソッドがついており、例えば mapBoth
を使えばResultを受け取って、
Ok()
Err()
data classに入った値を安全に処理を分岐させることができます。 mapBoth
はそのなの通り、Ok()をもらっても、Err()をもらっても同じ戻り値を返す時に便利で、例えば今回の場合は Unit
を返してるので使っているだけです。例えばここから、エラーの方を別のエラーで返したい場合は mapError
mapEither
などもあります。これらはRustやScalaなど使っている人はおなじみだと思います。それがKotlinでも使えるとなるとテンション上がりますね😄
もちろん関数型の人たち御用達な flatmap()
もあります(Rust使い用に andThen
もありますが、絶対この作者Rust好きだから入れただろ...)
fun returnResult1(): Result<ArgA, Error> = TODO() fun returnResult2(a: ArgA): Result<ArgB, Error> = TODO() fun returnResult3(b: ArgB): Result<ArgC, Error> = TODO() fun test(): Result<ArgC, Error> = returnResult1() .flatMap { a -> // a type is ArgA returnResult2(a) } .flatMap { b ->// b type is ArgB returnResult3(b) }
こんな感じで、共通なErrorの型を持ってResultで返してくれる関数をひとまとめにした時に、flatMapでエラーが発生した時に止めてくれて、 Ok()が帰ってくる限り処理を続けるみたいなことができます。
kotlinに嬉しいnullableからの変換
michaelbull/kotlin-resultには toResultOr
というメソッドがあり、kotlinの代表的な型表現である ?
をResultに変換できます。
fun returnResult1(): ArgA? = TODO() fun returnResult2(a: ArgA): Result<ArgB, Error> = TODO() fun test(): Result<ArgB, Error> = returnResult1() .toResultOr{ Error("ERROR") } .flatMap { a -> // a type is ArgA returnResult2(a) }
このように、例えばnullableな返り値の関数も、nullの場合をエラーとみなしてメソッドチェーンを繋げることが可能です。 そんな感じで、とてもいいぞmichaelbull/kotlin-resultという宣伝記事でした。
【Vue.js】【Nuxt.js】cookie-universal-nuxtで複数の値をcookieのキーとする。
cookie-universal-nuxt?
nuxtでcookieを使うライブラリ
cookieを入れるときはこんな感じ
export default { created() { this.$cookies.set('isVisited', true, { path: '/', maxAge: 60 * 60 * 24 * 7 }) } }
もらうときは
data () { return { isVisitedFromCache: this.$cookies.get('isVisited') } }
キーに2値入れたい時もある
タプルとか配列をキーには実はできない
export default { created() { this.$cookies.set(['isVisitedCount', 10], true, { path: '/', maxAge: 60 * 60 * 24 * 7 }) } }
cookie-universal-nuxt
は上手く動いてくれない。
こうする
export default { created() { this.$cookies.set({key: 'isVisitedCount', value: 10}, true, { path: '/', maxAge: 60 * 60 * 24 * 7 }) } }
無名オブジェクトなんてものがES6にはありましたね。
これでもらえる。
data () { return { isVisitedFromCache: this.$cookies.get({key: 'isVisitedCount', value: 10}) } }
これで解決
【Scala】関数型初心者のカリーの勉強
カリー
この事らしい。
def curry[A,B,C](f: (A, B) => C): A => (B => C) = (a: A) => (b => f(a, b)) // curry: [A, B, C](f: (A, B) => C)A => (B => C)
異なる型パラメータA,B,Cに対してA,Bを引数に取り戻り値がCとなるような関数をAを引数にして戻り値をBを引数にしてCを戻り値にする関数
ってことね 😓
具体化
先ほどの関数curryに対し、代入できる関数を用意する。
def sum(a: Int, b: Short): Long = a + b // sum: (a: Int, b: Short)Long
このexpression成立するんだscala。
んで、
val c = curry(sum) // c: Int => (Short => Long) = $$Lambda$1131/1160112616@443a53df
これがカリー化らしい。 つまりaとbからcを産む関数から、aからbを引数にして戻りにcにする関数に変換したみたいな話。
不思議な感覚がしますね。
val c_sum = c(10) // c_sum: Short => Long = $$Lambda$1132/92962244@61b60600
10を引数にして、c_sum関数を引数にした変数に10を足してLongで返します。
scalaは func()()
みたいに高階関数を書けるのでワンライナーで関数をほどくと、
val cc_sum = c(10)(100) // cc_sum: Long = 110
うん、確かに sum(10, 100)
と c(10)(100)
は同じ結果になったようだ。
アンカリー
多分カリーの美しいとされる所は、関数レイヤー操作を単純な関数で表現しつつも、それが副作用なし[要出典]で可逆な所なんだろうと思う。
つまりuncurry(curry(sum))
で sum
に戻せる関数を定義できる。
def uncurry[A,B,C](f: A => (B => C)): (A, B) => C = (a, b) => f(a)(b) // uncurry: [A, B, C](f: A => (B => C))(A, B) => C
val ex_sum = uncurry(curry(sum)) // ex_sum: (Int, Short) => Long = $$Lambda$1141/570434649@75ac326f
val result = ex_sum(10, 100) // result: Long = 110
書いてみるとすっと分かるね。
感想
要は関数のレイヤーを一段あげさげするってことなんだろうな。 なんとなくわかった。