Hatena::ブログ(Diary)

CLOVER

2017-12-15

HazelcastのClusterSafe/ClusterState、バージョン間の互換性を確認する

先日書かれた、こちらの記事を見まして。

クラスタ化された Payara Server のアップグレード - notepad

ちょっと気になったのは、こちらの部分。

Case 1 : Shoal を使用していない場合
Payara は GlassFish から引き継いだ Shoal を使用しなくても、Hazelcast のみでクラスタリングが可能です。その場合はおそらく、すべてのノードが DAS となっているはずで、通常のアップグレードをすべてのノードに対して実施すればよいことになります。ノードの接続・切断は Hazelcast が自動的に行うため考慮する必要はありません。

http://www.coppermine.jp/docs/notepad/2017/12/how-to-update-clustered-payara-server.html

確かにそのとおりだと思うのですが、個人的にはもうちょっと気になるところがあります。

特に、このあたり。

  • Hazelcast Nodeを接続・切断してよいタイミング
  • Hazelcast自体のバージョン間の互換性

というわけで、この視点でHazelcast側を見ていこうかなぁと思います。

ClusterSafeとClusterStatus

Hazelcastは分散KVS的な側面を持っていますが、Nodeの追加、削除が起こるとデータのリバランシングが発生します。

特にNodeが失われると一時的にデータが一部なくなりますが、他のNodeから復元することが可能です。ただ、デフォルトではデータのバックアップ数は
1なので、2つのNodeが同時にダウンするとデータが欠損する可能性があります。

例えばPayaraの場合は

  • Distributed Map
  • Cache
  • Topic

あたりのデータ構造を使用していますが、MapとCacheについてはこの理屈が当てはまります。
※Topicについては分散配置ができないので、2つのNodeが同時にダウンするとすべてのデータを失う可能性があります

そんなわけなので、リバランスが終わって安全な状態になったかどうか、確認したりしたいですよね?

このあたりを見るのに、ClusterSafeがあります。

ClusterSafeについては、こちら。

Safety Checking Cluster Members

PartitionServiceが持つisClusterSafeメソッドでは、Hazelcastクラスタが安全な状態にあるかどうかをチェックします。データのリバランスがなく
(Partitionの移行が発生していない)、すべてのバックアップが各Partitionで同期されている場合にtrueを返します。

isMemberSafeやisLocalMemberは、指定したMember、もしくは自分自身が持つPartitionがプライマリーとの同期が済んでいればtrueを返します。
trueを返す場合は、該当のMemberをシャットダウンすることができます。

使用するAPIとしては、こちら。
PartitionService (Hazelcast Root 3.9.1 API)
PartitionServiceMBean (Hazelcast Root 3.9.1 API)

続いて、クラスタの状態を確認するものとしてClusterStateがあります。
Managing Cluster and Member States

ClusterStateでは、クラスタ操作の許可/制限の状態を確認することができます。例えば、ClusterStatusがACTIVEであれば特に制限はありませんが、FROZEMや
PASSIVEの場合はPartitionが固定され、新規Memberを受け付けなくなったりします。

ClusterStateに関するAPIは、こちら。
Cluster (Hazelcast Root 3.9.1 API)
ClusterState (Hazelcast Root 3.9.1 API)

また、API以外にも、JMXやHealth Check(REST API)でも確認することができます。

Monitoring with JMX
Health Check and Monitoring

JMXは「hazelcast.jmx」を「true」に、Health Checkは「hazelcast.http.healthcheck.enabled」を「true」にすることで、それぞれ有効化することができます。
システムプロパティか、Hazelcastの設定ファイル/Configのproperties/propertyで設定します。

Hazelcastのバージョン間の互換性は?

HazelcastのRolling Upgrade(クラスタ内のMemberを順次アップグレードしていくこと)について、ドキュメントに以下の記載があります。

Rolling Member Upgrades

ところでこれ、Enterprise版のHazelcastの話です。

ここで、まずHazelcastのバージョンの定義を見てみます。

・Minor version: A version change after the decimal point, e.g., 3.8 and 3.9.
・Patch version: A version change after the second decimal point, e.g., 3.8.1 and 3.8.2.
・Member codebase version: The major.minor.patch version of the Hazelcast binary on which the member executes. For example, when running on hazelcast-3.8.jar, your member's codebase version is 3.8.0.
・Cluster version: The major.minor version at which the cluster operates. This ensures that cluster members are able to communicate using the same cluster protocol and determines the feature set exposed by the cluster.

http://docs.hazelcast.org/docs/3.9.1/manual/html-single/index.html#terminology

Member codebase versionが「major.minor.patch」バージョンを、Cluster versionが「major.minor」バージョンを指すみたいですね。

で、ここを読むとHazelcastはCluster version間で互換性がないことがわかります。

Hazelcast Members Compatibility Guarantees

Enterprise版のHazelcast 3.8以降であれば、ひとつ前のMinor Versionからの互換性があるようです(Hazelcast IMDG Enterprise 3.8から3.9へはRolling Upgradeが可能)。
ポイントかと思いますので、押さえておきましょう。

試してみる

と、ドキュメントベースに説明だけだとなんなので、試してみましょう。

簡単にGroovyスクリプトで、次のことを試してみたいと思います。

  • JMXおよびHealth Checkの有効化
  • クラスタ内にMemberを増減させた際の、クラスタの状態確認
  • バージョン間の互換性確認

用意したスクリプトは、こんな感じ。
hazelcast391-runner.groovy

@Grab('com.hazelcast:hazelcast:3.9.1')
import com.hazelcast.core.Hazelcast

import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.stream.IntStream
import java.time.LocalDateTime

System.setProperty('hazelcast.jmx', 'true')
System.setProperty('hazelcast.http.healthcheck.enabled', 'true')

def log = { message ->
  println("[${LocalDateTime.now()}] ${message}")
}

log('starting Hazelcast Instance...')
def hazelcast = Hazelcast.newHazelcastInstance()

def running = new AtomicBoolean(true)

new Thread({ -> 
  while (running.get()) {
    def clusterState = hazelcast.cluster.clusterState
    def partitionService = hazelcast.partitionService

    log("Cluster#clusterState = ${clusterState}")
    log("PartitionService#clusterSafe = ${partitionService.clusterSafe}")

    // Reflection!!
    log("InternalPartitionServicem#migrationQueueSize = ${hazelcast.original.node.partitionService.migrationQueueSize}")

    TimeUnit.SECONDS.sleep(1L)
  }
}).start()

log('Hazelcast Instance started.')

System.console().readLine('> Enter input data')

def map = hazelcast.getMap('default')
IntStream.rangeClosed(1, 100000).forEach { i -> map.put("${hazelcast.cluster.localMember.uuid}-key${i}", "value${i}") }

log("putted data, size = ${map.size()}")

System.console().readLine('> Enter stop Hazelcast Instance')
running.set(false)

hazelcast.shutdown()

log('Hazelcast Instance shutdown.')

この構成を基本にして、あとでGrapeで指定したバージョンを変えたスクリプトを作成していきます。

@Grab('com.hazelcast:hazelcast:3.9.1')
import com.hazelcast.core.Hazelcast

JMXとHealth Checkは、今回はシステムプロパティで有効化しました。

System.setProperty('hazelcast.jmx', 'true')
System.setProperty('hazelcast.http.healthcheck.enabled', 'true')

ところどころEnterを押すと進んでいくのですが、途中でClusterStateやClusterSafeなどを観測したり、データを登録したりする処理を付けたり
しています。

JMX & Health Check

とりあえず、この状態で起動してみましょう。

$ groovy hazelcast391-runner.groovy

JMXの方は、起動時に有効化されていることがログ出力されます。

12 15, 2017 11:41:58 午後 com.hazelcast.internal.jmx.ManagementService
情報: [172.20.0.1]:5701 [dev] [3.9.1] Hazelcast JMX agent enabled.

MBeanの情報を、JConsoleで見てみます。
f:id:Kazuhira:20171215235206p:image

PartitionServiceMBeanのAttributeを見ると、ClusterSafeや自分自身の状態を確認できます。

続いて、Health Checkを確認してみましょう。Health Checkは、「http://[IPアドレス]:[ポート]/hazelcast/health」で確認することができます。

$ curl http://localhost:5701/hazelcast/health
Hazelcast::NodeState=ACTIVE
Hazelcast::ClusterState=ACTIVE
Hazelcast::ClusterSafe=TRUE
Hazelcast::MigrationQueueSize=0
Hazelcast::ClusterSize=1

表示できましたね。

その他、cluster.shで確認する方法やREST APIで確認する方法もあるようですが、今回は割愛。
Using the Script cluster.sh

Using REST API for Cluster Management

Enterprise版だと、Management Centerがもっと豪華に…。
Clustered REST via Management Center

とりあえず、JMXとHealth Checkはこんなところで。

クラスタ内のMemberを増減させてみる

用意したスクリプトには、クラスタの状態を出力し続ける処理と

new Thread({ -> 
  while (running.get()) {
    def clusterState = hazelcast.cluster.clusterState
    def partitionService = hazelcast.partitionService

    log("Cluster#clusterState = ${clusterState}")
    log("PartitionService#clusterSafe = ${partitionService.clusterSafe}")

    // Reflection!!
    log("InternalPartitionServicem#migrationQueueSize = ${hazelcast.original.node.partitionService.migrationQueueSize}")

    TimeUnit.SECONDS.sleep(1L)
  }
}).start()

10万件ほどデータを放り込む処理を付けてあります。

def map = hazelcast.getMap('default')
IntStream.rangeClosed(1, 100000).forEach { i -> map.put("${hazelcast.cluster.localMember.uuid}-key${i}", "value${i}") }

log("putted data, size = ${map.size()}")

クラスタの状態確認は、それぞれClusterおよびPartitionServiceからClusterStateとClusterSafeを確認しています。

    def clusterState = hazelcast.cluster.clusterState
    def partitionService = hazelcast.partitionService

    log("Cluster#clusterState = ${clusterState}")
    log("PartitionService#clusterSafe = ${partitionService.clusterSafe}")

あと、ちょっと強引にアクセスしているのがMigrationQueueで、これでMemberの増減に伴うデータのマイグレーション(リバランシング)の状態を確認できます。

    // Reflection!!
    log("InternalPartitionServicem#migrationQueueSize = ${hazelcast.original.node.partitionService.migrationQueueSize}")

これはMember単位のサイズになります。

実は、先ほどのHealth Check時にも出力されています。

$ curl http://localhost:5701/hazelcast/health
Hazelcast::NodeState=ACTIVE
Hazelcast::ClusterState=ACTIVE
Hazelcast::ClusterSafe=TRUE
Hazelcast::MigrationQueueSize=0
Hazelcast::ClusterSize=1

これをちょっとズルして取得している感じです。

では、まずはひとつ起動。

$ groovy hazelcast391-runner.groovy

データを放り込みます。

[2017-12-16T00:03:51.329] putted data, size = 100000

ちなみに、この時はこんな感じでクラスタの状態が出力されます。

[2017-12-16T00:04:33.622] Cluster#clusterState = ACTIVE
[2017-12-16T00:04:33.623] PartitionService#clusterSafe = true
[2017-12-16T00:04:33.624] InternalPartitionServicem#migrationQueueSize = 0
[2017-12-16T00:04:34.624] Cluster#clusterState = ACTIVE
[2017-12-16T00:04:34.626] PartitionService#clusterSafe = true
[2017-12-16T00:04:34.626] InternalPartitionServicem#migrationQueueSize = 0
[2017-12-16T00:04:35.627] Cluster#clusterState = ACTIVE
[2017-12-16T00:04:35.629] PartitionService#clusterSafe = true
[2017-12-16T00:04:35.630] InternalPartitionServicem#migrationQueueSize = 0

ここで1 Nodeなので、次のNodeを起動してみましょう。

$ groovy hazelcast391-runner.groovy

クラスタにMemberが参加するとともに、ClusterSafeがfalseとなり、MigrationQueueのサイズも増えます。

12 16, 2017 12:05:20 午前 com.hazelcast.internal.cluster.ClusterService
情報: [172.20.0.1]:5701 [dev] [3.9.1] 

Members {size:2, ver:2} [
	Member [172.20.0.1]:5701 - 87b87aa5-4da8-40e9-924e-adb661d81b38 this
	Member [172.20.0.1]:5702 - c957669a-27c4-4483-917c-8d37afc54a6e
]

12 16, 2017 12:05:20 午前 com.hazelcast.internal.partition.impl.MigrationManager
情報: [172.20.0.1]:5701 [dev] [3.9.1] Re-partitioning cluster data... Migration queue size: 271
[2017-12-16T00:05:20.749] Cluster#clusterState = ACTIVE
[2017-12-16T00:05:20.753] PartitionService#clusterSafe = false
[2017-12-16T00:05:20.753] InternalPartitionServicem#migrationQueueSize = 253
[2017-12-16T00:05:21.754] Cluster#clusterState = ACTIVE
[2017-12-16T00:05:21.756] PartitionService#clusterSafe = false
[2017-12-16T00:05:21.757] InternalPartitionServicem#migrationQueueSize = 134
[2017-12-16T00:05:22.758] Cluster#clusterState = ACTIVE
[2017-12-16T00:05:23.061] PartitionService#clusterSafe = false
[2017-12-16T00:05:23.063] InternalPartitionServicem#migrationQueueSize = 34
[2017-12-16T00:05:24.064] Cluster#clusterState = ACTIVE
[2017-12-16T00:05:24.079] PartitionService#clusterSafe = true
[2017-12-16T00:05:24.080] InternalPartitionServicem#migrationQueueSize = 0
12 16, 2017 12:05:24 午前 com.hazelcast.internal.partition.impl.MigrationThread
情報: [172.20.0.1]:5701 [dev] [3.9.1] All migration tasks have been completed, queues are empty.

最後に、MigrationQueueのサイズが0になるとClusterSafeがtrueとなるようです。
※このあたりは、あとでもう少し

3つ目のNodeを足しても同じ。

12 16, 2017 12:07:32 午前 com.hazelcast.internal.cluster.ClusterService
情報: [172.20.0.1]:5701 [dev] [3.9.1] 

Members {size:3, ver:3} [
	Member [172.20.0.1]:5701 - 87b87aa5-4da8-40e9-924e-adb661d81b38 this
	Member [172.20.0.1]:5702 - c957669a-27c4-4483-917c-8d37afc54a6e
	Member [172.20.0.1]:5703 - 967fff3e-94e7-4b40-8c97-604bdb4a00ec
]

[2017-12-16T00:07:32.719] Cluster#clusterState = ACTIVE
[2017-12-16T00:07:32.722] PartitionService#clusterSafe = false
[2017-12-16T00:07:32.723] InternalPartitionServicem#migrationQueueSize = 1
12 16, 2017 12:07:32 午前 com.hazelcast.internal.partition.impl.MigrationManager
情報: [172.20.0.1]:5701 [dev] [3.9.1] Re-partitioning cluster data... Migration queue size: 271
[2017-12-16T00:07:33.724] Cluster#clusterState = ACTIVE
[2017-12-16T00:07:33.725] PartitionService#clusterSafe = false
[2017-12-16T00:07:33.726] InternalPartitionServicem#migrationQueueSize = 228
[2017-12-16T00:07:34.727] Cluster#clusterState = ACTIVE
[2017-12-16T00:07:34.992] PartitionService#clusterSafe = false
[2017-12-16T00:07:34.993] InternalPartitionServicem#migrationQueueSize = 101
[2017-12-16T00:07:35.995] Cluster#clusterState = ACTIVE
[2017-12-16T00:07:36.065] PartitionService#clusterSafe = true
[2017-12-16T00:07:36.066] InternalPartitionServicem#migrationQueueSize = 0
12 16, 2017 12:07:36 午前 com.hazelcast.internal.partition.impl.MigrationThread
情報: [172.20.0.1]:5701 [dev] [3.9.1] All migration tasks have been completed, queues are empty.

なお、念のためですが、データを追加したりしても別にデータのリバランシングが発生するとかいうわけではありませんので。

異なるバージョンのHazelcastでクラスタを構成してみる

最後に、異なるバージョンのHazelcastを使用してクラスタを構成してみましょう。

最初に、Hazelcast 3.9.1(現時点での最新)のNodeを起動してみます。

$ groovy hazelcast391-runner.groovy

ここで、Grapeでのバージョン指定だけ変えたスクリプトを用意してみます。
hazelcast39-runner.groovy

@Grab('com.hazelcast:hazelcast:3.9')
import com.hazelcast.core.Hazelcast

起動。

$ groovy hazelcast39-runner.groovy

無事、クラスタが構成されました。

12 16, 2017 12:12:37 午前 com.hazelcast.internal.cluster.ClusterService
情報: [172.20.0.1]:5701 [dev] [3.9.1] 

Members {size:2, ver:2} [
	Member [172.20.0.1]:5701 - 690a44e3-11b5-4728-93fb-2a1cbf76562a this
	Member [172.20.0.1]:5702 - 6233fe6c-f3e9-4b32-9efd-46a57a80f564
]


それでは、今度は3.9.1と3.8系の最新である3.8.8で試してみましょう。
hazelcast388-runner.groovy

@Grab('com.hazelcast:hazelcast:3.8.8')
import com.hazelcast.core.Hazelcast

起動。先に3.9.1の方を起動してみました。

## 3.9.1
$ groovy hazelcast391-runner.groovy

## 3.8.8
$ groovy hazelcast388-runner.groovy

すると、3.9.1側にはこういうログが出力され、クラスタに入ることを拒否されます。

12 16, 2017 12:15:09 午前 com.hazelcast.internal.cluster.impl.ClusterJoinManager
警告: [172.20.0.1]:5701 [dev] [3.9.1] Joining node's version 3.8.8 is not compatible with cluster version 3.9 (Rolling Member Upgrades are only supported for the next minor version) (Rolling Member Upgrades are only supported in Hazelcast Enterprise)
12 16, 2017 12:15:09 午前 com.hazelcast.nio.tcp.TcpIpConnection
情報: [172.20.0.1]:5701 [dev] [3.9.1] Connection[id=1, /172.20.0.1:5701->/192.168.254.128:34082, endpoint=[172.20.0.1]:5702, alive=false, type=MEMBER] closed. Reason: Connection closed by the other side


互換性ないよ、Rolling UpgradeしたかったらHazelcast Enterprise使ってね、って言われてますね。

警告: [172.20.0.1]:5701 [dev] [3.9.1] Joining node's version 3.8.8 is not compatible with cluster version 3.9 (Rolling Member Upgrades are only supported for the next minor version) (Rolling Member Upgrades are only supported in Hazelcast Enterprise)

なお、3.8.8の方は起動に失敗してシャットダウンしてしまいます。

12 16, 2017 12:15:09 午前 com.hazelcast.security
重大: [172.20.0.1]:5702 [dev] [3.8.8] Node could not join cluster. Before join check failed node is going to shutdown now!
12 16, 2017 12:15:09 午前 com.hazelcast.security
重大: [172.20.0.1]:5702 [dev] [3.8.8] Reason of failure for node join : Joining node's version 3.8.8 is not compatible with cluster version 3.9 (Rolling Member Upgrades are only supported for the next minor version) (Rolling Member Upgrades are only supported in Hazelcast Enterprise)
12 16, 2017 12:15:09 午前 com.hazelcast.instance.Node
警告: [172.20.0.1]:5702 [dev] [3.8.8] Terminating forcefully...
12 16, 2017 12:15:09 午前 com.hazelcast.instance.Node
情報: [172.20.0.1]:5702 [dev] [3.8.8] Shutting down multicast service...
12 16, 2017 12:15:09 午前 com.hazelcast.instance.Node
情報: [172.20.0.1]:5702 [dev] [3.8.8] Shutting down connection manager...
12 16, 2017 12:15:09 午前 com.hazelcast.nio.tcp.TcpIpConnection
情報: [172.20.0.1]:5702 [dev] [3.8.8] Connection[id=1, /172.20.0.1:34082->/172.20.0.1:5701, endpoint=[172.20.0.1]:5701, alive=false, type=MEMBER] closed. Reason: TcpIpConnectionManager is stopping
12 16, 2017 12:15:09 午前 com.hazelcast.instance.Node
情報: [172.20.0.1]:5702 [dev] [3.8.8] Shutting down node engine...
12 16, 2017 12:15:09 午前 com.hazelcast.instance.NodeExtension
情報: [172.20.0.1]:5702 [dev] [3.8.8] Destroying node NodeExtension.
12 16, 2017 12:15:09 午前 com.hazelcast.instance.Node
情報: [172.20.0.1]:5702 [dev] [3.8.8] Hazelcast Shutdown is completed in 59 ms.
12 16, 2017 12:15:10 午前 com.hazelcast.instance.Node
重大: [172.20.0.1]:5702 [dev] [3.8.8] Could not join cluster in 300000 ms. Shutting down now!
12 16, 2017 12:15:10 午前 com.hazelcast.core.LifecycleService
情報: [172.20.0.1]:5702 [dev] [3.8.8] [172.20.0.1]:5702 is SHUTTING_DOWN
12 16, 2017 12:15:10 午前 com.hazelcast.instance.Node
情報: [172.20.0.1]:5702 [dev] [3.8.8] Node is already shutting down... Waiting for shutdown process to complete...
12 16, 2017 12:15:10 午前 com.hazelcast.core.LifecycleService
情報: [172.20.0.1]:5702 [dev] [3.8.8] [172.20.0.1]:5702 is SHUTDOWN
12 16, 2017 12:15:10 午前 com.hazelcast.instance.Node
警告: [172.20.0.1]:5702 [dev] [3.8.8] Config seed port is 5701 and cluster size is 1. Some of the ports seem occupied!
12 16, 2017 12:15:10 午前 com.hazelcast.util.PhoneHome
警告: [172.20.0.1]:5702 [dev] [3.8.8] Could not schedule phone home! Most probably Hazelcast is failed to start.
12 16, 2017 12:15:10 午前 com.hazelcast.instance.Node
情報: [172.20.0.1]:5702 [dev] [3.8.8] Node is already shutting down... Waiting for shutdown process to complete...
Caught: java.lang.IllegalStateException: Node failed to start!
java.lang.IllegalStateException: Node failed to start!
	at com.hazelcast.instance.HazelcastInstanceImpl.<init>(HazelcastInstanceImpl.java:135)
	at com.hazelcast.instance.HazelcastInstanceFactory.constructHazelcastInstance(HazelcastInstanceFactory.java:218)
	at com.hazelcast.instance.HazelcastInstanceFactory.newHazelcastInstance(HazelcastInstanceFactory.java:176)
	at com.hazelcast.instance.HazelcastInstanceFactory.newHazelcastInstance(HazelcastInstanceFactory.java:126)
	at com.hazelcast.core.Hazelcast.newHazelcastInstance(Hazelcast.java:87)
	at com.hazelcast.core.Hazelcast$newHazelcastInstance.call(Unknown Source)
	at hazelcast388-runner.run(hazelcast388-runner.groovy:17)

起動順序を逆にすると、3.9.1側が停止することになります。

では、Hazelcast 3.8で。
hazelcast38-runner.groovy

@Grab('com.hazelcast:hazelcast:3.8')
import com.hazelcast.core.Hazelcast

## 3.8.8
$ groovy hazelcast388-runner.groovy

## 3.8
$ groovy hazelcast38-runner.groovy

こちらは、問題なくクラスタが構成できます。

12 16, 2017 12:19:53 午前 com.hazelcast.internal.cluster.ClusterService
情報: [172.20.0.1]:5701 [dev] [3.8.8] 

Members [2] {
	Member [172.20.0.1]:5701 - df07e168-d992-4bba-b7c0-c6fae1f35d30 this
	Member [172.20.0.1]:5702 - 86c92361-94e4-4260-ab71-9df691b458a7
}



くどくなりますが、今度は3.8.8と3.7.8。

## 3.8.8
$ groovy hazelcast388-runner.groovy

## 3.7.8
$ groovy hazelcast378-runner.groovy

3.7系が相手の場合は、Rolling Upgadeのサポートが3.8以降だからか、両方のMemberが独立して生き残ります。

3.8.8の方。互換性がないと怒られていますね。

12 16, 2017 12:21:11 午前 com.hazelcast.internal.cluster.impl.MulticastService
警告: [172.20.0.1]:5701 [dev] [3.8.8] Received a JoinRequest with an incompatible binary-format. An old version of Hazelcast may be using the same multicast discovery port. Are you running multiple Hazelcast clusters on this host? (This message will be suppressed for 60 seconds). 

3.7.8の方は、単純に認識できないだけになるようです。

12 16, 2017 12:21:14 午前 com.hazelcast.internal.cluster.impl.MulticastJoiner
情報: [172.20.0.1]:5702 [dev] [3.7.8] 


Members [1] {
	Member [172.20.0.1]:5702 - b90571a9-3f82-4613-8207-aa27445b4dcf this
}

12 16, 2017 12:21:14 午前 com.hazelcast.instance.Node
警告: [172.20.0.1]:5702 [dev] [3.7.8] Config seed port is 5701 and cluster size is 1. Some of the ports seem occupied!

今回は同じホストでクラスタを構成しようとしたので、TCPポートがかぶってずれましたが…。

というわけで、やっぱりMinor Versionで互換性がありません、と。Minor Versionを越えてアップデートしたければ、Enterprise版が必要だということですね。

少しだけ中身を

ClusterSafeとClusterStateがどういう実装になっているか、ちょっと確認してみましょう。

ClusterSafeは、全MemberにSafeStateCheckOperationを投げることで確認します。
https://github.com/hazelcast/hazelcast/blob/v3.9.1/hazelcast/src/main/java/com/hazelcast/internal/partition/PartitionServiceProxy.java#L117-L146

実体はInternalPartitionServiceImpl#isMemberStateSafeで、これがひとつでもfalseを返すとClusterSafeとしてはfalseとなります。全Memberがtrueに
なって、はじめてClusterSafeがtrueになりますと。
https://github.com/hazelcast/hazelcast/blob/v3.9.1/hazelcast/src/main/java/com/hazelcast/internal/partition/impl/InternalPartitionServiceImpl.java#L912-L914

InternalPartitionServiceImpl#isMemberStateSafeがtrueになるというのは、PartitionReplicaStateChecker#getPartitionServiceStateが
PartitionServiceState.SAFEを返すということで、この確認はバックアップの有無やデータのリバランス中でないかどうか、Masterが
マイグレーション中でないかどうかなどを元に確認します。
https://github.com/hazelcast/hazelcast/blob/v3.9.1/hazelcast/src/main/java/com/hazelcast/internal/partition/impl/PartitionReplicaStateChecker.java#L76-L94

ClusterStateの方は、ClusterStateManager#getSafeの結果がそのまま使われます。
https://github.com/hazelcast/hazelcast/blob/v3.9.1/hazelcast/src/main/java/com/hazelcast/internal/cluster/impl/ClusterServiceImpl.java#L955-L957

ClusterStateManagerがクラスタの状態を管理しています、と。
https://github.com/hazelcast/hazelcast/blob/v3.9.1/hazelcast/src/main/java/com/hazelcast/internal/cluster/impl/ClusterStateManager.java

まとめ

Hazelcastのクラスタの状態を確認する方法を調べたり、バージョン間の互換性についてまとめてみました。

このあたりはなんとなく把握はしていたのですが、実際に確認してみたのは初めてなので、いろいろ参考になりました。

今回作成したソースコードは、こちらに置いています。
https://github.com/kazuhira-r/hazelcast-examples/tree/master/hazelcast-cluster-safe-and-vesion-compatibility

2017-12-11

Queryアノテーションが使えるようになった、Spring Data Hazelcastを試す

Spring Data Hazelcast 1.1が半年くらい前にリリースされていたのですが、このバージョンから@Queryアノテーション
使うことができるようになっていたみたいです。

GitHub - hazelcast/spring-data-hazelcast: Hazelcast Spring Data integration Project http://projects.spring.io/spring-data/

Release 1.1 ? hazelcast/spring-data-hazelcast ? GitHub

ちょっと気になるなーということで、試してみましょうかと。

なお、Spring Data Hazelcast 1.0については、以前に試しています。基本的な説明はこちらでしているので、興味のある方は
見ておくとよろしいかと思います。

Spring Data Hazelcastで遊ぶ - CLOVER

準備

pom.xmlの定義は、このようにしました。

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <spring.boot.version>1.5.9.RELEASE</spring.boot.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>com.hazelcast</groupId>
            <artifactId>spring-data-hazelcast</artifactId>
            <version>1.1.1</version>
        </dependency>

        <dependency>
            <groupId>com.hazelcast</groupId>
            <artifactId>hazelcast</artifactId>
            <version>3.9.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring.boot.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

まずは、Spring Data Hazelcast。

        <dependency>
            <groupId>com.hazelcast</groupId>
            <artifactId>spring-data-hazelcast</artifactId>
            <version>1.1.1</version>
        </dependency>

Spring Data Hazelcastが依存しているHazelcastのバージョンは3.6なので、なんとなく引き上げてみます。

        <dependency>
            <groupId>com.hazelcast</groupId>
            <artifactId>hazelcast</artifactId>
            <version>3.9.1</version>
        </dependency>

あとは、お手軽に動作確認するために、Spring Bootで。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

Entity

とりあえず、お題は書籍で。
src/main/java/org/littlewings/hazelcast/spring/entity/Book.java

package org.littlewings.hazelcast.spring.entity;

import java.io.Serializable;

import org.springframework.data.annotation.Id;
import org.springframework.data.keyvalue.annotation.KeySpace;

@KeySpace("books")
public class Book implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    private String isbn;
    private String title;
    private int price;

    public static Book create(String isbn, String title, int price) {
        Book book = new Book();
        book.setIsbn(isbn);
        book.setTitle(title);
        book.setPrice(price);

        return book;
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
}

Repository

続いて、Repositoryを定義します。HazelcastRepositoryインターフェースを拡張したインターフェースを作成します。
src/main/java/org/littlewings/hazelcast/spring/repository/BookRepository.java

package org.littlewings.hazelcast.spring.repository;

import java.util.List;

import org.littlewings.hazelcast.spring.entity.Book;
import org.springframework.data.hazelcast.repository.HazelcastRepository;
import org.springframework.data.hazelcast.repository.query.Query;

public interface BookRepository extends HazelcastRepository<Book, String> {
    @Query("title = '%s'")
    List<Book> findByTitle(String title);

    @Query("price > %d")
    List<Book> findByPriceGreaterThan(int price);

    @Query("isbn = '%s' and price > %d")
    List<Book> findByIsbnAndGreaterThanPrice(String isbn, int price);
}

@Queryでクエリを定義できるのですが、いくつか注意点があるので、そちらについては後ほど。

なお、従来どおりメソッド名でのクエリの作成も可能です。

Config

最後はConfigです。こちらについては、GitHubリポジトリに書かれているサンプルとはちょっと変えています。
src/main/java/org/littlewings/hazelcast/spring/HazelcastConfig.java

package org.littlewings.hazelcast.spring;

import com.hazelcast.config.Config;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.hazelcast.HazelcastKeyValueAdapter;
import org.springframework.data.hazelcast.repository.config.Constants;
import org.springframework.data.hazelcast.repository.config.EnableHazelcastRepositories;
import org.springframework.data.keyvalue.core.KeyValueOperations;
import org.springframework.data.keyvalue.core.KeyValueTemplate;

@Configuration
@EnableHazelcastRepositories
public class HazelcastConfig {
    @Bean
    public KeyValueOperations keyValueTemplate(HazelcastKeyValueAdapter keyValueAdapter) {
        return new KeyValueTemplate(keyValueAdapter);
    }

    @Bean
    public HazelcastKeyValueAdapter hazelcastKeyValueAdapter() {
        return new HazelcastKeyValueAdapter();
    }
}

変えた理由は落とし穴があるからなのですが、こちらも後で記載します。

とりあえず、最低限としてはEnableHazelcastRepositoriesアノテーションを付与して、HazelcastKeyValueAdapterとKeyValueOperationsをBean定義
しておけばOKです。

動かしてみる

では、こちらをテストコードを書いて動かしてみます。

テストコードの雛形は、こちら。
src/test/java/org/littlewings/hazelcast/spring/SpringDataHazelcastTest.java

package org.littlewings.hazelcast.spring;

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

import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.littlewings.hazelcast.spring.entity.Book;
import org.littlewings.hazelcast.spring.repository.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = HazelcastConfig.class)
public class SpringDataHazelcastTest {
    List<Book> books =
            Arrays.asList(
                    Book.create("978-1785285332", "Getting Started With Hazelcast", 3812),
                    Book.create("978-4798142470", "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", 4320),
                    Book.create("978-4774182179", "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ", 4104),
                    Book.create("978-4777519699", "はじめてのSpring Boot―スプリング・フレームワークで簡単Javaアプリ開発", 2700)
            );

    @Autowired
    BookRepository bookRepository;

    // ここに、テストを書く!

    void withOtherHazelcastInstances(int numInstances, Runnable runnable) {
        List<HazelcastInstance> hazelcasts =
                IntStream
                        .rangeClosed(1, numInstances)
                        .mapToObj(i -> Hazelcast.newHazelcastInstance())
                        .collect(Collectors.toList());

        try {
            runnable.run();
        } finally {
            hazelcasts.forEach(HazelcastInstance::shutdown);
        }
    }
}

簡単にHazelcastクラスタを構成する、ヘルパーメソッド付き。

テストデータはあらかじめ用意しておくのと、先ほど定義したRepositoryはAutowiredするように作成しておきます。

    List<Book> books =
            Arrays.asList(
                    Book.create("978-1785285332", "Getting Started With Hazelcast", 3812),
                    Book.create("978-4798142470", "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", 4320),
                    Book.create("978-4774182179", "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ", 4104),
                    Book.create("978-4777519699", "はじめてのSpring Boot―スプリング・フレームワークで簡単Javaアプリ開発", 2700)
            );

    @Autowired
    BookRepository bookRepository;

まずは、1件だけヒットするように仕組んだものでテストしてみます。

    @Test
    public void simpleQuery() {
        withOtherHazelcastInstances(2, () -> {
            bookRepository.save(books);

            List<Book> resultBooks = bookRepository.findByTitle(books.get(0).getTitle());

            assertThat(resultBooks).hasSize(1);
            assertThat(resultBooks.get(0).getIsbn()).isEqualTo("978-1785285332");
            assertThat(resultBooks.get(0).getTitle()).isEqualTo("Getting Started With Hazelcast");
        });
    }

HazelcastのNode数は、3にしてみました。テスト内で起動するNodeが2、Spring Data Hazelcastを使うためのBean定義で起動するNodeが1、です。

ここで、返ってくる件数とかメソッドの定義によらず、返ってくるデータ型はコレクションとなります。

            List<Book> resultBooks = bookRepository.findByTitle(books.get(0).getTitle());

1件しか返らないと思って、こんなメソッドを定義して

    @Query("title = '%s'")
    Book findByTitle(String title);

1件だけ受け取るようなコードを書いていると

            Book resultBook = bookRepository.findByTitle(books.get(0).getTitle());

キャストできないと怒られます。

java.lang.ClassCastException: com.hazelcast.map.impl.query.QueryResultCollection cannot be cast to org.littlewings.hazelcast.spring.entity.Book

また、よくよく見るとメソッドの引数をあてている箇所は、%sで書く上にシングルクォートで囲っておく必要があります。

    @Query("title = '%s'")
    List<Book> findByTitle(String title);

はい。

もうちょっとバリエーションを。

数値を使う検索。

    @Test
    public void greaterThanQuery() {
        withOtherHazelcastInstances(2, () -> {
            bookRepository.save(books);

            List<Book> resultBooks = bookRepository.findByPriceGreaterThan(4000);

            assertThat(resultBooks).hasSize(2);
            assertThat(resultBooks.get(0).getIsbn()).isEqualTo("978-4798142470");
            assertThat(resultBooks.get(0).getTitle()).isEqualTo("Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発");
            assertThat(resultBooks.get(0).getPrice()).isEqualTo(4320);
            assertThat(resultBooks.get(1).getIsbn()).isEqualTo("978-4774182179");
            assertThat(resultBooks.get(1).getTitle()).isEqualTo("[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ");
            assertThat(resultBooks.get(1).getPrice()).isEqualTo(4104);
        });
    }

元のクエリ定義は、こちらです。

    @Query("price > %d")
    List<Book> findByPriceGreaterThan(int price);

%dで定義。

andとかも使えます。

    @Query("isbn = '%s' and price > %d")
    List<Book> findByIsbnAndGreaterThanPrice(String isbn, int price);

利用例。

    @Test
    public void andQuery() {
        withOtherHazelcastInstances(2, () -> {
            bookRepository.save(books);

            List<Book> resultBooks1 =
                    bookRepository.findByIsbnAndGreaterThanPrice("978-1785285332", 3000);

            assertThat(resultBooks1).hasSize(1);
            assertThat(resultBooks1.get(0).getIsbn()).isEqualTo("978-1785285332");
            assertThat(resultBooks1.get(0).getTitle()).isEqualTo("Getting Started With Hazelcast");

            List<Book> resultBooks2 =
                    bookRepository.findByIsbnAndGreaterThanPrice("978-1785285332", 4000);

            assertThat(resultBooks2).isEmpty();
        });
    }

ちょっと気になるところ

クエリのパラメータに指定する文字列?

@Queryアノテーションでメソッドの引数を指定する際に、%sとか%dとかを使っていました。

    @Query("title = '%s'")
    List<Book> findByTitle(String title);

    @Query("price > %d")
    List<Book> findByPriceGreaterThan(int price);

    @Query("isbn = '%s' and price > %d")
    List<Book> findByIsbnAndGreaterThanPrice(String isbn, int price);

想像には難くありませんが、これはString#formatで指定できる書式と同じです。というか、String#formatが使われています。

文字列が条件になる時には、シングルクォートを省略することもできないのでバインド変数っぽいものではありません。

@Configurationのサンプル?

GitHubのREADME.mdを見ると、@Configurationはこんな感じに書かれているのですが、これは実は@Queryを使う時には動作しません。

@Configuration
@EnableHazelcastRepositories(basePackages={"example.springdata.keyvalue.chemistry"}) (1)
public class ApplicationConfiguration {

    @Bean
    HazelcastInstance hazelcastInstance() {     (2)
        return Hazelcast.newHazelcastInstance();
        // return HazelcastClient.newHazelcastClient();
    }

    @Bean
    public KeyValueOperations keyValueTemplate() {  (3)
        return new KeyValueTemplate(new HazelcastKeyValueAdapter(hazelcastInstance()));
    }

    @Bean
    public HazelcastKeyValueAdapter hazelcastKeyValueAdapter(HazelcastInstance hazelcastInstance) {
        return new HazelcastKeyValueAdapter(hazelcastInstance);
    }
}

HazelcastInstanceの作り方が良くなくて、インスタンスの名前が「spring-data-hazelcast-instance」となるように指定してあげる必要があります。
もしくは、HazelcastKeyValueAdapterを引数なしのコンストラクタを呼び出してインスタンス化するかです。

ソート例がなかったのでは?

残念ながら、@Queryを使った場合にはできません…。その場合は、KeyValueOperationsとKeyValueQueryを使用しましょう。

そもそも、Hazelcastのクエリー単体にはPagingPredicateを除いてソートできる方法が…。

どういうことか?

ここまでの疑問点は、すべてがこのクラスに集約されます。
https://github.com/hazelcast/spring-data-hazelcast/blob/v1.1.1/src/main/java/org/springframework/data/hazelcast/repository/support/StringBasedHazelcastRepositoryQuery.java

@Queryで指定された文字列をHazelcastのPredicateに変換する際に、単純にString#formatで変換しているだけになります。

    @Override
    public Object execute(Object[] parameters) {

        String queryStringTemplate = queryMethod.getAnnotatedQuery();

        String queryString = String.format(queryStringTemplate, parameters);

        SqlPredicate sqlPredicate = new SqlPredicate(queryString);

        return getMap(keySpace).values(sqlPredicate);
    }

また、検索はIMap#valuesなので、戻り値はそりゃあCollectionになりますよね、と…。

中で使われているのは、SqlPredicateです。
Querying with SQL

さらに、ここでgetMapというメソッドがあるのですが、この中でHazelcastInstanceを取得する際には、名前で指定して取得することを期待しています。
この名前が、「spring-data-hazelcast-instance」です(Constantsというクラスに定数として宣言されていますが)。

    private IMap getMap(String keySpace) {
        return Hazelcast.getHazelcastInstanceByName(Constants.HAZELCAST_INSTANCE_NAME).getMap(keySpace);
    }

ここまで見ると、@Queryの引数の指定がString#formatっぽかったり、取得結果についての扱いとかいろいろわかるでしょう。

また@Configurationなクラスについては、明示的にHazelcastInstanceを作成する場合は以下のように名前を指定して作成するのが正解、ということになります。

    @Bean(destroyMethod = "shutdown")
    public HazelcastInstance hazelcastInstance() {
        Config config = new Config();
        config.setInstanceName(Constants.HAZELCAST_INSTANCE_NAME);
        return Hazelcast.newHazelcastInstance(config);
    }

    @Bean
    public KeyValueOperations keyValueTemplate(HazelcastKeyValueAdapter keyValueAdapter) {
        return new KeyValueTemplate(keyValueAdapter);
    }

    @Bean
    public HazelcastKeyValueAdapter hazelcastKeyValueAdapter(HazelcastInstance hazelcast) {
        return new HazelcastKeyValueAdapter(hazelcast);
    }

HazelcastKeyValueAdapterの引数なしのコンストラクタを呼び出した場合は、内部的に「Constants.HAZELCAST_INSTANCE_NAME」が指定された状態で
HazelcastInstanceが作成されます。
https://github.com/hazelcast/spring-data-hazelcast/blob/v1.1.1/src/main/java/org/springframework/data/hazelcast/HazelcastKeyValueAdapter.java#L42

これが、最初のコード例では動作した理由です。

まとめ

まあいろいろあったのですが、とりあえずSpring Data Hazelcastで@Queryを使うことができました。

なんかいろいろ踏んで中身を見ることにはなりましたが、雰囲気はわかったのでよしとしましょう。結論としては、なんとも
微妙な機能な気がしますけどね…。

今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/hazelcast-examples/tree/master/hazelcast-spring-data-query

2017-12-08

Hazelcast 3.8で追加された、Scheduled Executor Serviceを試す

Hazelcast 3.8のリリースで追加された、Schduled Executor Serviceを試してみようと思います。

現時点のHazelcastのバージョンは3.9.1なので、少し前に追加された機能にあたりますが、ちょっと気になっていたので
触っておこうかと。

Scheduled Executor Service

3.8 - Hazelcast Release Notes

Hazelcast/Scheduled Executor Service

ScheduledExecutorServiceといえば、java.util.concurrentパッケージに同名のインターフェースがありますが、

ScheduledExecutorService (Java Platform SE 8)

これのHazelcast版と思っていただければ問題ありません。

IScheduledExecutorService (Hazelcast Root 3.9.1 API)

機能としては

  • schedule … 指定した遅延時間の後、タスクを実行(単発)
  • scheduleAtFixedRate … 指定した遅延時間の後、指定した期間ごとにタスクを実行(繰り返し)

の2つのメソッドに対して

  • schedule … クラスタ内のいずれかのMember上でタスクを実行
  • scheduleOnMember: クラスタ内の指定したMember上でタスクを実行
  • scheduleOnKeyOwner: クラスタ内の、指定したキーのオーナーであるMember上でタスクを実行
  • scheduleOnMembers: クラスタ内の、指定したMember(複数可)上でタスクを実行
  • scheduleOnAllMembers: クラスタ内の全Member上でタスクを実行

の5つの組み合わせの中から実行パターンを選ぶことができます。

なお、java.util.concurrent.ScheduledExecutorServiceが提供している、scheduleWithFixedDelayメソッドについては
サポートしていません。

とまあこんな概要なのですが、とりあえず使ってみましょうか。

準備

Maven依存関係は、こちら。

        <dependency>
            <groupId>com.hazelcast</groupId>
            <artifactId>hazelcast</artifactId>
            <version>3.9.1</version>
        </dependency>

あと、クラスタを構成するために、浮いていてもらうNodeを作っておきましょう。
src/main/java/org/littlewings/hazelcast/distexec/EmbeddedHazelcastServer.java

package org.littlewings.hazelcast.distexec;

import java.time.LocalDateTime;

import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;

public class EmbeddedHazelcastServer {
    public static void main(String... args) {
        HazelcastInstance hazelcast = Hazelcast.newHazelcastInstance();

        try {
            System.out.printf("[%s] startup, Embedded Hazelcast Server.%n", LocalDateTime.now());
            System.console().readLine("> Enter stop.");
        } finally {
            hazelcast.shutdown();
            Hazelcast.shutdownAll();
        }
    }
}

起動。2 Node浮いていてもらいます。

## Node 1
$ mvn compile exec:java -Dexec.mainClass=org.littlewings.hazelcast.distexec.EmbeddedHazelcastServer

## Node 2
$ mvn compile exec:java -Dexec.mainClass=org.littlewings.hazelcast.distexec.EmbeddedHazelcastServer

クラスタが構成されました。

Members {size:2, ver:2} [
	Member [172.22.0.1]:5702 - 2b49f301-3dd2-4b6a-b7a8-5e955006e938
	Member [172.22.0.1]:5701 - d769e220-53b1-45b7-889a-9936a53ddb60 this
]

ここから先は、このクラスタに参加して、タスクを放り込むNodeを作成します。

はじめてのHazelcast Scheduled Executor Service

基本的な使い方はjava.util.concurrent.ScheduledExecutorServiceと同じなので、RunnableやCallableをタスクとして
放り込むことになります。

簡単なRunnableを作ってみます。
src/main/java/org/littlewings/hazelcast/distexec/SayHelloTask.java

package org.littlewings.hazelcast.distexec;

import java.io.Serializable;
import java.time.LocalDateTime;

public class SayHelloTask implements Runnable, Serializable {
    @Override
    public void run() {
        System.out.printf("[%s] Hello!!%n", LocalDateTime.now());
    }
}

まあ大したことのないコードですが、Serializableを実装しているところがポイントです。

最初は、ただのscheduleメソッドでどこかのMember上で動かしてみましょう。
src/main/java/org/littlewings/hazelcast/distexec/SingleMemberTaskExecutor.java

package org.littlewings.hazelcast.distexec;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.scheduledexecutor.IScheduledExecutorService;
import com.hazelcast.scheduledexecutor.IScheduledFuture;

public class SingleMemberTaskExecutor {
    public static void main(String... args) throws ExecutionException, InterruptedException {
        HazelcastInstance hazelcast = Hazelcast.newHazelcastInstance();

        try {
            IScheduledExecutorService es = hazelcast.getScheduledExecutorService("default");
            IScheduledFuture<?> future = es.schedule(new SayHelloTask(), 5, TimeUnit.SECONDS);
            future.get();
        } finally {
            hazelcast.shutdown();
            Hazelcast.shutdownAll();
        }
    }
}

作成したRunnableをIScheduledExecutorService#scheduleに放り込めばいいのですが、返ってくるのはIScheduledFutureです。
ふつうのFutureと同じく、getで待ち合わせをします。

            IScheduledExecutorService es = hazelcast.getScheduledExecutorService("default");
            IScheduledFuture<?> future = es.schedule(new SayHelloTask(), 5, TimeUnit.SECONDS);
            future.get();

では、実行。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.hazelcast.distexec.SingleMemberTaskExecutor

クラスタが構成されて

Members {size:3, ver:5} [
	Member [172.22.0.1]:5702 - 2b49f301-3dd2-4b6a-b7a8-5e955006e938
	Member [172.22.0.1]:5701 - d769e220-53b1-45b7-889a-9936a53ddb60
	Member [172.22.0.1]:5703 - 1552aba3-b350-41df-85ac-9f31fb560e07 this
]

今回は、最初に起動したNodeで動作しました(この見た目ではわかりませんが)。

[2017-12-08T23:29:06.150] Hello!!

今度はCallableで

次は、Callableで試してみます。
src/main/java/org/littlewings/hazelcast/distexec/HelloCallableTask.java

package org.littlewings.hazelcast.distexec;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.concurrent.Callable;

import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.HazelcastInstanceAware;

public class HelloCallableTask implements Callable<String>, HazelcastInstanceAware, Serializable {
    transient HazelcastInstance hazelcast;

    @Override
    public void setHazelcastInstance(HazelcastInstance hazelcast) {
        this.hazelcast = hazelcast;
    }

    @Override
    public String call() throws Exception {
        return String.format("[%s] Hello from %s!!", LocalDateTime.now(), hazelcast.getCluster().getLocalMember().getUuid());
    }
}

こちらも大したことのない処理ですが、今回はHazelcastInstanceAwareインターフェースを実装し、HazelcastInstanceを
受け取れるようにしました。こちらを使って、自分のUUIDを返却するようにしています。

なお、HazelcastInstanceはSerializableではないので、transientにしておく必要があります。

    transient HazelcastInstance hazelcast;

こちらのCallableを使ってみましょう。

public class SingleMemberCallableTaskExecutor {
    public static void main(String... args) throws ExecutionException, InterruptedException {
        HazelcastInstance hazelcast = Hazelcast.newHazelcastInstance();

        try {
            IScheduledExecutorService es = hazelcast.getScheduledExecutorService("default");
            IScheduledFuture<String> future = es.schedule(new HelloCallableTask(), 5, TimeUnit.SECONDS);
            System.out.println(future.get());
        } finally {
            hazelcast.shutdown();
            Hazelcast.shutdownAll();
        }
    }
}

結果。

## 実行
$ mvn compile exec:java -Dexec.mainClass=org.littlewings.hazelcast.distexec.SingleMemberCallableTaskExecutor


## クラスタ
Members {size:3, ver:7} [
	Member [172.22.0.1]:5702 - 2b49f301-3dd2-4b6a-b7a8-5e955006e938
	Member [172.22.0.1]:5701 - d769e220-53b1-45b7-889a-9936a53ddb60
	Member [172.22.0.1]:5703 - b24c4140-f2b6-4bbe-96ab-66a088b03f0c this
]


## 結果
[2017-12-08T23:34:28.163] Hello from 2b49f301-3dd2-4b6a-b7a8-5e955006e938!!

結果は、2番目に起動したNodeから返ってきたようです。

もっとバリエーション

もうちょっといろいろ試してみましょう。

今度は、RunnableでもHazelcastInstanceAwareインターフェースを実装したクラスを作成してみます。
src/main/java/org/littlewings/hazelcast/distexec/HelloRunnableTask.java

package org.littlewings.hazelcast.distexec;

import java.io.Serializable;
import java.time.LocalDateTime;

import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.HazelcastInstanceAware;

public class HelloRunnableTask implements Runnable, HazelcastInstanceAware, Serializable {
    transient HazelcastInstance hazelcast;

    @Override
    public void setHazelcastInstance(HazelcastInstance hazelcast) {
        this.hazelcast = hazelcast;
    }

    @Override
    public void run() {
        System.out.printf("[%s] Hello from %s%n", LocalDateTime.now(), hazelcast.getCluster().getLocalMember().getUuid());
    }
}

こちらを、あるキーのオーナーであるMember上で実行。

public class OnKeyOwnerTaskExecutor {
    public static void main(String... args) throws ExecutionException, InterruptedException {
        HazelcastInstance hazelcast = Hazelcast.newHazelcastInstance();

        try {
            String key = "key1";

            IScheduledExecutorService es = hazelcast.getScheduledExecutorService("default");
            IScheduledFuture<?> future = es.scheduleOnKeyOwner(new HelloRunnableTask(), key, 5, TimeUnit.SECONDS);
            future.get();
        } finally {
            hazelcast.shutdown();
            Hazelcast.shutdownAll();
        }
    }
}

実行。

## 実行
$ mvn compile exec:java -Dexec.mainClass=org.littlewings.hazelcast.distexec.OnKeyOwnerTaskExecutor


## クラスタ
Members {size:3, ver:9} [
	Member [172.22.0.1]:5702 - 2b49f301-3dd2-4b6a-b7a8-5e955006e938
	Member [172.22.0.1]:5701 - d769e220-53b1-45b7-889a-9936a53ddb60
	Member [172.22.0.1]:5703 - 53afbfbc-0c4d-4a3d-9ce6-a67bad9f8e1d this
]


## 結果(自Node)
[2017-12-08T23:37:24.953] Hello from 53afbfbc-0c4d-4a3d-9ce6-a67bad9f8e1d

今回は、自Nodeで動作しました…。

タスクの定義はこれくらいにして、他のバリエーションも。

クラスタ内の、複数のMember上でタスクを実行。
src/main/java/org/littlewings/hazelcast/distexec/ClusterMembersCallableTaskExecutor.java

package org.littlewings.hazelcast.distexec;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.Member;
import com.hazelcast.scheduledexecutor.IScheduledExecutorService;
import com.hazelcast.scheduledexecutor.IScheduledFuture;

public class ClusterMembersCallableTaskExecutor {
    public static void main(String... args) throws ExecutionException, InterruptedException {
        HazelcastInstance hazelcast = Hazelcast.newHazelcastInstance();

        try {
            Set<Member> members = new HashSet<>(hazelcast.getCluster().getMembers());
            members.remove(hazelcast.getCluster().getLocalMember());

            IScheduledExecutorService es = hazelcast.getScheduledExecutorService("default");
            Map<Member, IScheduledFuture<String>> futures =
                    es.scheduleOnMembers(new HelloCallableTask(), members, 5, TimeUnit.SECONDS);

            for (Map.Entry<Member, IScheduledFuture<String>> entry : futures.entrySet()) {
                System.out.printf("member[%s] from message = %s%n", entry.getKey().getUuid(), entry.getValue().get());
            }

            System.out.printf("self = %s%n", hazelcast.getCluster().getLocalMember().getUuid());
        } finally {
            hazelcast.shutdown();
            Hazelcast.shutdownAll();
        }
    }
}

今回は、自Member以外のNodeで実行してみることにしました。

            Set<Member> members = new HashSet<>(hazelcast.getCluster().getMembers());
            members.remove(hazelcast.getCluster().getLocalMember());

IScheduledExecutorService#scheduleOnMembersで、指定したMember上で実行しますが、この場合はMemberをキー、IScheduledFutureを
値にしたMapが返ってきます。

            IScheduledExecutorService es = hazelcast.getScheduledExecutorService("default");
            Map<Member, IScheduledFuture<String>> futures =
                    es.scheduleOnMembers(new HelloCallableTask(), members, 5, TimeUnit.SECONDS);

実行。

## 実行
$ mvn compile exec:java -Dexec.mainClass=org.littlewings.hazelcast.distexec.ClusterMembersCallableTaskExecutor


## クラスタ
Members {size:3, ver:11} [
	Member [172.22.0.1]:5702 - 2b49f301-3dd2-4b6a-b7a8-5e955006e938
	Member [172.22.0.1]:5701 - d769e220-53b1-45b7-889a-9936a53ddb60
	Member [172.22.0.1]:5703 - 4a100710-4c28-4e6e-88c1-a1e1926d7f3a this
]


## 結果
member[d769e220-53b1-45b7-889a-9936a53ddb60] from message = [2017-12-08T23:41:50.435] Hello from d769e220-53b1-45b7-889a-9936a53ddb60!!
member[2b49f301-3dd2-4b6a-b7a8-5e955006e938] from message = [2017-12-08T23:41:50.440] Hello from 2b49f301-3dd2-4b6a-b7a8-5e955006e938!!
self = 4a100710-4c28-4e6e-88c1-a1e1926d7f3a

最後に出力しているのは自MemberのUUIDですが、それ以外のNodeから結果が返ってきました。

繰り返し実行

最後は、繰り返しタスクを実行してみます。
src/main/java/org/littlewings/hazelcast/distexec/AllMemberRepeatTaskExecutor.java

package org.littlewings.hazelcast.distexec;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.Member;
import com.hazelcast.scheduledexecutor.IScheduledExecutorService;
import com.hazelcast.scheduledexecutor.IScheduledFuture;

public class AllMemberRepeatTaskExecutor {
    public static void main(String... args) throws ExecutionException, InterruptedException {
        HazelcastInstance hazelcast = Hazelcast.newHazelcastInstance();

        try {
            IScheduledExecutorService es = hazelcast.getScheduledExecutorService("default");

            Map<Member,IScheduledFuture<?>> futures =
                    es.scheduleOnAllMembersAtFixedRate(new HelloRunnableTask(), 5, 5, TimeUnit.SECONDS);

            System.console().readLine("> Enter stop tasks.");

            for (Map.Entry<Member, IScheduledFuture<?>> entry : futures.entrySet()) {
                entry.getValue().cancel(false);
            }

            es.shutdown();

            System.out.println("Task Finish!!");
        } finally {
            hazelcast.shutdown();
            Hazelcast.shutdownAll();
        }
    }
}

繰り返しタスクを実行するには、IScheduledExecutorService#schedule〜AtFixedRateを使用します。今回は、
scheduleOnAllMembersAtFixedRateを使用して全Member上で動かすことにしました。

            Map<Member,IScheduledFuture<?>> futures =
                    es.scheduleOnAllMembersAtFixedRate(new HelloRunnableTask(), 5, 5, TimeUnit.SECONDS);

この場合は、Runnableしか渡すことができません(java.util.concurrent.ScheduledExecutorServiceも同様)。

また、タスクを延々と実行し続けるので、Enterを押したら停止するようにしました。IScheduledFuture#cancelで
タスクをキャンセルできるのですが、cancelに渡す引数はfalseでなければなりません。

            System.console().readLine("> Enter stop tasks.");

            for (Map.Entry<Member, IScheduledFuture<?>> entry : futures.entrySet()) {
                entry.getValue().cancel(false);
            }

            es.shutdown();

trueを渡すと、UnsupportedOperationExceptionが飛んできます。
https://github.com/hazelcast/hazelcast/blob/v3.9.1/hazelcast/src/main/java/com/hazelcast/scheduledexecutor/impl/ScheduledFutureProxy.java#L108-L113

では、実行。

## 実行
$ mvn compile exec:java -Dexec.mainClass=org.littlewings.hazelcast.distexec.AllMemberRepeatTaskExecutor


## クラスタ
Members {size:3, ver:13} [
	Member [172.22.0.1]:5702 - 2b49f301-3dd2-4b6a-b7a8-5e955006e938
	Member [172.22.0.1]:5701 - d769e220-53b1-45b7-889a-9936a53ddb60
	Member [172.22.0.1]:5703 - f4d079d4-5427-4080-a01f-7129a7ca36b3 this
]


## 結果
### Node 1
[2017-12-08T23:48:21.983] Hello from d769e220-53b1-45b7-889a-9936a53ddb60
[2017-12-08T23:48:26.983] Hello from d769e220-53b1-45b7-889a-9936a53ddb60
[2017-12-08T23:48:31.983] Hello from d769e220-53b1-45b7-889a-9936a53ddb60
[2017-12-08T23:48:36.983] Hello from d769e220-53b1-45b7-889a-9936a53ddb60
[2017-12-08T23:48:41.983] Hello from d769e220-53b1-45b7-889a-9936a53ddb60

### Node 2
[2017-12-08T23:48:21.978] Hello from 2b49f301-3dd2-4b6a-b7a8-5e955006e938
[2017-12-08T23:48:26.978] Hello from 2b49f301-3dd2-4b6a-b7a8-5e955006e938
[2017-12-08T23:48:31.978] Hello from 2b49f301-3dd2-4b6a-b7a8-5e955006e938
[2017-12-08T23:48:36.978] Hello from 2b49f301-3dd2-4b6a-b7a8-5e955006e938
[2017-12-08T23:48:41.978] Hello from 2b49f301-3dd2-4b6a-b7a8-5e955006e938

### 自Node
[2017-12-08T23:48:22.014] Hello from f4d079d4-5427-4080-a01f-7129a7ca36b3
[2017-12-08T23:48:26.985] Hello from f4d079d4-5427-4080-a01f-7129a7ca36b3
[2017-12-08T23:48:31.985] Hello from f4d079d4-5427-4080-a01f-7129a7ca36b3
[2017-12-08T23:48:36.985] Hello from f4d079d4-5427-4080-a01f-7129a7ca36b3
[2017-12-08T23:48:41.985] Hello from f4d079d4-5427-4080-a01f-7129a7ca36b3

適当なところでEnterを押すと、すべてのIScheduledFutureをキャンセルするので、タスクが終了します。

ここで、キャンセルせずに起動元を止めても、タスクが他Nodeでそのまま走り続けます…。

とりあえず、基本的なバリエーションは確認してみたというところですね。

設定

Scheduled Executor Serviceは、いくつか設定項目があります。

Configuring Scheduled Executor Service

設定可能なのは

  • pool-size … Member単位のExecutorが持つスレッド数。デフォルト16
  • durability … Executorの耐久度(Nodeダウンに備えて、タスクをどれだけバックアップするか)。デフォルト1
  • capacity … Partitionごとにタスクを保持できる最大数。これを越えてタスクを追加しようとすると、RejectedExecutionExceptionが発生する。デフォルト100

の3つです。

とりあえず、設定してみましょう。
src/main/resources/hazelcast.xml

<?xml version="1.0" encoding="UTF-8"?>
<hazelcast xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.9.xsd"
           xmlns="http://www.hazelcast.com/schema/config"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <scheduled-executor-service name="configuredScheduledExecSvc">
        <pool-size>32</pool-size>
        <durability>2</durability>
        <capacity>200</capacity>
    </scheduled-executor-service>

    <scheduled-executor-service name="defaultValuedScheduledExecSvc">
        <pool-size>16</pool-size>
        <durability>1</durability>
        <capacity>100</capacity>
    </scheduled-executor-service>
</hazelcast>

2つ目の設定は、デフォルト値を明示的に設定したものです。

ちょっとデフォルト値と、設定した値を表示してみましょう。
src/main/java/org/littlewings/hazelcast/distexec/ConfiguredExecutorServiceRunner.java

package org.littlewings.hazelcast.distexec;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import com.hazelcast.config.ScheduledExecutorConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.scheduledexecutor.IScheduledExecutorService;
import com.hazelcast.scheduledexecutor.IScheduledFuture;

public class ConfiguredExecutorServiceRunner {
    public static void main(String... args) throws ExecutionException, InterruptedException {
        HazelcastInstance hazelcast = Hazelcast.newHazelcastInstance();

        try {
            ScheduledExecutorConfig defaultConfig =
                    hazelcast.getConfig().getScheduledExecutorConfig("default");
            System.out.printf("default pool-size = %d%n", defaultConfig.getPoolSize());
            System.out.printf("default durability = %d%n", defaultConfig.getDurability());
            System.out.printf("default capacity = %d%n", defaultConfig.getCapacity());

            ScheduledExecutorConfig customConfig =
                    hazelcast.getConfig().getScheduledExecutorConfig("configuredScheduledExecSvc");
            System.out.printf("custom pool-size = %d%n", customConfig.getPoolSize());
            System.out.printf("custom durability = %d%n", customConfig.getDurability());
            System.out.printf("custom capacity = %d%n", customConfig.getCapacity());

            IScheduledExecutorService es = hazelcast.getScheduledExecutorService("configuredScheduledExecSvc");
            IScheduledFuture<?> future = es.schedule(new HelloRunnableTask(), 0, TimeUnit.SECONDS);
            future.get();
        } finally {
            hazelcast.shutdown();
            Hazelcast.shutdownAll();
        }
    }
}

結果。

## 実行
mvn compile exec:java -Dexec.mainClass=org.littlewings.hazelcast.distexec.ConfiguredExecutorServiceRunner

## 結果
default pool-size = 16
default durability = 1
default capacity = 100
custom pool-size = 32
custom durability = 2
custom capacity = 200

ちゃんと、設定されていますよ、と。

もう少し深堀り

ところで、このScheduled Executor Serviceってどうやって実現されているんでしょう?

内部的には、タスクが依頼された各Nodeで、ローカルなjava.util.concurrent.ScheduledExecutorServiceを動かしています。
https://github.com/hazelcast/hazelcast/blob/v3.9.1/hazelcast/src/main/java/com/hazelcast/util/executor/LoggingScheduledExecutor.java#L51

これらのScheduledExecutorServiceを作成しているのは、このあたりですね。
https://github.com/hazelcast/hazelcast/blob/v3.9.1/hazelcast/src/main/java/com/hazelcast/spi/impl/executionservice/impl/ExecutionServiceImpl.java#L141-L150

また、単にscheduleメソッドを呼び出した時にタスクを実行するMemberは、どのように決まるのでしょう?

この場合は自前でUUIDを生成して、それをキーにして動作させるPartitionId(=Member)を決定します。
https://github.com/hazelcast/hazelcast/blob/v3.9.1/hazelcast/src/main/java/com/hazelcast/scheduledexecutor/impl/ScheduledExecutorServiceProxy.java#L417-L424
https://github.com/hazelcast/hazelcast/blob/v3.9.1/hazelcast/src/main/java/com/hazelcast/scheduledexecutor/impl/ScheduledExecutorServiceProxy.java#L104-L105
https://github.com/hazelcast/hazelcast/blob/v3.9.1/hazelcast/src/main/java/com/hazelcast/scheduledexecutor/impl/ScheduledExecutorServiceProxy.java#L391-L393

要するに、scheduleOnKeyOwnerと比べると明示的にキーを指定するか、Hazelcastに勝手にUUIDを振ってもらうかの差、ということですね。

まとめ

Hazelcast 3.8で追加された、Scheduled Executor Serviceを試してみました。

java.util.concurrent.ScheduledExecutorServiceをHazelcast上で実行できるようにしたものですが、知っていると役に立つ…かも?

今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/hazelcast-examples/tree/master/hazelcast-scheduled-distexec

2017-11-26

Go製Webサーバー、Caddyで遊ぶ

ちょっとした調べ物をしていたら、CaddyというGo製のWebサーバーが便利だということを知りまして。

Caddy

CADDYで手軽にHTTP/2サーバを立てる - yuw27b’s blog

Caddy で HTTP/2 と php-fpm を利用する - Qiita

Caddy?

Goで実装されたWebサーバーだそうです。

Caddy

GitHub - mholt/caddy: Fast, cross-platform HTTP/2 web server with automatic HTTPS

機能はこちら。
Features

  • 設定ファイルが簡単&バリデーションができる
  • 静的ファイル配信&動的配信(テンプレート、プロキシ、FastCGI)が可能
  • CLIサポート
  • プラグイン
  • マルチコア対応
  • 組み込みWebサーバーとして使用可能
  • コマンド起動が可能
  • Let's Encrypt対応
  • 自己書名証明書の自動生成も可能
  • HTTP/2、QUIC、WebSocketが使える

などなど、いろいろあるようですが、ちょっとしたWebサーバーとして扱うことも簡単にできそうなので、良さそうだなぁと。

というわけで、ちょっと試してみます。

インストール

ダウンロードは、こちらから。
Download

今回は、無料のPersonalライセンスを選択します。ページ下部にダイレクトリンクができるので、そちらを使ってダウンロード。

現時点でのバージョンは、0.10.10のようです。

$ wget https://caddyserver.com/download/linux/amd64?license=personal -O caddy_v0.10.10_linux_amd64_personal.tar.gz

ダウンロード時に、プラグインを付けることもできるみたいですね。

https://caddyserver.com/download/linux/amd64?plugins=http.prometheus&license=personal

とっかかりは、このあたりを見るとよいでしょう。

Quick Start

Begginer Tutorial

今回は、特になにも付けずダウンロード、展開。

$ tar -zxvf caddy_v0.10.10_linux_amd64_personal.tar.gz

起動は、カレントディレクトリに展開された「caddy」ファイルを実行します。

ファイル一覧。

$ ls -1
CHANGES.txt
EULA.txt
LICENSES.txt
README.txt
caddy
caddy_v0.10.10_linux_amd64_personal.tar.gz
init

起動。

$ ./caddy 
Activating privacy features... done.
http://:2015
WARNING: File descriptor limit 1024 is too low for production servers. At least 8192 is recommended. Fix with "ulimit -n 8192".

ファイルディスクリプタ数の警告が設定によっては出るみたいですが、これでカレントディレクトリをドキュメントルートとして2015ポートをリッスンして
起動します。

確認。

$ curl localhost:2015/README.txt
CADDY 0.10.10

Website
	https://caddyserver.com

Community Forum
	https://caddy.community

Twitter
	@caddyserver

Source Code
	https://github.com/mholt/caddy
	https://github.com/caddyserver


For instructions on using Caddy, please see the docs on the
website. For a list of what's new in this version, see
CHANGES.txt.

For a good time, follow @mholt6 on Twitter.

Want to get involved with Caddy's development? We love to have
contributions! Please file an issue on GitHub to discuss a
change or fix you'd like to make, then submit a pull request
and we'll review it! Your contributions will reach millions
of people who connect to sites served by Caddy.

Extend Caddy by developing a plugin for it! Instructions on
the project wiki: https://github.com/mholt/caddy/wiki

And thanks - you're awesome!

If you think Caddy is awesome too, consider sponsoring it:
https://caddyserver.com/pricing - and help keep Caddy free
for personal use.


---
(c) 2015-2017 Light Code Labs, LLC

はい。

CLIでオプション指定

caddyコマンドには、けっこうな数の起動オプションがあり、設定を変更できます。

Command Line Interface

いくつか、使いそうなものを。

ポートの変更。リッスンポートを8080に。

$ ./caddy -port 8080

ドキュメントルートの変更。

## コンテンツ作成
$ mkdir docroot
$ echo 'Hello Caddy!' > docroot/hello.txt

## ドキュメントルートを指定して起動
$ ./caddy -root docroot

## 確認
$ curl localhost:2015/hello.txt
Hello Caddy!

Caddyfileを作成する

設定は、CLIで指定するだけではなく、「Caddyfile」という設定ファイルを使って指定することもできます。

Caddyfile Syntax

先ほどの、ポート8080でdocrootな指定の例だとこうなるのですが…
※ちなみに、ポートを指定しないと80でリッスンしようとするみたいです
Caddyfile

:8080
root docroot

このまま起動すると、デフォルトでメールアドレスの入力を求められ、Let's Encryptを使おうとします。

$ ./caddy 
Activating privacy features...
Your sites will be served over HTTPS automatically using Let's Encrypt.
By continuing, you agree to the Let's Encrypt Subscriber Agreement at:
  https://acme-v01.api.letsencrypt.org/terms
Please enter your email address so you can recover your account if needed.
You can leave it blank, but you'll lose the ability to recover your account.
Email address:

とりあえず、HTTPSについてはオフにしたい場合は、「tls off」を指定します。
Caddyfile

:8080
root docroot

tls off

これで起動すると

$ ./caddy

先ほどCLIで指定した内容を、合わせた挙動になります。

$ curl localhost:8080/hello.txt
Hello Caddy!

デフォルトではカレントディレクトリにある「Caddyfile」を読み込むようですが、別ディレクトリにある場合は「-conf」で指定すればよいようです。

$ ./caddy -conf /path/to/Caddyfile

ちなみに、自己書名証明書をCaddyに自動生成させる場合は、「tls self_signed」と指定します。
tls

tls self_signed

確認。

$ curl -k https://localhost:8080/hello.txt
Hello Caddy!

このあたりを見ていると、もっと複雑な設定が書けそうですが、今回は対象外。

GitHub - caddyserver/examples: Simple guided examples of how to use Caddy

プロキシ

最後に、プロキシサーバーとして使ってみましょう。

proxy

172.17.0.2にnginxサーバーを立てたとします。

$ curl 172.17.0.2
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>

</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

このサーバーにプロキシしてみましょう。

Caddyfileを、次のように設定します。
Caddyfile

:2015
tls off

proxy / 172.17.0.2/

proxyディレクティブを指定します。

これで起動して

$ ./caddy

確認。

$ curl localhost:2015
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>

</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

無事、プロキシできたようです。

お手軽に使えそうで、なかなか便利そうな感じですね。ちょっとしたところから、使っていってみようかなと思います。

Apache Velocity 2.0がリリースされたよという話

ちょっとした小ネタです。

ひっそりと、Apache Velocity 2.0がリリースされていました。

Velocity Engine 2.0 released

リリース日は、2017年8月6日。4ヶ月近く、気付いてませんでした(笑)。

Apache Velocityの更新の様子は時々見ているのですが、2.0タグが振られたと思ったらRCに戻ったり、2.0タグがまた更新
されたりしてだいぶ落ち着きがない感じがしていたのですが、いつの間にかリリースされていたようです…。

Apache Velocity 1.7が、2010年11月29日のリリースであることを考えると、実に7年ぶりくらいのリリースとなります。

で、せっかくなのでちょっと見てみましょうと。

お断り

世間的には、Apache Velocityはすでに役目を終えた印象かと思います。Spring Frameworkのサポートも外れていますし、
一般的に使うのであればVelocity Toolsも合わせて使うことになるとは思いますが、最新のVelocity Toolsはまだリリース
されていません。

[SPR-13795] Remove Velocity support - Spring JIRA

今回リリースされているのは、Velocity Engineのみとなります。

Note for Velocity Tools users: Velocity Tools 3.0 shall soon be released. Meanwhile, you are encouraged to use the Velocity Tools 3.x last snapshot (see Velocity Tools 3.x Upgrading notes).

http://velocity.apache.org/news.html#engine20

また、Apache Velocity 1.7から2.0になってものすごく劇的に変わったというわけでもないので、2.0のリリースで
なにか状況が変わるようなこともないと思います。

Apache Velocity自体の更新頻度も、とても低いですしね(Velocity Toolsはいつ出ることやら…)。

そんなわけで、ここから先はそれでもApache Velocityに興味のある方だけお読みいただければよいと思います。

Apache Velocity 2.0

正確には、Velocity Engine 2.0です。

リリース内容はこちら。
Velocity Engine 2.0 released

詳細なRelease Notesは、こちら。
Apache Velocity Engine - Changes Report

ざっくりとリリース内容を書くと

  • LoggingがSLF4Jへ
  • 空白の設定ができるようになったよ
  • メソッド引数と配列の添字に、式が使えるよ
  • 型変換ハンドラが設定可能だよ
  • メモリ消費量を削減したよ
  • JSR-223のサポート

という感じです。JSR-223のサポートってなにさ?という感じですけどね…。

あと、ビルドや依存関係の視点でいくと…

  • ビルドシステムがApache AntからApache Maven
  • 必要な依存関係がSLF4JとCommons Lang 3のみに(前はCommons Collectionも必要だったり、オプションでOROとかJDOMとかいろいろあった)
  • アーティファクトのIDが「velocity」から「velocity-engine-core」へ

というような変化もあります。

Commons CollectionsのExtendedPropertiesというクラスに依存していたのですが、これをApache Velocity側に取り込んだようです…。
https://github.com/apache/velocity-engine/blob/2.0/velocity-engine-core/src/main/java/org/apache/velocity/util/ExtProperties.java

* This class is a clone of org.apache.commons.collections.ExtendedProperties
* (which has been removed from commons-collections-4.x)

https://github.com/apache/velocity-engine/blob/2.0/velocity-engine-core/src/main/java/org/apache/velocity/util/ExtProperties.java#L131-L132

まあ、依存関係がスッキリしたのは良いですよね。

地味な改善では、入出力のデフォルトがUTF-8になったり、JSR-223のサポート(!)とかどういう動機なのか不明なものもありますが、
ざっくり使ってみましょうか。

Getting Started

では、使ってみましょうというところですが、まともにドキュメントが揃っていないのでUser Guideで構文を、あとはサンプルを見て書くことに
なるでしょう。
https://github.com/apache/velocity-engine/tree/2.0/velocity-engine-examples

Velocity Engineを利用するのに、必要な依存関係はこちら。

        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.0</version>
        </dependency>

ロギングについてはSLF4Jですが、今回はslf4j-simpleを入れておくことにします。

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.25</version>
        </dependency>

実行はテストで。

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.0.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.8.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.19</version>
                <dependencies>
                    <dependency>
                        <groupId>org.junit.platform</groupId>
                        <artifactId>junit-platform-surefire-provider</artifactId>
                        <version>1.0.2</version>
                    </dependency>
                    <dependency>
                        <groupId>org.junit.jupiter</groupId>
                        <artifactId>junit-jupiter-engine</artifactId>
                        <version>5.0.2</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>

では、コードの雛形を…。
src/test/java/org/littlewings/velocity/VelocityCoreTest.java

package org.littlewings.velocity;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.VelocityEngine;
import org.junit.jupiter.api.Test;

public class VelocityCoreTest {
    static {
        System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
    }

    // ここに、テストを書く!
}

なんとなく、slf4j-simpleをデバッグモードに。

velocity.propertiesは、こんな感じで用意。
src/test/resources/velocity.properties

resource.loader=classpath
classpath.resource.loader.class=org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader

設定ファイルを、クラスパスからロードできてもいいんではないかと…。

テンプレートは、こんな感じで用意。
src/test/resources/simple.vm

こんにちは $!{name}!!

Fruits:
#foreach ($fruit in $!fruits)
  #if ($fruit.price > 500)
  $fruit.name
  #end
#end

使い方は、これまでのApache Velocityの使い方と変わりません。

    @Test
    public void simpleUsage() throws IOException {
        Properties properties = new Properties();
        try (InputStream is =
                     getClass().getClassLoader().getResourceAsStream("velocity.properties")) {
            properties.load(is);
        }

        Velocity.init(properties);

        VelocityContext context = new VelocityContext();
        context.put("name", "Velocity");

        List<Fruit> fruits = Arrays.asList(
                Fruit.create("apple", 600),
                Fruit.create("orange", 300),
                Fruit.create("banana", 800)
        );

        context.put("fruits", fruits);

        Template template = Velocity.getTemplate("simple.vm");

        StringWriter writer = new StringWriter();
        template.merge(context, writer);
        System.out.println(writer.toString());
    }

Fruitクラスの定義は、こんな感じです。

    public static class Fruit {
        String name;
        int price;

        static Fruit create(String name, int price) {
            Fruit fruit = new Fruit();
            fruit.setName(name);
            fruit.setPrice(price);
            return fruit;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getPrice() {
            return price;
        }

        public void setPrice(int price) {
            this.price = price;
        }
    }

結果。

こんにちは Velocity!!

Fruits:
  apple
  banana

VelocityEngineのインスタンスを個別に作成する場合。

    @Test
    public void instance() throws IOException {
        Properties properties = new Properties();
        try (InputStream is =
                     getClass().getClassLoader().getResourceAsStream("velocity.properties")) {
            properties.load(is);
        }

        VelocityEngine velocity = new VelocityEngine();
        velocity.init(properties);

        VelocityContext context = new VelocityContext();
        context.put("name", "Velocity");

        List<Fruit> fruits = Arrays.asList(
                Fruit.create("apple", 600),
                Fruit.create("orange", 300),
                Fruit.create("banana", 800)
        );

        context.put("fruits", fruits);

        Template template = velocity.getTemplate("simple.vm");

        StringWriter writer = new StringWriter();
        template.merge(context, writer);
        System.out.println(writer.toString());
    }

まあ、ふつうに使えそうですね?

JSR-223サポート

最初、これがなにを言っているのかわかりませんでしたが、Apache VelocityのテンプレートをスクリプトとしてJSR-223のAPIで扱うことができます。

要するに、Apache VelocityがJSR-223のScriptEngineの実装を提供します。

これを使うには、「velocity-engine-scripting」を依存関係に追加します。

        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-scripting</artifactId>
            <version>2.0</version>
        </dependency>

「velocity-engine-core」は、scripting側の依存関係に含まれます。

コードの雛形。
src/test/java/org/littlewings/velocity/VelocityScriptingTest.java

package org.littlewings.velocity;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;

import org.apache.velocity.script.VelocityScriptEngine;
import org.junit.jupiter.api.Test;

public class VelocityScriptingTest {
    static {
        System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
    }

    // ここに、テストを書く!
}

単純に使う場合には、こんな使い方になります。

    @Test
    public void simpleScripting() throws ScriptException {
        ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
        ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("velocity");

        Bindings bindings = scriptEngine.createBindings();

        bindings.put("name", "Velocity");

        PrintWriter writer = (PrintWriter) scriptEngine.eval("Hello ${name}!!", bindings);
        writer.println();
    }

なんと、ScriptEngineManagerから、「velocity」でScriptEngineを取得できます。

        ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
        ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("velocity");

テンプレートに渡す値は、Bindingsで設定します。

        Bindings bindings = scriptEngine.createBindings();

        bindings.put("name", "Velocity");

ScriptEngine#evalに、テンプレートをStringとして渡したり、Readerとして渡したりするとテンプレートを評価することができます。

        PrintWriter writer = (PrintWriter) scriptEngine.eval("Hello ${name}!!", bindings);
        writer.println();

ScriptEngine#evalの結果は、デフォルトではPrintWriterとして戻ってきます。

        PrintWriter writer = (PrintWriter) scriptEngine.eval("Hello ${name}!!", bindings);
        writer.println();

Writerが戻ってくるという動作自体は、変更できないようです。

結果。

Hello Velocity!!

動いてますね…。

ちなみに、この時のApache Velocityの設定はデフォルトのもので動作します。

[main] DEBUG org.apache.velocity - Default Properties resource: org/apache/velocity/runtime/defaults/velocity.properties
[main] DEBUG org.apache.velocity - ResourceLoader instantiated: org.apache.velocity.runtime.resource.loader.FileResourceLoader

このあたりをカスタマイズしたい場合は、こんな感じになります。

    @Test
    public void complex() throws IOException, ScriptException {
        ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
        ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("velocity");

        Properties properties = new Properties();
        try (InputStream is =
                     getClass().getClassLoader().getResourceAsStream("velocity.properties")) {
            properties.load(is);
        }

        ScriptContext context = new SimpleScriptContext();
        Bindings bindings = scriptEngine.createBindings();
        context.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
        context.setWriter(new StringWriter());

        bindings.put(VelocityScriptEngine.VELOCITY_PROPERTIES_KEY, properties);
        bindings.put(VelocityScriptEngine.FILENAME, "simple.vm");

        bindings.put("name", "Velocity");

        List<Fruit> fruits = Arrays.asList(
                Fruit.create("apple", 600),
                Fruit.create("orange", 300),
                Fruit.create("banana", 800)
        );

        bindings.put("fruits", fruits);

        try (BufferedReader reader =
                     new BufferedReader(new InputStreamReader(
                             getClass().getClassLoader().getResourceAsStream("simple.vm"),
                             StandardCharsets.UTF_8)
                     )) {
            StringWriter writer = (StringWriter) scriptEngine.eval(reader, context);

            System.out.println(writer.toString());
        }
    }

velocity.propertiesをPropertiesとして取り込んでおいて

        Properties properties = new Properties();
        try (InputStream is =
                     getClass().getClassLoader().getResourceAsStream("velocity.properties")) {
            properties.load(is);
        }

設定自体は、Bindingsにキー「VelocityScriptEngine.VELOCITY_PROPERTIES_KEY」で登録します。

        ScriptContext context = new SimpleScriptContext();
        Bindings bindings = scriptEngine.createBindings();
        context.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
        context.setWriter(new StringWriter());

        bindings.put(VelocityScriptEngine.VELOCITY_PROPERTIES_KEY, properties);
        bindings.put(VelocityScriptEngine.FILENAME, "simple.vm");

        bindings.put("name", "Velocity");

        List<Fruit> fruits = Arrays.asList(
                Fruit.create("apple", 600),
                Fruit.create("orange", 300),
                Fruit.create("banana", 800)
        );

また、ScriptContext#setWriterでWriterは差し換えることが可能です。

        context.setWriter(new StringWriter());

ファイル名も指定できるのですが、これは実はロギングのタグとして扱われるだけです。

        bindings.put(VelocityScriptEngine.FILENAME, "simple.vm");

ScriptEngine#evalには、この場合はScriptContextを渡します。

        try (BufferedReader reader =
                     new BufferedReader(new InputStreamReader(
                             getClass().getClassLoader().getResourceAsStream("simple.vm"),
                             StandardCharsets.UTF_8)
                     )) {
            StringWriter writer = (StringWriter) scriptEngine.eval(reader, context);

            System.out.println(writer.toString());
        }

結果。

こんにちは Velocity!!

Fruits:
  apple
  banana

動きましたね?

ここまでの動作についてのドキュメント?申し訳程度に、このくらいが…。
Velocity Scripting

結局、スクリプト(というかテンプレート)はStringかReaderとして渡す必要があり、ResourceLoaderを設定しようが
関係なかったりします。どうにも中途半端感が…。Propertiesを指定できることはできますが、それ以外を設定するのには
有効、という程度です。

まとめ

2017年夏にひっそりとリリースされていた、Apache Velocity 2.0を試してみました。

Velocity Toolsはまだ出ていませんし、今更Velocity感もあるとは思いますが、興味があればどうぞ的な。

個人的には、汎用のテンプレートエンジンとしては扱いやすくて割と好きなんですけどね…。

2017-11-25

HttpURLConnection#disconnectとKeep-Aliveと

あんまりHttpURLConnectionってふだん使っていないのですが(Apache HttpComponentsとかを使っていることの方が多い)、
HttpURLConnection#disconnectを呼んでないソースコードを見て、「あれ、これいいの?」と疑問に思い、ちょっと
調べてみることにしました。

ふつう、disconnect呼ぶんじゃない?と思っていたので。

で、少し調べるとこんなエントリを見たりして

HttpURLConnectionのdisconnect()を使うと他の通信が中断されてしまうことがあるので注意 - ytch's diary

これはなにかあるんだろうなぁと。

で、あらためてHttpURLConnectionのJavadocを読んでみます。

単一の要求を行う際には個々のHttpURLConnectionインスタンスが使用されますが、その背後のHTTPサーバーへのネットワーク接続は、ほかのインスタンスと透過的に共有される可能性があります。要求後、HttpURLConnectionのInputStreamまたはOutputStream上でclose()メソッドを呼び出すと、そのインスタンスに関連付けられていたネットワーク・リソースが解放される可能性がありますが、共有されている持続接続への影響はまったくありません。disconnect()メソッドを呼び出した場合、持続接続がその時点でアイドル状態になっていれば、使用していたソケットがクローズされる可能性があります。

https://docs.oracle.com/javase/jp/8/docs/api/java/net/HttpURLConnection.html

「透過的に共有」…?Keep-Aliveの話?

HttpURLConnectionは、デフォルトでKeep-Aliveが有効です(http.keepalive、http.maxConnections)。
※http.keepAlive?

その他のHTTPプロパティ

せっかくなので、ちょっと確認&ソースコードを見てみましょう。

簡単なHTTPサーバー

なにはともあれ、HTTPサーバーが必要なので簡単にGroovyで書いてみます。
simple-httpd.groovy

import java.nio.charset.StandardCharsets
import java.time.LocalDateTime
import java.util.concurrent.Executors

import com.sun.net.httpserver.HttpHandler
import com.sun.net.httpserver.HttpServer

def responseHandler = { exchange ->
    println("[${LocalDateTime.now()}] Accept: Client[$exchange.remoteAddress], Method[${exchange.requestMethod}] Url[$exchange.requestURI]")

    try {
        exchange.responseHeaders.with {
            add("Content-Type", "text/plain; charset=UTF-8")

            if (!exchange.requestHeaders['Connection'].contains('keep-alive')) {
                add("Connection", "close")
            }
        }

        def bodyText = "Hello Simple Httpd!!\n".stripMargin().getBytes(StandardCharsets.UTF_8)
        exchange.sendResponseHeaders(200, bodyText.size())
        exchange.responseBody.withStream { it.write(bodyText) }
    } catch (e) {
        e.printStackTrace()
    }
}

def server =
    HttpServer.create(new InetSocketAddress(args.length > 0 ? args[0].toInteger() : 8080), 0)
server.executor = Executors.newCachedThreadPool()
server.createContext("/", responseHandler as HttpHandler)
server.start()

println("[${LocalDateTime.now()}] SimpleJdkHttpd Startup[${server.address}]")

どのアクセスに対しても「Hello Simple Httpd!!」と返すだけの、ExecutorServiceを使った単純なHTTPサーバーです。
クライアントから「Connection: keep-alive」を受け取らなかった場合、サーバー側は「Connection: close」を送るように
しています。

これに対してアクセスする、HTTPクライアントを書いて確認してみます。

HTTPクライアント側

まずは、Maven依存関係。テストコードで確認することにしましょう。

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.0.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.8.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.19</version>
                <dependencies>
                    <dependency>
                        <groupId>org.junit.platform</groupId>
                        <artifactId>junit-platform-surefire-provider</artifactId>
                        <version>1.0.2</version>
                    </dependency>
                    <dependency>
                        <groupId>org.junit.jupiter</groupId>
                        <artifactId>junit-jupiter-engine</artifactId>
                        <version>5.0.2</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>

テストコードの雛形としては、このように。
src/test/java/org/littlewings/urlconnection/HttpUrlConnectionTest.java

package org.littlewings.urlconnection;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.nio.charset.StandardCharsets;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class HttpUrlConnectionTest {
    // ここに、テストを書く!
}

とりあえず、ふつうに書くとこんな感じですね。

    @Test
    public void simpleUsage() throws IOException {
        HttpURLConnection connection = (HttpURLConnection) URI.create("http://localhost:8080").toURL().openConnection();
        connection.setDoInput(true);

        try (InputStream is = connection.getInputStream();
             BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {

            assertThat(reader.readLine()).isEqualTo("Hello Simple Httpd!!");
        } finally {
            connection.disconnect();
        }
    }

最後に、HttpURLConnection#disconnectを呼んで終了。

では、ここでリクエストを100回送ってみます。

    @Test
    public void request100() throws IOException {
        for (int i = 0; i < 100; i++) {
            HttpURLConnection connection = (HttpURLConnection) URI.create("http://localhost:8080").toURL().openConnection();
            connection.setDoInput(true);

            try (InputStream is = connection.getInputStream();
                 BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {

                assertThat(reader.readLine()).isEqualTo("Hello Simple Httpd!!");
            } finally {
                // connection.disconnect();
            }
        }
    }

HttpURLConnection#disconnectは、意図的に外してみます。

接続状態を確認してみましょう。多くのリクエストを送っている割には、ssコマンドで現れる接続数は1です。

$ ss -an | grep 8080
tcp    LISTEN     0      50                    :::8080                 :::*     
tcp    ESTAB      0      138     ::ffff:127.0.0.1:8080   ::ffff:127.0.0.1:44692 
tcp    ESTAB      0      0       ::ffff:127.0.0.1:44692  ::ffff:127.0.0.1:8080  

終了後は、TIME-WAITに移行し、しばらくするといなくなります。

$ ss -an | grep 8080
tcp    LISTEN     0      50                    :::8080                 :::*     
tcp    TIME-WAIT  0      0       ::ffff:127.0.0.1:44692  ::ffff:127.0.0.1:8080

なるほど、Keep-Aliveが効いているようです。この結果は、HttpURLConnection#disconnectを呼び出しても変わりません。

では、Keep-Aliveを使わないようにしてみます。クライアントから「Connection: close」を送ってみるパターンと、システムプロパティ「http.keepalive」で
無効にするパターンで書いてみましょう。

    @Test
    public void request100DisableKeepAlive1() throws IOException {
        for (int i = 0; i < 100; i++) {
            HttpURLConnection connection = (HttpURLConnection) URI.create("http://localhost:8080").toURL().openConnection();

            connection.addRequestProperty("Connection", "close");

            connection.setDoInput(true);

            try (InputStream is = connection.getInputStream();
                 BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {

                assertThat(reader.readLine()).isEqualTo("Hello Simple Httpd!!");
            } finally {
                // connection.disconnect();
            }
        }
    }

    @Test
    public void request100DisableKeepAlive2() throws IOException {
        System.setProperty("http.keepAlive", "false");

        for (int i = 0; i < 100; i++) {
            HttpURLConnection connection = (HttpURLConnection) URI.create("http://localhost:8080").toURL().openConnection();

            connection.setDoInput(true);

            try (InputStream is = connection.getInputStream();
                 BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {

                assertThat(reader.readLine()).isEqualTo("Hello Simple Httpd!!");
            } finally {
                // connection.disconnect();
            }
        }
    }

ん?「http.keepAlive」??
※あとで

ともに、ssコマンドで見ると大量のTIME-WAIT…というか接続ができていることがわかります。Keep-Aliveが無効になりましたと。

$ ss -an | grep 8080 | wc -l
104

もう少し中身を

では、もう少し中身を追ってみましょう。そもそも、HttpURLConnection#disconnectってなんなのでしょう。

InputStreamを使って入ればそれを閉じ、そうでなければSocketをクローズするようです。
http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/31bc1a681b51/src/share/classes/sun/net/www/protocol/http/HttpURLConnection.java#l2840
http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/31bc1a681b51/src/share/classes/sun/net/www/http/HttpClient.java#l1050

この時、Keep-Aliveが有効であればHttpClient#closeIdleConnectionが呼び出されます。

                // un-synchronized
                boolean ka = hc.isKeepingAlive();

                try {
                    inputStream.close();
                } catch (IOException ioe) { }

                // if the connection is persistent it may have been closed
                // or returned to the keep-alive cache. If it's been returned
                // to the keep-alive cache then we would like to close it
                // but it may have been allocated

                if (ka) {
                    hc.closeIdleConnection();
                }

HttpClient#closeIdleConnectionでは、URLをキーにしてKeepAliveCacheからHttpClientを取得し、nullでなければSocketをクローズしようとします。
http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/31bc1a681b51/src/share/classes/sun/net/www/http/HttpClient.java#l450

ここが、

disconnect()メソッドを呼び出した場合、持続接続がその時点でアイドル状態になっていれば、使用していたソケットがクローズされる可能性があります。

https://docs.oracle.com/javase/jp/8/docs/api/java/net/HttpURLConnection.html

に該当する部分ですね。

KeepAliveCacheとは、HttpURLConnectionの実装が内部的にstaticなフィールドとして保持しています。
http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/bedc29a6d074/src/share/classes/sun/net/www/http/HttpClient.java#l93

KeepAliveCacheはHashMapを継承したクラスで、
http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/31bc1a681b51/src/share/classes/sun/net/www/http/KeepAliveCache.java#l41

パッと見、URLをキーにしてKeepAliveCacheを利用しているように見えるので、一見キーはURLなのかなぁとも思うのですが
http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/bedc29a6d074/src/share/classes/sun/net/www/http/HttpClient.java#l407

    protected synchronized void putInKeepAliveCache() {
        if (inCache) {
            assert false : "Duplicate put to keep alive cache";
            return;
        }
        inCache = true;
        kac.put(url, null, this);
    }

(接続時にHttpClientを使いまわしているところ)
http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/31bc1a681b51/src/share/classes/sun/net/www/http/HttpClient.java#l303

        if (useCache) {
            ret = kac.get(url, null);
            if (ret != null && httpuc != null &&
                httpuc.streaming() &&
                httpuc.getRequestMethod() == "POST") {
                if (!ret.available()) {
                    ret.inCache = false;
                    ret.closeServer();
                    ret = null;
                }
            }

実際にKeepAliveCacheのキーとして機能しているのはKeepAliveKeyというクラスで、プロトコル、ホスト、ポートを比較しているので、結局接続先(とプロトコル)単位で
管理しているということになります。
http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/31bc1a681b51/src/share/classes/sun/net/www/http/KeepAliveCache.java#l321

    @Override
    public boolean equals(Object obj) {
        if ((obj instanceof KeepAliveKey) == false)
            return false;
        KeepAliveKey kae = (KeepAliveKey)obj;
        return host.equals(kae.host)
            && (port == kae.port)
            && protocol.equals(kae.protocol)
            && this.obj == kae.obj;
    }

よって、URLパスを変えてアクセスしたりしても、Keep-Aliveが有効な場合は接続が増えたりすることはありません。

    @Test
    public void request100SeparateUrl() throws IOException {
        for (int i = 0; i < 100; i++) {
            HttpURLConnection connection = (HttpURLConnection) URI.create("http://localhost:8080/" + i).toURL().openConnection();
            connection.setDoInput(true);

            try (InputStream is = connection.getInputStream();
                 BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {

                assertThat(reader.readLine()).isEqualTo("Hello Simple Httpd!!");
            } finally {
                // connection.disconnect();
            }
        }
    }

確認。

$ ss -an | grep 8080
tcp    LISTEN     0      50                    :::8080                 :::*     
tcp    ESTAB      0      0       ::ffff:127.0.0.1:45490  ::ffff:127.0.0.1:8080  
tcp    ESTAB      0      0       ::ffff:127.0.0.1:8080   ::ffff:127.0.0.1:45490 

$ ss -an | grep 8080
tcp    LISTEN     0      50                    :::8080                 :::*     
tcp    TIME-WAIT  0      0       ::ffff:127.0.0.1:45490  ::ffff:127.0.0.1:8080

Keep-Aliveのタイムアウト値は、デフォルトの5000ミリ秒か、サーバーから返ってきたKeep-Aliveヘッダのtimeout値が使われます。
http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/31bc1a681b51/src/share/classes/sun/net/www/http/HttpClient.java#l811
http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/31bc1a681b51/src/share/classes/sun/net/www/http/KeepAliveCache.java#l123

KeepAliveCacheから取得する時に、すでにタイムアウト値を越えていた場合、その接続は破棄されます。
http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/31bc1a681b51/src/share/classes/sun/net/www/http/KeepAliveCache.java#l262

KeepAliveCacheから取得できたということは、利用可能なHttpClient(接続)であるということになります。

また、定期的にクリーンアップも行われており
http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/31bc1a681b51/src/share/classes/sun/net/www/http/KeepAliveCache.java#l199

この処理は、KeepAliveCacheに登録される時に、接続ごとにスレッドが割り当てられて監視されるようです。
http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/31bc1a681b51/src/share/classes/sun/net/www/http/KeepAliveCache.java#l112

で、ここでちょっと気になることがあって、HttpURLConnectionはdisconnect時にKeep-Aliveが有効であれば、アイドルなコネクションを
切断しようとします。
http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/31bc1a681b51/src/share/classes/sun/net/www/protocol/http/HttpURLConnection.java#l2890

                if (ka) {
                    hc.closeIdleConnection();
                }

http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/31bc1a681b51/src/share/classes/sun/net/www/http/HttpClient.java#l452

    public void closeIdleConnection() {
        HttpClient http = kac.get(url, null);
        if (http != null) {
            http.closeServer();
        }
    }

でも、KeepAliveCacheの実装を見ていると、これむしろアクティブな接続を切っているのでは…?と不思議に思ったのですが、InputStreamをクローズしたり
すると、ここへは到達しなくなるようです。
http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/31bc1a681b51/src/share/classes/sun/net/www/protocol/http/HttpURLConnection.java#l3474

なお、HttpURLConnection#disconnectのこのあたりの説明が書いているようです。

If we have an input stream this means we received a response from the server.

That stream may have been read to EOF and dependening on the stream type may already be closed or the http client may be returned to the keep-alive cache.

If the http client has been returned to the keep-alive cache it may be closed (idle timeout) or may be allocated to another request.

In other to avoid timing issues we close the input stream which will either close the underlying connection or return the client to the cache.

If there's a possibility that the client has been returned to the cache (ie: stream is a keep alive stream or a chunked input stream) then we remove an idle connection to the server.

Note that this approach can be considered an approximation in that we may close a different idle connection to that used by the request.

Additionally it's possible that we close two connections - the first becuase it wasn't an EOF (and couldn't be hurried) - the second, another idle connection to the same server.

The is okay because "disconnect" is an indication that the application doesn't intend to access this http server for a while.

http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/31bc1a681b51/src/share/classes/sun/net/www/protocol/http/HttpURLConnection.java#l2848

で、結局disconnectは呼んだ方がいいのか?

最初に戻ると、HttpURLConnection#disconnectは呼んだ方が良いのかどうかということになりますが、HttpURLConnectionについて
言えば、Keep-Aliveで使っているアイドルなコネクションの破棄(放っておいてもバックグラウンドで破棄される)と
内部的な情報をnullにするくらいの差しか出ないようなので、情報の読み込み、書き出しに使うInputStream/OutputStreamを
きちんとcloseしておけば問題ないような気がします。

URLConnectionの作法に則る(?)のであれば、disconnectした方がよいとは思いますが。

ただまあ、単発のHTTP通信に使いたい程度であれば、APIドキュメントのとおりStreamをcloseしておけば最低限OKということに
なるのでしょうね。

要求後、HttpURLConnectionのInputStreamまたはOutputStream上でclose()メソッドを呼び出すと、そのインスタンスに関連付けられていたネットワーク・リソースが解放される可能性がありますが、共有されている持続接続への影響はまったくありません。

https://docs.oracle.com/javase/jp/8/docs/api/java/net/HttpURLConnection.html

オマケ

Keep-Aliveの有効/無効はシステムプロパティで制御しますが、以下のソースコードを見ると「http.keepAlive」に見えるのですが、これいかに?
http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/31bc1a681b51/src/share/classes/sun/net/www/http/HttpClient.java#l160

ドキュメントは、「http.keepalive」のような…。