nがひとつ多い。

えぬなおの技術的なことを書いていくとこ。

【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
}

https://github.com/kubernetes/kubernetes/blob/1514bb2141d3c82830f64aa0e1f8c3650116b803/cmd/kube-controller-manager/app/batch.go#L45

うん、、、どうやら予想通り 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)
    }

https://github.com/kubernetes/kubernetes/blob/1514bb2141/pkg/controller/cronjob/controller.go#L268-L281

うんーどうやらここっぽいねえ。その後の処理を見ても、どうやらこの 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
}

https://github.com/kubernetes/kubernetes/blob/1514bb2141d3c82830f64aa0e1f8c3650116b803/pkg/controller/cronjob/utils.go#L89-L149

コメントアウトから

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"

https://github.com/kubernetes/kubernetes/blob/1514bb2141d3c82830f64aa0e1f8c3650116b803/pkg/controller/cronjob/utils.go#L23

   sched, err := cron.ParseStandard(sj.Spec.Schedule)

https://github.com/kubernetes/kubernetes/blob/1514bb2141d3c82830f64aa0e1f8c3650116b803/pkg/controller/cronjob/utils.go#L95

おおー!これが答えみたいなもんです・・・!なんと、予想の ③第三者のなんかのライブラリで間接的に使っている でした・・・結構意外でしょう?取れ高ですね。 さて、最後にこの関数をみていきましょう

github.com

(めっちゃスターついてますやん)

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 ですが、これは、

  • The "standard" cron format, described on the Cron wikipedia page and used by the cron Linux system utility.
  • The cron format used by the Quartz Scheduler, commonly used for scheduled jobs in Java software

以下の二つのページを参照してくれよと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ファイルを作りたくないですし、 javaclassPath指定とかめっちゃ面倒です。

そんな時どうすればいいのか、代表的な三つの方法を書いていきます。

  • 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を使う。

出ました外部ライブラリ。

github.com

どうやら 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は使い勝手がよく、AndroidJavaの資産を両方に使える器用なやつです。 サーバサイドとしてはバッチ処理なども任せることも今後も多くなってくるでしょう。 そんな時に上記の方法が頭に残っていると便利かもしれません。

【kotlin】kotlin製ORM Exposed小技集

はじめに

本記事は Kotlin Advent Calendar 2019の8日目の記事です。 Jetbrain社製のkotlinで書かれたORMについて話していきます。

github.com

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のクエリも揃っていますが、 jooqHibernate に比べるとsyntaxにまだまだ不揃いなだったり、情報がたらない時があります。
そんな漢字の、僕が初めて使ったときに困ったところと、それを切り抜ける技を紹介していきます。

ただ、困ったらまずは以下のgithubwikiをみましょう。

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 ってなんでしょ?

docs.oracle.com

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ですが、要は、 縦結合 です。

sql55.com

分析用途や正規化されてないテーブル同士を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体験をすることできます。

ただ、まだまだ他の jooqHibernate などのORMに比べると些か実装が足らない部分もあります。 そんなところは思い切ってPRを出すか、生SQLなどを組み合わせてできるんだということを忘れなければ、 十分使っていけるORMだなと思っております。

kotlinのコードにReturn Resultを組み込む

はじめに

本記事は Kotlin Advent Calendar 2019の1日目の記事です。 今回は前回当該ブログでも紹介しました、https://github.com/michaelbull/kotlin-resultについて事例をつけてご紹介させていただければと思います。

michaelbull/kotlin-resultの概要

github.com

この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抜きで俯瞰して見たい方は以下のサイトにある動画を見ると非常にわかりやすいです。

fsharpforfunandprofit.com

一応、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 ってだけで、OkErr の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)
    }
}

のようなインライン関数で、レシーバの ResultOkErr かどちらかの 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で使われる andThenflatMap も同じ処理を行いますし、なんなら flatMapandThen のコールバックみたいです 😆

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 classenum 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アプリで使った場合どうなるんだろう?」と思った方はぜひご覧になってみてください。

github.com

【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チェックなどをするまでもなく、 StringError が帰ってきた方を仕分けすれば良くなる。 他にも 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),
}

要はジェネリクスenumだって話です。

一方Kotlinでは...

一応実装はあるんですが、

qiita.com

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) }

誤解なきよう、これはこれで便利な訳です

それなら使おうライブラリ

今回はこちらです

github.com

このライブラリを使うことで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を使うライブラリ

www.npmjs.com

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で返します。 scalafunc()() みたいに高階関数を書けるのでワンライナーで関数をほどくと、

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

書いてみるとすっと分かるね。

感想

要は関数のレイヤーを一段あげさげするってことなんだろうな。 なんとなくわかった。