tokuhirom's Blog

JFR Event Streaming を利用して、ByteBuffer で OutOfMemoryError が起きたときに harakiri する。

import jdk.jfr.consumer.RecordedClass
import jdk.jfr.consumer.RecordedEvent
import jdk.jfr.consumer.RecordingStream
import java.nio.ByteBuffer
import java.time.Duration
import kotlin.system.exitProcess


fun main(args: Array<String>) {
    // OOME を発行させるスレッド
    Thread {
        Thread.sleep(1000)

        val bbList = mutableListOf<ByteBuffer>()
        while (true) {
            try {
                val bb = ByteBuffer.allocateDirect(Int.MAX_VALUE)
                bbList.add(bb)
            } catch (e: Error) {
                println("Caught $e")
            }
        }
    }.start()

    // OOME があったら harakiri する。
    RecordingStream().use { rs ->
        rs.enable("jdk.JavaExceptionThrow").withPeriod(Duration.ofSeconds(1))
        rs.onEvent("jdk.JavaExceptionThrow") { event: RecordedEvent ->
            val message = event.getString("message")
            val thrownClass = event.getValue<RecordedClass>("thrownClass")
            if (thrownClass.name == "java.lang.OutOfMemoryError" && message.startsWith("Cannot reserve")) {
                println("Caught OutOfMemoryError! $event")
                exitProcess(4)
            }
        }
        println("Starting RecordingStream")
        rs.start()
    }
}

Java 17 以後は Clock の interface として InstantSource が定義されている

Clock を mockito で @Spy しようとした場合などに、最近では制限が厳しくていじりづらくなっているが、、

Java 17 からは Clock の interface が切り出されて、InstantSource という名前になっている。Java 17 以後の場合は、実装コード内では InstantSource を利用するのが基本となっていくだろう。

https://bugs.openjdk.java.net/browse/JDK-8266846

JFR の event stream を Fluency で fluentd に送る

ここまでできれば、あとは fluentd で storage に格納して、flamegraph 等を描画すれば良いだけである。

import jdk.jfr.consumer.RecordedEvent
import jdk.jfr.consumer.RecordingStream
import org.komamitsu.fluency.Fluency
import org.slf4j.LoggerFactory
import java.time.Duration
import java.time.ZoneId

class JfmonClient(
    private val fluency: Fluency,
    private val tag: String,
    private val siteId: String,
    private val instanceId: String
) : AutoCloseable {
    private var rs: RecordingStream = RecordingStream()
    private val logger = LoggerFactory.getLogger(JfmonClient::class.java)

    fun start() {
        this.rs.start()
    }

    fun startAsync() {
        this.rs.startAsync()
    }

    override fun close() {
        logger.info("Closing jfmon-client")
        this.rs.close()

        this.fluency.flush()
        this.fluency.close()
    }

    fun flush() {
        logger.info("Flushing jfmon-client")
        this.fluency.flush()
    }

    /**
     * jdk.SocketRead: [jdk.jfr.events.SocketReadEvent]
     */
    fun enable(name: String, period: Duration?, threshold: Duration? = null, stackTrace: Boolean? = null) {
        logger.info("Enabling {}(period={}, threshold={}, stackTrace={})", name, period, threshold, stackTrace)

        val settings = rs.enable(name)
        if (period != null) {
            settings.withPeriod(period)
        }
        if (threshold != null) {
            settings.withThreshold(threshold)
        }
        if (stackTrace != null) {
            if (stackTrace) {
                settings.withStackTrace()
            } else {
                settings.withoutStackTrace()
            }
        }
        rs.onEvent(name, this::emitEvent)
    }

    private fun emitEvent(event: RecordedEvent) {
        val data = buildMap(event)
        if (logger.isDebugEnabled) {
            logger.debug(
                "{}: {} {}",
                event.eventType.name,
                event.startTime.atZone(ZoneId.of("Asia/Tokyo")),
                data
            )
        }
        fluency.emit(tag, data)
    }

    private fun buildMap(event: RecordedEvent): Map<String, Any?> {
        return mapOf(
            "siteId" to siteId,
            "instanceId" to instanceId,
            "type" to event.eventType.name,
            "data" to buildData(event)
        )
    }

    private fun buildData(event: RecordedEvent): Map<String, Any?> {
        return event.fields.associate { dv ->
            val value: Any? = when (dv.typeName) {
                "boolean", "long", "int", "java.lang.String" -> event.getValue<Any>(dv.name)
                "java.lang.Thread" -> event.getThread(dv.name).javaName
                "jdk.types.StackTrace" -> {
                    val stackTrace = event.stackTrace
                    stackTrace?.frames?.map { frame ->
                        // 型が違うものを一つの配列に入れると、Elasticsearch が怒る。
                        listOf(
                            frame.method?.type?.name,
                            frame.method?.name?.toString(),
                            frame.lineNumber.toString(),
                        )
                    }
                }
                else -> null
            }
            dv.name to value
        }
    }
}

jfr で取れる生データがどんなものか確認できるウェブアプリを作った

https://github.com/tokuhirom/jfrdemo

JFR のイベントってどんなんあるんかなーと思ってもドキュメントとかが見当たらないし、確認の方法がよくわからないわけだが、、 実データを見るのが一番良さそうなのだが、実データ見るのがめんどくさいので、ある程度データを見れるように整理したというわけ。

Mac で synergy がうごかないとき

Linux を server, Mac を client として synergy を使おうとした。

[2019-03-13T22:16:56] DEBUG: can't get the active group, use the first group instead

とかなんとか言われて、うまく動かない場合。

MacOS 側に "U.S." キーボードレイアウトがないと日本語入力できないらしい!これはわからん!!

Settings の "Keyboard" -> "Input Sources" から U.S. を追加すればOK。Google IME だけ、とかにしてると使えないのだった。

hirose31 san におしえてもらって解決しました!!!

ref. https://members.symless.com/forums/topic/6176-keyboard-does-not-work-clientmacos-mojave-serverubuntu-1804/

SparkSQL のクエリをユニットテストしたい

品質向上のために Spark クエリのユニットテストを実施したいという場合、JVM 言語で開発している場合には、Spark/hive をライブラリとしてロードできるから、容易に実装することができる。

dependencies {
    implementation 'org.apache.spark:spark-core_2.12:3.0.0'
    implementation 'org.apache.spark:spark-sql_2.12:3.0.0'
}

のように、関連するモジュールを依存に追加する。

以下のような、テストに利用するデータを json 形式などで用意する(spark は CSV, TSV などの形式も利用可能だから、好きなものを使えばよい)

{"name": "Nick",	"age":35,	"extra_fields": "{\"interests\":[\"car\", \"golf\"]}"}
{"name": "John",	"age":23}
{"name":"Cathy",	"age":44,	"extra_fields":"{\"interests\":[\"cooking\"]}"}

あとは、実際に spark session を作成し、local モードで spark を起動させれば良い。

import org.apache.spark.sql.Dataset
import org.apache.spark.sql.Row
import org.apache.spark.sql.SparkSession

// test without
class SimpleTest {
    fun run() {
        val spark: SparkSession = SparkSession
            .builder()
            .appName("Java Spark SQL basic example") // your application name
            .config("spark.master", "local")  // run on local machine, single thread.
            .config("spark.ui.enabled", false)
            .getOrCreate()

        val resourcePath = javaClass.classLoader.getResource("test-data/people.json")!!.toString()
        println("++++ read csv from: $resourcePath")

        val df = spark.read()
            .json(resourcePath)
        df.show()
        df.printSchema()

        println("++++ create table")
        df.createTempView("people")

        println("++++ select")
        val sqlDF: Dataset<Row> = spark.sql("SELECT * FROM people")
        sqlDF.show(false)

        println("++++ select 2nd")
        val sqlDF2: Dataset<Row> = spark.sql("SELECT name, get_json_object(extra_fields, '$.interests') interests FROM people")
        sqlDF2.show()

        println("++++ select 3rd")
        val sqlDF3: Dataset<Row> = spark.sql("SELECT avg(age) avg_age FROM people")
        sqlDF3.show()
    }
}

fun main() {
    SimpleTest().run()
}

hive を使う場合

実際に動かすクエリが select * from your_db_name.your_table のようにDB 名を指定していて、そのクエリ自体を変えずにテストしたいという場合には、hive サポートを有効にする必要がある。

hive を使う場合、spark-hive を依存に追加する。

dependencies {
    implementation 'org.apache.spark:spark-core_2.12:3.0.0'
    implementation 'org.apache.spark:spark-sql_2.12:3.0.0'
    implementation 'org.apache.spark:spark-hive_2.12:3.0.0'
}

あとは以下のように DB を作って入れるだけ。

import org.apache.spark.sql.Dataset
import org.apache.spark.sql.Row
import org.apache.spark.sql.SaveMode
import org.apache.spark.sql.SparkSession

class TestClass {
    fun run() {
        val warehouseLocation = createTempDir()
        println("++++ warehouseLocation=$warehouseLocation")
        val spark: SparkSession = SparkSession
            .builder()
            .appName("Java Spark SQL basic example") // your application name
            .config("spark.master", "local")  // run on local machine, single thread.
            .config("spark.sql.warehouse.dir", warehouseLocation.toString())
            .config("spark.ui.enabled", false)
            .enableHiveSupport()
            .getOrCreate()

        val resourcePath = javaClass.classLoader.getResource("test-data/people.json")!!.toString()
        println("++++ read csv from: $resourcePath")

        val df = spark.read()
            .json(resourcePath)
        df.show()
        df.printSchema()

        println("++++ create table")
        spark.sql("create database if not exists foo")
        df.write().mode(SaveMode.Overwrite).saveAsTable("foo.people")
        spark.sql("show tables").show()
        spark.sql("show create table foo.people").show(false)

        // If the type of data is the important thing, you need to write your schema by yourself.
        //        spark.sql("""drop table if exists `foo`.`people`""")
//        spark.sql("""
//        CREATE TABLE `foo`.`people` (
//            `name` STRING,
//            `age` long,
//            `extra_fields` STRING)
//        USING parquet""".trimIndent())
//        df.write().insertInto("foo.people")


        println("++++ select")
        val sqlDF: Dataset<Row> = spark.sql("SELECT * FROM foo.people")
        sqlDF.show(false)

        println("++++ select 2nd")
        val sqlDF2: Dataset<Row> = spark.sql("SELECT name, get_json_object(extra_fields, '$.interests') interests FROM foo.people")
        sqlDF2.show()

        println("++++ select 3rd")
        val sqlDF3: Dataset<Row> = spark.sql("SELECT avg(age) avg_age FROM foo.people")
        sqlDF3.show()
    }
}

fun main() {
    TestClass().run()
}

クエリを変更しなくていいというメリットがある一方で、hive にアクセスするので依存も増えるし、実行もめちゃくちゃ遅くなります。

df.write().mode(SaveMode.Overwrite).saveAsTable("foo.people")

のようにすると、df 側の型をみていい感じにテーブル定義してくれて便利だが、明示的に create table したいときは以下のようにしたほうがいいかも。

        spark.sql("""drop table if exists `foo`.`people`""")
        spark.sql("""
        CREATE TABLE `foo`.`people` (
            `name` STRING,
            `age` long,
            `extra_fields` STRING)
        USING parquet""".trimIndent())
        df.write().insertInto("foo.people")

両者の比較

hive を利用しない場合、上記コードは 4.427 sec 程度で終わりますが、hive を利用する場合は 19.676 sec 程度かかるようになります。 プロダクションコードのテストをする場合はこの差はそこそこでかいかも。

sample code

https://github.com/tokuhirom/sparksql-unittest

curl で silence したいけどエラーはみたい。

       -s, --silent
              Silent or quiet mode. Don't show progress meter or error messages.  Makes Curl mute.

で silence できるが、これを入れると、error message も抑制されてしまう。

       -S, --show-error
              When used with -s it makes curl show an error message if it fails.

-S を追加で入れると、エラーは stderr に出るようになるのでちょうどいい感じになる。

Gradle の dependency locking について

昔の gradle には dependency locking 機能がなかった。ビルドするタイミングによって、別の依存モジュールが利用されたりしていた。。 最近、gradle に dependency locking 機能がついたので試してみた。 carton.lock とか package-lock.json とか、そういうのと同じようなことができるようになる。 同じレポジトリからビルドしたら同じ jar が生成されるようになる。便利。

dependency locking を利用すると gradle.lockfile というファイルが生成される。

デフォルトだとフェーズ単位でファイルが生成されるから enableFeaturePreview('ONE_LOCKFILE_PER_PROJECT') を settings.gradle に書いて1ファイルにまとめるようにしたほうが良い。gradle 7.0 ではこの方式がデフォルトになる予定なので、最初からこの feature flag は enabled にしたほうが良いです。管理上も、そのほうが便利。

たぶんもう普通に使えるけど、まだ開発途中って感じはする。./gradlew dependencies してもサブプロジェクトのぶんを一括で作れない、とか。。

↓実際に line-bot-sdk-java を利用して試しに生成してみたやつがこれ。 https://github.com/tokuhirom/line-bot-sdk-java/commit/08a53ed86eedcf1072e7c12e77d7e1777f54c933

mutable なクラスがどのぐらい DI 対象になってるか調べたい

BeanPostProcessor で探せる。

import org.springframework.beans.factory.config.BeanPostProcessor
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Component
import java.lang.reflect.Modifier

@Component
@Profile("!real")
class MyProcessor : BeanPostProcessor {
    override fun postProcessAfterInitialization(bean: Any, beanName: String): Any? {
        val fields = bean.javaClass.declaredFields
        val packageName = bean.javaClass.packageName
        if ((!packageName.startsWith("org.springframework"))
            && fields.filter { !Modifier.isFinal(it.modifiers) }.count() > 0) {
            println("$bean has mutable field.")
        }
        return bean;
    }
}

[spring] @Configuration と @Component は違う、あるいは @Bean lite mode について

Spring の @Configuration は @Component の stereotype だと思っていたが、挙動が違うとのこと。

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Bean.html

@Bean Lite Mode というものがあって、@Component を利用すると CGLIB proxy を利用した aspect 系の処理が動かないらしい。

https://github.com/spring-projects/spring-framework/issues/14061

歴史を振り返ると、spring 3 の頃にバグ報告されたから lite mode ということにしたようにもみえる(深追いしてない)。

(全く知らなくてまつださんに教えてもらった)


【20191123 追記】

Spring framework 5.2 以後では、@Configuration(proxyBeanMethods=false) と書けるようになっているから、これを利用するのが良さそう(意図が明確なので)。

`ClassLoader#getResources("")` の返り値が Java9 以後は jar:file: も含むようになっている

package com.example;

public class Example {
    public static void main(String[] args) throws java.io.IOException {
        System.err.println("JVM: " + java.lang.management.ManagementFactory.getRuntimeMXBean().getVmVersion());

        ClassLoader classLoader = Example.class.getClassLoader();
        System.out.println(classLoader);

        java.util.Enumeration<java.net.URL> resources = classLoader.getResources("");
        while (resources.hasMoreElements()) {
            System.out.println("-- " + resources.nextElement());
        }
    }
}

このようなプログラムの実行結果が、Java 9 以後では異なる。

Run with Java 8

JVM: 25.201-b09
sun.misc.Launcher$AppClassLoader@2a139a55
-- file:/Users/tokuhirom/work/urlclassloader-behavoiour/build/classes/java/main/

https://docs.oracle.com/javase/8/docs/api/java/lang/ClassLoader.html#getResources-java.lang.String-

Run with Java 11

JVM: 11.0.1+13-LTS
jdk.internal.loader.ClassLoaders$AppClassLoader@799f7e29
-- file:/Users/tokuhirom/work/urlclassloader-behavoiour/build/classes/java/main/
-- jar:file:/Users/tokuhirom/.gradle/caches/modules-2/files-2.1/net.bytebuddy/byte-buddy-agent/1.10.2/fbfe9bf099287c35b8336ea9da194f301a112a11/byte-buddy-agent-1.10.2.jar!/META-INF/versions/9/

https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/ClassLoader.html#getResources(java.lang.String)

こんな感じ。

Jackson 2.10 についてのメモ

ざっくりいうと


Jackson 2.10 features - @cowtowncoder - Medium を参考のこと。

Jackson 2.10 は、Jackson 3 に向けての Migration 用のリリースになっている。2.9までのインターフェースも @Deprecated 状態で残しつつ、3以後のあたらしいインターフェースも実装されている。今のうちに新しいインターフェースに移行しておくと後々楽になる、と思う。 (Spring Boot 2.2 は Jackson 2.10 に依存している)

2.10 では以下の3つのメジャーなイシューを解決している。

  1. デシリアライズ時におけるセキュリティイシューの根本解決
  2. 3.0 における新しいインターフェースへの移行促進
  3. module-info.class 関連

で、1 はまあいいとして、2 が重要だなと思っている。新しいインターフェースがどういうものかというと以下のような感じ。

の2点が新しいインターフェースの特徴。mutable な deserializer とか、最近は流行らないからね。

Jackson 3.0 以後では ObjectMapper は configuration 系のメソッドは持たない。

Spring Framework 5.2 からは WebClient の retrieve でちゃんと http status を取れる

Spring Framework 5.2 がリリースされた。

Support for Kotlin Coroutines.

も大きいのだが、、個人的には以下に注目したい。

Refinements to WebClient API to make the retrieve() method useful for most common cases, specifically adding the ability to retrieve status and headers and addition to the body. The exchange() method is only for genuinely advanced cases, and when using it, applications can now rely on ClientResponse#createException to simplify selective handling of exceptions.

これまで、Webclient を利用した場合、.retreive().bodyToMono(String.class) などとして response body のみを取得するメソッドしかなく、異常に使いづらかった。 HTTP Status Code が 2xx 以外の場合には例外が上がる設計になっているのはいいのだが、現実的にはどの HTTP Status Code かは例外ではなく通常の処理としてハンドリングしたいというケースも多いのである。

Spring Framework 5.2 以後では以下のように記述可能になった。

        WebClient client = WebClient.create();
        Mono<ResponseEntity<String>> responseEntityMono = client.get()
                                                                .uri(url)
                                                                .retrieve()
                                                                .toEntity(String.class);

        ResponseEntity<String> responseEntity = responseEntityMono.block();
        assert responseEntity != null;
        log.info("url={} status={} headers={} body={}",
                 url,
                 responseEntity.getStatusCodeValue(),
                 responseEntity.getHeaders(),
                 responseEntity.getBody());

便利。

sparksql で emoji/pictogram を含む行を検出する

select text from table where dt='20190911' and text rlike '[\\uD800-\\uDFFF]'

とかでとりあえず良さそう。

scala で guava のバージョンを確認する

とりあえずこんな感じで。try-with-resources でやりたかったが、あんま短くかけなそうだった。

%spark
import java.util.Properties
import com.google.common.io.Resources

val resourceName = "META-INF/maven/com.google.guava/guava/pom.properties";
val properties = new Properties();
val inputStream = Resources.getResource(resourceName).openStream();
properties.load(inputStream);
println(properties.getProperty("version"))
inputStream.close()

spring の 4.3.15, 5.0.5 以後で mockmvc の json path のマッチングで matcher 使わなくてもよくなってる

mockmvc で jsonpath のマッチングを利用する場合、以下のようにする必要があった。

.andExpect(jsonPath("$.id", is(5963)))

これはこれでいいのだが、他の API と一貫性がなく、補完も効きづらく、hamcrest API ではなく assertj をメインで使ってる身としては辛かった。

また long の扱いが渋くて、辛かった。。 https://stackoverflow.com/questions/37996419/how-to-force-andexpectjsonpath-to-return-long-long-instead-int-for-int-num

SPR-16587 MockMvcResultMatchers.jsonPath(String).value() should have a matching method to declare the expected type で、問題が解決されて以下のような書き方ができるようになっている。

.andExpect(jsonPath("$.id").value(5963)))

MockMvc でテストを書いているときに部分一致をしたいケースは殆どないと思われるので、このスタイルで書くのが良いと私は思います。

Spring 4.3.15, 5.0.5 以後で使えるので、このスタイルで書いたほうが良い。

古いコードをコピペし続けているとこのような古い書き方に引きづられがちなので気をつけていきたい。

【追記】 null であることを保証したいときは hamcrest matcher でやらないと駄目。

gRPC-WEB がもたらす我々の生活への変化

gRPC-WEB が GA となった。これが我々の生活にどのような変化をもたらすのかについて考える。

従来の gRPC の課題

gRPC は google が公開している RPC 方式であり、java, golang などの言語で利用可能になっている。 gRPC は protocol buffers over HTTP/2 を基本としているため、通信が multiplexing されるし、schema 定義がきっちりされるのでクライアント側とのコミュニケーションがしやすい。

一方、protocol buffers はbinary であるためにbinaryの取扱が苦手な Browser JavaScript からのアクセスが難しいという問題があった。

grpc-gateway

grpc-gateway という実装があって、これを利用すれば Browser JavaScript からのアクセスも可能ではある。しかし、専用の gateway server を golang で生成して運用することになって煩雑である。

また、client library を protobuf 定義ファイルから生成することができないので、そういった意味では、gRPC の魅力を 100% 引き出すことができていないといえるかもしれない。 もちろん、Swagger の定義を生成することができるので、そこから codegen することはできる。

外部に向けて JSON API を提供する必要がある場合には grpc-gateway は引き続き有力な選択肢となるが、SPA web application のためのエンドポイントの場合には、grpc-web の方が今後は良い選択となると私は考える

また、grpc-gateway はコミュニティ実装であるから、公式ではない。

gRPC-WEB

gRPC-WEB のプロトコルは PROTOCOL-WEB.md で解説されている。

多くのブラウザで動作するように、base64で encode する方法などが protocol で設定されている。

gRPC-WEB の実装

現在のところ nginx module と envoy というプロキシサーバーによる実装、grpcwebproxy という go 実装が提供されている。

nginx module

https://github.com/grpc/grpc-web/tree/master/net/grpc/gateway/nginx

nginx module も提供されている。

grpcweb

https://github.com/improbable-eng/grpc-web/tree/master/go/grpcweb

grpcweb は go のライブラリ実装。既存の golang で書かれた gRPC サーバー実装の中に組み込んで、 gRPC-web で export する機能を追加することができる。 便利。

grpcwebproxy

https://github.com/improbable-eng/grpc-web/tree/master/go/grpcwebproxy

golang で実装されている grpc-web の実装。go で single binary で導入できるので、一番導入が簡単そう。

envoy

Envoy は C++ で書かれた proxy server です。拡張が容易になっていて、C++ で簡単に拡張できるようになっています。

Envoy の grpc-web の実装は envoy の repository の中にあります。C++ ですがコメントがたくさんあるのでわかりやすいですね。

Envoy の中に実装されているのは、Proxy layer でカバーできていればすべてのサーバー実装で使えるので、まずは proxy layer で実装したということのようだ。

Envoy は C++ で書かれているのでビルドがそこそこ面倒なので docker での運用が現実的と思われる。プリビルドバイナリも提供されているが、ubuntu と alpine のみなので centos 勢としては悲しい。

今後の grpc-web の roadmap

https://github.com/grpc/grpc-web/blob/master/ROADMAP.md

このドキュメントで今後の ROADMAP が述べられている。 いくつか気になったところを紹介する。

非バイナリフォーマット

Binary の protobuf を base64 などにエンコーディングする方式は CPU overhead/memory overhead が大きい。よって、gmail などで採用されている、text protocol だが高速に処理できるフォーマットを使うようにすれば良いのではないか、とのこと。

Local Proxies

各言語用のサーバーライブラリの側に、gRPC-WEB サポートを追加しようというプラン。 今は go だけあるっぽい。Java support が待ち遠しい。

TypeScript support

これはほしい。Protobuf から生成されたクライアントに型情報が付けば、IDE 上での作業が快適になることは間違いない。

Web UI Support

gRPC-WEB を実際に試せる web console を作りたい、とのこと。

長期的な展望

whatwg streams api が各ブラウザに実装されて普及すれば、native の gRPC protocol を利用可能になるのではなるとのこと。

MySQL の X Protocol/X DevAPI 周りについて調査したのをまとめたののメモ

MySQL 8 以後では X Protocol がサポートされている(5.7 系では部分的なサポートであり、X Protocol を本格的に利用する場合には 8 を利用することが推奨されているようだ) 通信は従来の MySQL Protocol と異なり、Protocol Buffers Based となっていて、各言語のドライバの実装が簡単になっている(protocol buffers がその言語でサポートされていれば、だが) これにより今後 libmysqlclient に依存せずに各言語のドライバが実装されるようになって運用管理が簡単になるんじゃないかと私は考えています。 実際に、mysql-connector-nodejsは X Protocol のみをサポートしていて、libmysqlclient への依存がありません。

また、X Protocol/X DevAPI は async を前提に設計されているため、各言語の Connector ではその言語の特性を生かして CompletableFuture/Promise などを利用して実装されています。

X DevAPI というのものがあって、これは MySQL Shell と MySQL Connectors で実装されている API。どの言語を利用していても統一的に MySQL を扱えるプログラミング言語レベルの API になっている。

X Protocol への接続について

X Protocol は mysql 8 ではデフォルトで有効になっている模様。有効かどうかは show plugins などして mysqlx plugin が有効かどうかを確認すれば良い。 port も 3306 ではなく 33060 なので注意。

Node 実装について

Node の MySQL connector は promise based になっており使いやすい

NODE_DEBUG=protobuf という環境変数を設定すれば、protobuf の serialization のログが見れて便利。

Java 実装について

Java 実装も割と普通に実行できる。今まで通りの MySQL Connector/J の実装で実行可能。 生で使うなら X DevAPI は JDBC API の 2億倍使いやすい。

package com.example;

import com.mysql.cj.xdevapi.Session;
import com.mysql.cj.xdevapi.SessionFactory;
import com.mysql.cj.xdevapi.SqlResult;

import java.util.List;
import java.util.stream.Collectors;

public class App {
    public static void main(String[] args) {
        SessionFactory sessionFactory = new SessionFactory();
        Session session = sessionFactory.getSession("mysqlx://[email protected]:33060/test");
        runQuery(session, "SHOW PROCESSLIST");
        runQuery(session, "SELECT SLEEP(15)");
        runQuery(session, "SHOW PROCESSLIST");
        session.close();
    }

    private static void runQuery(Session session, String query) {
        System.out.println(" クエリ開始" + query);
        session.sql(query).executeAsync()
                .thenAccept(rows -> {
                    System.out.println(" クエリ完了 " + query);
                    dumpRows(rows);
                });
    }

    private static void dumpRows(SqlResult rows) {
        List<String> columnNames = rows.getColumnNames();
        System.out.println("\n\n結果結果結果結果結果結果結果結果結果結果結果");
        System.out.println("    " + columnNames.stream().collect(Collectors.joining("\t")));
        System.out.println(rows.fetchAll()
                .stream()
                .map(row -> columnNames.stream()
                        .map(row::getString)
                        .collect(Collectors.joining("\t")))
                .map(line -> "    " + line)
                .collect(Collectors.joining("\n")));
        System.out.println("\n\n終了終了終了終了終了終了終了終了終了終了終了");
    }
}

のような実装では、以下のような結果を得るだろう。

 クエリ開始SHOW PROCESSLIST
 クエリ完了 SHOW PROCESSLIST


結果結果結果結果結果結果結果結果結果結果結果
    Id    User    Host    db    Command    Time    State    Info
    4    event_scheduler    localhost    null    Daemon    342510    Waiting on empty queue    null
    25    root    172.17.0.1:36154    null    Sleep    26244        null
    102    root    172.17.0.1:39884    test    Sleep    1599    null    PLUGIN
    204    root    172.17.0.1:40090    test    Query    0    null    PLUGIN: SHOW PROCESSLIST


終了終了終了終了終了終了終了終了終了終了終了
 クエリ開始SELECT SLEEP(15)
 クエリ開始SHOW PROCESSLIST
 クエリ完了 SELECT SLEEP(15)


結果結果結果結果結果結果結果結果結果結果結果
    SLEEP(15)
    0


終了終了終了終了終了終了終了終了終了終了終了
 クエリ完了 SHOW PROCESSLIST


結果結果結果結果結果結果結果結果結果結果結果
    Id    User    Host    db    Command    Time    State    Info
    4    event_scheduler    localhost    null    Daemon    342525    Waiting on empty queue    null
    25    root    172.17.0.1:36154    null    Sleep    26259        null
    102    root    172.17.0.1:39884    test    Sleep    1614    null    PLUGIN
    204    root    172.17.0.1:40090    test    Query    0    null    PLUGIN: SHOW PROCESSLIST


終了終了終了終了終了終了終了終了終了終了終了

com.mysql.cj.protocol.x.AsyncMessageSender#writeAsync"[SEND] ===> " + message.getMessage().getClass().getSimpleName() + "\n" + message.getMessage().toString()というデバッグログを設置して、com.mysql.cj.protocol.x.ResultMessageListener#createFromMessage"[RECEIVE] <== " + message.getMessage().getClass().getName() + "\n" + message.getMessage().toString() というデバッグログを設置すると、通信の様子を垣間見ることができる。

上記のコードの場合の出力は以下のようになる。

[SEND] ===> CapabilitiesGet

[SEND] ===> CapabilitiesSet
capabilities {
  capabilities {
    name: "tls"
    value {
      type: SCALAR
      scalar {
        type: V_BOOL
        v_bool: true
      }
    }
  }
}

[SEND] ===> AuthenticateStart
mech_name: "PLAIN"
auth_data: "test\000root\000"

[SEND] ===> StmtExecute
stmt: "select @@mysqlx_max_allowed_packet"

 クエリ開始SHOW PROCESSLIST
[SEND] ===> StmtExecute
stmt: "SHOW PROCESSLIST"

 クエリ開始SELECT SLEEP(15)
[SEND] ===> StmtExecute
stmt: "SELECT SLEEP(15)"

 クエリ開始SHOW PROCESSLIST
[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$ColumnMetaData
type: SINT
name: "Id"
original_name: ""
table: ""
original_table: ""
schema: ""
catalog: "def"
length: 21
flags: 16

[SEND] ===> StmtExecute
stmt: "SHOW PROCESSLIST"

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$ColumnMetaData
type: BYTES
name: "User"
original_name: ""
table: ""
original_table: ""
schema: ""
catalog: "def"
collation: 33
length: 96
flags: 16

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$ColumnMetaData
type: BYTES
name: "Host"
original_name: ""
table: ""
original_table: ""
schema: ""
catalog: "def"
collation: 33
length: 192
flags: 16

[SEND] ===> Close

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$ColumnMetaData
type: BYTES
name: "db"
original_name: ""
table: ""
original_table: ""
schema: ""
catalog: "def"
collation: 33
length: 192

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$ColumnMetaData
type: BYTES
name: "Command"
original_name: ""
table: ""
original_table: ""
schema: ""
catalog: "def"
collation: 33
length: 48
flags: 16

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$ColumnMetaData
type: SINT
name: "Time"
original_name: ""
table: ""
original_table: ""
schema: ""
catalog: "def"
length: 7
flags: 16

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$ColumnMetaData
type: BYTES
name: "State"
original_name: ""
table: ""
original_table: ""
schema: ""
catalog: "def"
collation: 33
length: 90

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$ColumnMetaData
type: BYTES
name: "Info"
original_name: ""
table: ""
original_table: ""
schema: ""
catalog: "def"
collation: 33
length: 300

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$Row
field: "\b"
field: "event_scheduler\000"
field: "localhost\000"
field: ""
field: "Daemon\000"
field: "\216\353)"
field: "Waiting on empty queue\000"
field: ""

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$Row
field: "2"
field: "root\000"
field: "172.17.0.1:36154\000"
field: ""
field: "Sleep\000"
field: "\272\235\003"
field: "\000"
field: ""

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$Row
field: "\314\001"
field: "root\000"
field: "172.17.0.1:39884\000"
field: "test\000"
field: "Sleep\000"
field: "\260\034"
field: ""
field: "PLUGIN\000"

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$Row
field: "\234\003"
field: "root\000"
field: "172.17.0.1:40094\000"
field: "test\000"
field: "Query\000"
field: "\000"
field: ""
field: "PLUGIN: SHOW PROCESSLIST\000"

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$FetchDone

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxNotice$Frame
type: 3
scope: LOCAL
payload: "\b\004\022\004\b\002\030\000"

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxSql$StmtExecuteOk

 クエリ完了 SHOW PROCESSLIST


結果結果結果結果結果結果結果結果結果結果結果
    Id    User    Host    db    Command    Time    State    Info
    4    event_scheduler    localhost    null    Daemon    342727    Waiting on empty queue    null
    25    root    172.17.0.1:36154    null    Sleep    26461        null
    102    root    172.17.0.1:39884    test    Sleep    1816    null    PLUGIN
    206    root    172.17.0.1:40094    test    Query    0    null    PLUGIN: SHOW PROCESSLIST


終了終了終了終了終了終了終了終了終了終了終了
[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$ColumnMetaData
type: SINT
name: "SLEEP(15)"
original_name: ""
table: ""
original_table: ""
schema: ""
catalog: "def"
length: 21
flags: 16

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$Row
field: "\000"

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$FetchDone

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxNotice$Frame
type: 3
scope: LOCAL
payload: "\b\004\022\004\b\002\030\000"

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxSql$StmtExecuteOk

 クエリ完了 SELECT SLEEP(15)


結果結果結果結果結果結果結果結果結果結果結果
    SLEEP(15)
    0


終了終了終了終了終了終了終了終了終了終了終了
[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$ColumnMetaData
type: SINT
name: "Id"
original_name: ""
table: ""
original_table: ""
schema: ""
catalog: "def"
length: 21
flags: 16

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$ColumnMetaData
type: BYTES
name: "User"
original_name: ""
table: ""
original_table: ""
schema: ""
catalog: "def"
collation: 33
length: 96
flags: 16

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$ColumnMetaData
type: BYTES
name: "Host"
original_name: ""
table: ""
original_table: ""
schema: ""
catalog: "def"
collation: 33
length: 192
flags: 16

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$ColumnMetaData
type: BYTES
name: "db"
original_name: ""
table: ""
original_table: ""
schema: ""
catalog: "def"
collation: 33
length: 192

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$ColumnMetaData
type: BYTES
name: "Command"
original_name: ""
table: ""
original_table: ""
schema: ""
catalog: "def"
collation: 33
length: 48
flags: 16

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$ColumnMetaData
type: SINT
name: "Time"
original_name: ""
table: ""
original_table: ""
schema: ""
catalog: "def"
length: 7
flags: 16

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$ColumnMetaData
type: BYTES
name: "State"
original_name: ""
table: ""
original_table: ""
schema: ""
catalog: "def"
collation: 33
length: 90

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$ColumnMetaData
type: BYTES
name: "Info"
original_name: ""
table: ""
original_table: ""
schema: ""
catalog: "def"
collation: 33
length: 300

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$Row
field: "\b"
field: "event_scheduler\000"
field: "localhost\000"
field: ""
field: "Daemon\000"
field: "\254\353)"
field: "Waiting on empty queue\000"
field: ""

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$Row
field: "2"
field: "root\000"
field: "172.17.0.1:36154\000"
field: ""
field: "Sleep\000"
field: "\330\235\003"
field: "\000"
field: ""

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$Row
field: "\314\001"
field: "root\000"
field: "172.17.0.1:39884\000"
field: "test\000"
field: "Sleep\000"
field: "\316\034"
field: ""
field: "PLUGIN\000"

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$Row
field: "\234\003"
field: "root\000"
field: "172.17.0.1:40094\000"
field: "test\000"
field: "Query\000"
field: "\000"
field: ""
field: "PLUGIN: SHOW PROCESSLIST\000"

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxResultset$FetchDone

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxNotice$Frame
type: 3
scope: LOCAL
payload: "\b\004\022\004\b\002\030\000"

[RECEIVE] <== com.mysql.cj.x.protobuf.MysqlxSql$StmtExecuteOk

 クエリ完了 SHOW PROCESSLIST


結果結果結果結果結果結果結果結果結果結果結果
    Id    User    Host    db    Command    Time    State    Info
    4    event_scheduler    localhost    null    Daemon    342742    Waiting on empty queue    null
    25    root    172.17.0.1:36154    null    Sleep    26476        null
    102    root    172.17.0.1:39884    test    Sleep    1831    null    PLUGIN
    206    root    172.17.0.1:40094    test    Query    0    null    PLUGIN: SHOW PROCESSLIST


終了終了終了終了終了終了終了終了終了終了終了

現在の Java connector/mysqld の実装では、select sleep(15) などのクエリが発行された場合、その後のクエリの結果が先に帰ってくることはない。これは実際問題、session が状態を持つ以上、そうならざるを得ない。このため、他の状態を持たないプロトコルのクライアントと同じ気分で使っているとハマるかも。 そして、transaction は session に紐づく が、 1session あたり 1 TCP connection 以上 という実装に現時点ではなっている(以上、というのは slave への自動送信などを x devapi 上で 1 セッションとして扱う可能性があるため)。 (通信を多重化することも可能だったと思うが、現在の実装はそうなっていない。なんでだろうか。MySQL Server の実装上の制約?)

Nginx caches DNS records forever in proxy_redirect directive

If a domain name resolves to several addresses, all of them will be used in a round-robin fashion. In addition, an address can be specified as a server group.

The parameter value can contain variables. In this case, if an address is specified as a domain name, the name is searched among the described server groups, and, if not found, is determined using a resolver. <<<

http://d.hatena.ne.jp/hirose31/20131112/1384251646

github pages で underscore を含むディレクトリが表示されないとき

具体的には amon.64p.org を github pages に移したのだが、その際にハマった。 sphinx はデフォルトで _static/ 以下に静的ファイルを生成するからだ。

https://help.github.com/articles/files-that-start-with-an-underscore-are-missing/ に解決策が載ってるよ、と tmaesaka さんに教えてもらった。

.nojekyll 置いて解決。