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()
}
}
Clock を mockito で @Spy しようとした場合などに、最近では制限が厳しくていじりづらくなっているが、、
Java 17 からは Clock の interface が切り出されて、InstantSource という名前になっている。Java 17 以後の場合は、実装コード内では InstantSource を利用するのが基本となっていくだろう。
https://bugs.openjdk.java.net/browse/JDK-8266846
ここまでできれば、あとは 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
}
}
}
https://github.com/tokuhirom/jfrdemo
JFR のイベントってどんなんあるんかなーと思ってもドキュメントとかが見当たらないし、確認の方法がよくわからないわけだが、、
実データを見るのが一番良さそうなのだが、実データ見るのがめんどくさいので、ある程度データを見れるように整理したというわけ。
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/
品質向上のために 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
-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 機能がついたので試してみた。
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 --write-locks
ってするとロックファイルが書かれる
./gradlew classes --update-locks org.apache.commons:commons-lang3,org.slf4j:slf4j-api
とかで特定のモジュールだけアップデートできる
たぶんもう普通に使えるけど、まだ開発途中って感じはする。./gradlew dependencies
してもサブプロジェクトのぶんを一括で作れない、とか。。
↓実際に line-bot-sdk-java を利用して試しに生成してみたやつがこれ。
https://github.com/tokuhirom/line-bot-sdk-java/commit/08a53ed86eedcf1072e7c12e77d7e1777f54c933
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 の 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)
と書けるようになっているから、これを利用するのが良さそう(意図が明確なので)。
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 3 のインターフェースを含みつつ 2 系の旧インターフェースを新インターフェースを @Deprecated
扱いで含んだリリース
Spring Boot 2.2 は Jackson 2.10 に依存している
Boot 2.2 にした段階で、Deprecation Warnings でまくるからシュッと Jackson 3 にしたほうが良さそう。
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つのメジャーなイシューを解決している。
デシリアライズ時におけるセキュリティイシューの根本解決
3.0 における新しいインターフェースへの移行促進
module-info.class 関連
で、1 はまあいいとして、2 が重要だなと思っている。新しいインターフェースがどういうものかというと以下のような感じ。
Builder Pattern ベースになっている
特定のフォーマット固有のオプションとそうじゃないものの分離
の2点が新しいインターフェースの特徴。mutable な deserializer とか、最近は流行らないからね。
Jackson 3.0 以後では ObjectMapper は configuration 系のメソッドは持たない。
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());
便利。
select text from table where dt='20190911' and text rlike '[\\uD800-\\uDFFF]'
とかでとりあえず良さそう。
とりあえずこんな感じで。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()
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 が 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 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 の実装上の制約?)
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
具体的には amon.64p.org を github pages に移したのだが、その際にハマった。
sphinx はデフォルトで _static/ 以下に静的ファイルを生成するからだ。
https://help.github.com/articles/files-that-start-with-an-underscore-are-missing/ に解決策が載ってるよ、と tmaesaka さんに教えてもらった。
.nojekyll 置いて解決。