Ramda で入れ子のオブジェクトをフラットにする
Ramda を使って入れ子になったオブジェクトをフラットにする処理を考えてみました。
ソースは http://github.com/fits/try_samples/tree/master/blog/20200216/
サンプル1
まずは、以下のように入れ子になったオブジェクトを使って
{ item: { name: 'item-1', details: { color: 'white', size: 'L' } }, num: 5 }
下記のように変換してみます。
{ name: 'item-1', color: 'white', size: 'L', num: 5 }
方法はいろいろありそうですが、ここではオブジェクトの値部分を単一要素のオブジェクト {<項目名>: <値>}
へ変換してから R.mergeAll
でマージするようにしてみました。
sample1.js
const R = require('ramda') const data = { item: { name: 'item-1', details: { color: 'white', size: 'L' } }, num: 5 } // Object の判定 const isObject = R.pipe(R.type, R.equals('Object')) // 値部分を単一要素のオブジェクトへ変換 const valuesToObjOf = R.pipe( R.mapObjIndexed( R.ifElse( isObject, v => valuesToObjOf(v), R.flip(R.objOf) ) ), R.values, R.flatten ) const flatObj = R.pipe( valuesToObjOf, R.mergeAll ) console.log( valuesToObjOf(data) ) console.log('------') console.log( flatObj(data) )
値部分がオブジェクトの場合は再帰的に処理するようにしていますが、その判定に R.is(Object)
を使うと Array
等も true
になってしまい都合が悪いので、R.type
の結果が Object
か否かで判定するようにしています。
実行結果
> node sample1.js [ { name: 'item-1' }, { color: 'white' }, { size: 'L' }, { num: 5 } ] ------ { name: 'item-1', color: 'white', size: 'L', num: 5 }
サンプル2
上記の処理だと項目名が重複した際に不都合が生じるため、項目名を連結するように valuesToObjOf
を改良してみました。
sample2.js
・・・ const valuesToObjOf = (obj, prefix = '') => R.pipe( R.mapObjIndexed( R.ifElse( isObject, (v, k) => valuesToObjOf(v, `${prefix}${k}_`), (v, k) => R.objOf(`${prefix}${k}`, v) ) ), R.values, R.flatten )(obj) const flatObj = R.pipe( valuesToObjOf, R.mergeAll ) console.log( flatObj(data) )
実行結果
> node sample2.js { item_name: 'item-1', item_details_color: 'white', item_details_size: 'L', num: 5 }
gRPC Server Reflection のクライアント処理
gRPC Server Reflection を呼び出す処理を Node.js で実装してみました。
ソースは http://github.com/fits/try_samples/tree/master/blog/20191008/
事前準備(サーバー実装)
まずは、gRPC Server Reflection を有効にしたサーバー処理を用意します。 Node.js での実装は無理そうだったので(未実装らしい) Go 言語で実装します。
protoc コマンドをインストールした後、Go 言語用の protoc プラグインである protoc-gen-go
をインストールします。
protoc-gen-go インストール
> go get -u github.com/golang/protobuf/protoc-gen-go
google.golang.org/grpc
等のライブラリをビルド時に自動ダウンロードするように、Go のモジュールファイル go.mod
を用意しておきます。
go.mod の作成
> go mod init sample
proto ファイルを作成してインターフェースを定義します。
今回は、go_package
を使って Go 用のパッケージを別途定義してみました。
proto/item/item.proto
syntax = "proto3"; import "google/protobuf/empty.proto"; option go_package = "sample/proto/item"; package item; message ItemRequest { string item_id = 1; } message AddItemRequest { string item_id = 1; uint64 price = 2; } message Item { string item_id = 1; uint64 price = 2; } service ItemService { rpc GetItem(ItemRequest) returns (Item); rpc AddItem(AddItemRequest) returns (google.protobuf.Empty); }
protoc コマンドで Go 用のコードを生成します。
protoc によるコード生成
> protoc -I=proto --go_out=plugins=grpc,paths=source_relative:./proto proto/item/item.proto
サーバー処理を実装します。
Server Reflection を有効化するには google.golang.org/grpc/reflection
を import して reflection.Register
を適用するだけなので簡単です。
server/main.go
package main import ( "context" "fmt" "net" "log" "google.golang.org/grpc" "google.golang.org/grpc/reflection" empty "github.com/golang/protobuf/ptypes/empty" pb "sample/proto/item" ) type Server struct { Items map[string]pb.Item } func (s *Server) GetItem(ctx context.Context, req *pb.ItemRequest) (*pb.Item, error) { log.Println("call GetItem: ", req) item, ok := s.Items[req.GetItemId()] if !ok { return nil, fmt.Errorf("item not found: %s", req.GetItemId()) } return &item, nil } func (s *Server) AddItem(ctx context.Context, req *pb.AddItemRequest) (*empty.Empty, error) { log.Println("call AddItem: ", req) s.Items[req.GetItemId()] = pb.Item{ItemId: req.GetItemId(), Price: req.GetPrice()} return &empty.Empty{}, nil } func main() { address := ":50051" listen, err := net.Listen("tcp", address) if err != nil { log.Fatalf("error: %v", err) } s := grpc.NewServer() pb.RegisterItemServiceServer(s, &Server{Items: make(map[string]pb.Item)}) // gRPC Server Reflection 有効化 reflection.Register(s) log.Println("server start:", address) if err := s.Serve(listen); err != nil { log.Fatalf("failed serve: %v", err) } }
上記を実行しておきます。(go run でビルドも実施されます)
初回実行時は google.golang.org/grpc
等の依存ライブラリが自動的にダウンロードされます。
実行
> go run server/main.go ・・・ 2019/10/06 22:00:00 server start: :50051
サーバー側のファイル構成は以下のようになっています。
ファイル構成
- go.mod
- go.sum
- proto
- item
- item.proto
- item.pb.go
- item
- server
- main.go
gRPC Server Reflection クライアント処理
それでは、本題の gRPC Server Reflection を呼び出す処理を実装していきます。
準備
どのように実装すべきか分からなかったので、とりあえず今回は https://github.com/grpc/grpc/blob/master/src/proto/grpc/reflection/v1alpha/reflection.proto を使う事にしました。
まずは、この proto ファイルをローカルにダウンロードしておきます。
proto ファイルのダウンロード
> curl -O https://raw.githubusercontent.com/grpc/grpc/master/src/proto/grpc/reflection/v1alpha/reflection.proto
Node.js で gRPC 処理を実装するため grpc
と proto-loader
をインストールしておきます。
grpc と proto-loader のインストール
> npm install grpc @grpc/proto-loader
(a) サービスのリスト取得
はじめに、サービス名をリストアップする処理を実装してみます。
reflection.proto
を見てみると、以下のように ServerReflectionInfo
メソッドの引数である ServerReflectionRequest
の message_request
にどのフィールドを指定するかで取得内容が変わるようになっています。
サービスのリストを取得するには list_services
フィールドを使えば良さそうです。
reflection.proto の該当箇所
・・・ package grpc.reflection.v1alpha; service ServerReflection { rpc ServerReflectionInfo(stream ServerReflectionRequest) returns (stream ServerReflectionResponse); } message ServerReflectionRequest { string host = 1; oneof message_request { string file_by_filename = 3; string file_containing_symbol = 4; ExtensionRequest file_containing_extension = 5; string all_extension_numbers_of_type = 6; string list_services = 7; } } ・・・
ServerReflectionInfo
メソッドは引数と戻り値の両方に stream
が指定されている双方向ストリーミング RPC となっているため、以下のように write
でリクエストメッセージを送信して on('data', ・・・)
でレスポンスメッセージを取得する事になります。
また、end
でストリーミング処理を終了します。
list_services.js
const grpc = require('grpc') const protoLoader = require('@grpc/proto-loader') const pd = protoLoader.loadSync('reflection.proto', { keepCase: true, defaults: true }) const proto = grpc.loadPackageDefinition(pd).grpc.reflection.v1alpha const client = new proto.ServerReflection( '127.0.0.1:50051', grpc.credentials.createInsecure() ) const call = client.ServerReflectionInfo() call.on('error', err => { // ストリーミングの終了 call.end() console.error(err) }) // レスポンスメッセージの受信 call.on('data', res => { // ストリーミングの終了 call.end() // サービス名の出力 res.list_services_response.service.forEach(s => { console.log(s.name) }) }) // リクエストメッセージの送信 call.write({host: 'localhost', list_services: ''})
実行結果は以下の通り、サービス名を出力できました。
実行結果
> node list_services.js grpc.reflection.v1alpha.ServerReflection item.ItemService
(b) サービスのインターフェース定義取得
次に、サービスの内容(インターフェース定義)を取得してみます。
こちらは、リクエストメッセージの file_containing_symbol
フィールドにサービス名を指定する事で取得できます。
ただ、レスポンスメッセージの該当フィールドの内容が reflection.proto のコメントにあるように FileDescriptorProto
をシリアライズした結果 ※ (の配列)となっている点に注意が必要です。
※ bytes 型は Node.js では Buffer として扱われる
reflection.proto の該当箇所
・・・ message FileDescriptorResponse { // Serialized FileDescriptorProto messages. We avoid taking a dependency on // descriptor.proto, which uses proto2 only features, by making them opaque // bytes instead. repeated bytes file_descriptor_proto = 1; } ・・・
FileDescriptorProto へのデシリアライズに関しては試行錯誤しましたが、最も手軽そうな protobufjs/ext/descriptor
の FileDescriptorProto.decode
を使う事にしました。
load_symbol.js
const grpc = require('grpc') const protoLoader = require('@grpc/proto-loader') const descriptor = require('protobufjs/ext/descriptor') const serviceName = process.argv[2] ・・・ const call = client.ServerReflectionInfo() call.on('error', err => { call.end() console.error(err) }) call.on('data', res => { call.end() res.file_descriptor_response.file_descriptor_proto // FileDescriptorProto デシリアライズ .map(buf => descriptor.FileDescriptorProto.decode(buf)) .forEach(d => { // JSON 化して出力 console.log(JSON.stringify(d, null, 2)) }) }) call.write({host: 'localhost', file_containing_symbol: serviceName})
item.ItemService のサービス内容を取得してみた結果です。 go_package の内容も含め、問題なく取得できているようです。
実行結果
> node load_symbol.js item.ItemService { "name": "item/item.proto", "package": "item", "dependency": [ "google/protobuf/empty.proto" ], "messageType": [ { "name": "ItemRequest", "field": [ { "name": "item_id", "number": 1, "label": "LABEL_OPTIONAL", "type": "TYPE_STRING", "jsonName": "itemId" } ] }, { "name": "AddItemRequest", "field": [ { "name": "item_id", "number": 1, "label": "LABEL_OPTIONAL", "type": "TYPE_STRING", "jsonName": "itemId" }, { "name": "price", "number": 2, "label": "LABEL_OPTIONAL", "type": "TYPE_UINT64", "jsonName": "price" } ] }, { "name": "Item", "field": [ { "name": "item_id", "number": 1, "label": "LABEL_OPTIONAL", "type": "TYPE_STRING", "jsonName": "itemId" }, { "name": "price", "number": 2, "label": "LABEL_OPTIONAL", "type": "TYPE_UINT64", "jsonName": "price" } ] } ], "service": [ { "name": "ItemService", "method": [ { "name": "GetItem", "inputType": ".item.ItemRequest", "outputType": ".item.Item" }, { "name": "AddItem", "inputType": ".item.AddItemRequest", "outputType": ".google.protobuf.Empty" } ] } ], "options": { "goPackage": "sample/proto/item" }, "syntax": "proto3" }
(c) 全サービスのインターフェース定義取得
最後に、全サービスのインターフェース定義を取得する処理を実装してみます。
双方向ストリーミング RPC を活用して、サービスのリスト取得とサービス内容の取得を同じストリーミング上で行ってみました。
load_symbols.js
const grpc = require('grpc') const protoLoader = require('@grpc/proto-loader') const descriptor = require('protobufjs/ext/descriptor') ・・・ const call = client.ServerReflectionInfo() call.on('error', err => { call.end() console.error(err) }) let count = 0 call.on('data', res => { // サービスのリスト取得時の処理 if (res.list_services_response) { const names = res.list_services_response.service.map(s => s.name) count = names.length names.forEach(name => call.write({host: 'localhost', file_containing_symbol: name}) ) } // サービスのインターフェース定義取得時の処理 else if (res.file_descriptor_response) { if (--count == 0) { // インターフェース定義を全て取得したら終了 call.end() } res.file_descriptor_response.file_descriptor_proto .map(buf => descriptor.FileDescriptorProto.decode(buf)) .forEach(d => { console.log(JSON.stringify(d, null, 2)) }) } else { console.log(res) call.end() } }) call.write({host: 'localhost', list_services: ''})
実行結果は以下の通りです。
実行結果
> node load_symbols.js { "name": "grpc_reflection_v1alpha/reflection.proto", "package": "grpc.reflection.v1alpha", ・・・ "service": [ { "name": "ServerReflection", "method": [ { "name": "ServerReflectionInfo", "inputType": ".grpc.reflection.v1alpha.ServerReflectionRequest", "outputType": ".grpc.reflection.v1alpha.ServerReflectionResponse", "clientStreaming": true, "serverStreaming": true } ] } ], "syntax": "proto3" } { "name": "item/item.proto", "package": "item", "dependency": [ "google/protobuf/empty.proto" ], ・・・ "service": [ { "name": "ItemService", "method": [ { "name": "GetItem", "inputType": ".item.ItemRequest", "outputType": ".item.Item" }, { "name": "AddItem", "inputType": ".item.AddItemRequest", "outputType": ".google.protobuf.Empty" } ] } ], "options": { "goPackage": "sample/proto/item" }, "syntax": "proto3" }
備考
別の実装例として、処理毎に個別のストリーミングで処理するようにして Promise 化してみました。
load_symbols2.js
const grpc = require('grpc') const protoLoader = require('@grpc/proto-loader') const descriptor = require('protobufjs/ext/descriptor') ・・・ const merge = a => b => Object.fromEntries([a, b].map(Object.entries).flat()) const serverReflectionInfo = (f, g) => new Promise((resolve, revoke) => { const call = client.ServerReflectionInfo() call.on('error', err => { call.end() revoke(err) }) call.on('data', res => { call.end() resolve( g(res) ) }) call.write( f({host: 'localhost'}) ) }) // サービスのリスト取得 const listServices = () => serverReflectionInfo( merge({list_services: ''}), res => res.list_services_response.service.map(s => s.name) ) // サービスのインターフェース定義取得 const loadSymbol = name => serverReflectionInfo( merge({file_containing_symbol: name}), res => res.file_descriptor_response.file_descriptor_proto .map(buf => descriptor.FileDescriptorProto.decode(buf)) ) listServices() .then(names => Promise.all(names.map(loadSymbol)) ) .then(ds => ds.flat()) .then(ds => ds.forEach(d => console.log(JSON.stringify(d, null, 2)))) .catch(err => console.error(err))
実行結果
> node load_symbols2.js { "name": "grpc_reflection_v1alpha/reflection.proto", "package": "grpc.reflection.v1alpha", ・・・ "service": [ { "name": "ServerReflection", "method": [ { "name": "ServerReflectionInfo", "inputType": ".grpc.reflection.v1alpha.ServerReflectionRequest", "outputType": ".grpc.reflection.v1alpha.ServerReflectionResponse", "clientStreaming": true, "serverStreaming": true } ] } ], "syntax": "proto3" } { "name": "item/item.proto", "package": "item", "dependency": [ "google/protobuf/empty.proto" ], ・・・ "service": [ { "name": "ItemService", "method": [ { "name": "GetItem", "inputType": ".item.ItemRequest", "outputType": ".item.Item" }, { "name": "AddItem", "inputType": ".item.AddItemRequest", "outputType": ".google.protobuf.Empty" } ] } ], "options": { "goPackage": "sample/proto/item" }, "syntax": "proto3" }
Pulumi を使って Kubernetes へ CRD を登録
Pulumi は JavaScript・Python・Go のようなプログラミング言語で Infrastructure as Code するためのツールです。
今回は、この Pulumi を使って Kubernetes(k3s を使用)へカスタムリソースを登録してみます。
ソースは http://github.com/fits/try_samples/tree/master/blog/20190825/
はじめに
今回は、k3s(Lightweight Kubernetes) がインストール済みの Ubuntu 環境を使います。※
※ ただし、k3s の前に microk8s をインストールしたりしているので クリーンな環境とは言えないかもしれない (Istio と Knative のために Helm 等をインストールしていたりもする)
Pulumi を以下のようにインストールします。
Pulumi インストール例
$ curl -fsSL https://get.pulumi.com | sh
$HOME/.pulumi
ディレクトリへ各種ファイルが展開され、.bashrc
ファイルへ PATH の設定が追加されました。
なお、このままだと pulumi コマンド実行時に kubeconfig を参照できないようだったので、とりあえず /etc/rancher/k3s/k3s.yaml
を $HOME/.kube/config
ファイルとしてコピーし chown しています。
動作確認
$ pulumi version v0.17.28
Kubernetes へ CRD を登録(Node.js 使用)
準備
プロジェクトを作成する前に、まずは Pulumi でログインを実施しておきます。
今回はローカル環境の Kubernetes を使うので --local
を指定して login を実施しました。
ログイン
$ pulumi login --local
プロジェクト作成
適当なディレクトリを用意し、テンプレートを指定してプロジェクトのひな型を作成します。
今回は Kubernetes を対象とした Node.js 用のプロジェクトを作成するため kubernetes-javascript
を指定しました。
プロジェクト作成
$ mkdir sample $ cd sample $ pulumi new kubernetes-javascript ・・・ project name: (sample) project description: (A minimal Kubernetes JavaScript Pulumi program) Created project 'sample' stack name: (dev) Enter your passphrase to protect config/secrets: Re-enter your passphrase to confirm: Created stack 'dev' Enter your passphrase to unlock config/secrets (set PULUMI_CONFIG_PASSPHRASE to remember): Installing dependencies... ・・・
実装
上記で生成された index.js
ファイルに Kubernetes へ登録する内容を実装していきます。
今回は下記のようなカスタムリソースの登録を実装します。
例. カスタムリソース登録内容
apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: items.example.com spec: group: example.com version: v1alpha1 scope: Namespaced names: kind: Item plural: items singular: item preserveUnknownFields: false validation: openAPIV3Schema: type: object properties: spec: type: object properties: value: type: integer note: type: string --- apiVersion: "example.com/v1alpha1" kind: Item metadata: name: item1 spec: value: 100 note: sample item 1 --- apiVersion: "example.com/v1alpha1" kind: Item metadata: name: item2 spec: value: 20 note: sample item 2
上記のカスタムリソースを実装したコードが以下です。
Pulumi でカスタムリソースを定義する場合、@pulumi/kubernetes
の CustomResourceDefinition
と CustomResource
を使えば良さそうです。
YAML と同等の内容を JavaScript の Object で表現して、CustomResourceDefinition 等のコンストラクタの第 2引数として渡すだけです。
index.js (カスタムリソース登録内容の実装)
'use strict' const k8s = require('@pulumi/kubernetes') const capitalize = s => `${s[0].toUpperCase()}${s.slice(1)}` const crdName = 'item' const crdGroup = 'example.com' const crdVersion = 'v1alpha1' const props = { value: 'integer', note: 'string' } const items = [ { name: 'item1', value: 100, note: 'sample item 1' }, { name: 'item2', value: 20, note: 'sample item 2' } ] const crdKind = capitalize(crdName) const crdPlural = `${crdName}s` new k8s.apiextensions.v1beta1.CustomResourceDefinition(crdName, { metadata: { name: `${crdPlural}.${crdGroup}` }, spec: { group: crdGroup, version: crdVersion, scope: 'Namespaced', names: { kind: crdKind, plural: crdPlural, singular: crdName }, preserveUnknownFields: false, validation: { openAPIV3Schema: { type: 'object', properties: { spec: { type: 'object', properties: Object.fromEntries( Object.entries(props).map(([k, v]) => [k, { type: v }] ) ) } } } } } }) items.forEach(it => new k8s.apiextensions.CustomResource(it.name, { apiVersion: `${crdGroup}/${crdVersion}`, kind: crdKind, metadata: { name: it.name }, spec: Object.fromEntries( Object.keys(props).map(k => [k, it[k]]) ) }) )
デプロイ
pulumi up でデプロイします。
デプロイ
$ pulumi up ・・・ Previewing update (dev): Type Name P + pulumi:pulumi:Stack sample-dev c + tq kubernetes:example.com:Item item2 c + tq kubernetes:example.com:Item item1 c + mq kubernetes:apiextensions.k8s.io:CustomResourceDefinition item c Resources: + 4 to create Do you want to perform this update? yes > no details
Do you want to perform this update?
で yes
を選択すると実際にデプロイが実施されます。
Do you want to perform this update? yes Updating (dev): Type Name S + pulumi:pulumi:Stack sample-dev c + tq kubernetes:apiextensions.k8s.io:CustomResourceDefinition item c + tq kubernetes:example.com:Item item1 c + mq kubernetes:example.com:Item item2 c Resources: + 4 created ・・・
ちなみに、details を選ぶと登録内容(YAML)を確認できます。
正常に登録されたか、kubectl コマンドで確認してみます。(k3s の一般的な環境では k3s kubectl とする必要があるかもしれません)
CRD の確認
$ kubectl get crd | grep items items.example.com 2019-08-14T08:57:22Z
カスタムリソースの確認
$ kubectl get item NAME AGE item1 13m item2 13m
カスタムリソース詳細1
$ kubectl describe item item1 Name: item1 Namespace: default Labels: app.kubernetes.io/managed-by=pulumi Annotations: kubectl.kubernetes.io/last-applied-configuration: {"apiVersion":"example.com/v1alpha1","kind":"Item","metadata":{"labels":{"app.kubernetes.io/managed-by":"pulumi"},"name":"item1"},"spec":{... API Version: example.com/v1alpha1 Kind: Item Metadata: Creation Timestamp: 2019-08-14T08:57:22Z Generation: 1 Resource Version: 19763 Self Link: /apis/example.com/v1alpha1/namespaces/default/items/item1 UID: 87503274-be71-11e9-aeea-025c19d6acb9 Spec: Note: sample item 1 Value: 100 Events: <none>
カスタムリソース詳細2
$ kubectl describe item item2 Name: item2 Namespace: default Labels: app.kubernetes.io/managed-by=pulumi Annotations: kubectl.kubernetes.io/last-applied-configuration: {"apiVersion":"example.com/v1alpha1","kind":"Item","metadata":{"labels":{"app.kubernetes.io/managed-by":"pulumi"},"name":"item2"},"spec":{... API Version: example.com/v1alpha1 Kind: Item Metadata: Creation Timestamp: 2019-08-14T08:57:22Z Generation: 1 Resource Version: 19764 Self Link: /apis/example.com/v1alpha1/namespaces/default/items/item2 UID: 875af773-be71-11e9-aeea-025c19d6acb9 Spec: Note: sample item 2 Value: 20 Events: <none>
問題なく登録できているようです。
アンデプロイ
デプロイ内容を削除(アンデプロイ)する場合は destroy を実行します。
アンデプロイ
$ pulumi destroy ・・・ Do you want to perform this destroy? yes Destroying (dev): Type Name S - pulumi:pulumi:Stack sample-dev d - tq kubernetes:apiextensions.k8s.io:CustomResourceDefinition item d - tq kubernetes:example.com:Item item2 d - mq kubernetes:example.com:Item item1 d Resources: - 4 deleted ・・・
Keras.js によるランドマーク検出の Web アプリケーション化2
前回 はランドマーク検出対象の画像サイズを固定(256x256)しましたが、今回は任意の画像サイズに対応できるように改造してみます。
ソースは http://github.com/fits/try_samples/tree/master/blog/20190506/
可変サイズ対応
ドラッグアンドドロップした画像のサイズに合わせてランドマーク検出を実施するようにしてみます。(ファイル構成などは 前回 と同じ)
ただ、Keras.js を通常とは異なる使い方をするため、何らかの不都合が生じるかもしれませんし、別バージョンでは動作しないかもしれません。
(a) UI 処理(src/app.js)
canvas のサイズを画像サイズに合わせて変更し、ランドマーク検出処理へ画像サイズ(幅、高さ)の情報を渡すようにします。
src/app.js
・・・ const loadImage = url => new Promise(resolve => { const img = new Image() img.addEventListener('load', () => { // canvas のサイズを画像サイズに合わせて変更 canvas.width = img.width canvas.height = img.height ctx.clearRect(0, 0, canvas.width, canvas.height) ctx.drawImage(img, 0, 0) const d = ctx.getImageData(0, 0, canvas.width, canvas.height) resolve({width: img.width, height: img.height, data: imgToArray(d)}) }) img.src = url }) ・・・ const ready = () => { ・・・ canvas.addEventListener('drop', ev => { ev.preventDefault() canvas.classList.remove('dragging') const file = ev.dataTransfer.files[0] if (imageTypes.includes(file.type)) { clearLandmarksInfo() const reader = new FileReader() reader.onload = ev => { loadImage(reader.result) .then(d => { detectDialog.showModal() // 画像のサイズ情報を追加 worker.postMessage({type: 'predict', input: d.data, width: d.width, height: d.height}) }) } reader.readAsDataURL(file) } }, false) } ・・・
(b) ランドマーク検出処理(src/worker.js)
通常は(Keras.js の)モデル内でレイヤー毎の入出力の形状が固定化されているので、このままでは任意の画像サイズには対応できません。
そこで、検出処理の度に入出力の形状を強制的にリセットする処理(以下)を加える事で可変サイズに対応します。
- (1) 入力データの形状(画像サイズ)を変更
- (2) 各レイヤーの出力形状をクリア
- (3) inputTensorsMap のリセット
src/worker.js
・・・ onmessage = ev => { switch (ev.data.type) { ・・・ case 'predict': const inputLayerName = model.inputLayerNames[0] const outputLayerName = model.outputLayerNames[0] const w = ev.data.width const h = ev.data.height // (1) 入力データの形状(画像サイズ)を変更 const inputLayer = model.modelLayersMap.get(inputLayerName) inputLayer.shape[0] = h inputLayer.shape[1] = w // (2) 各レイヤーの出力形状をクリア model.modelLayersMap.forEach(n => { if (n.outputShape) { n.outputShape = null n.imColsMat = null } }) // (3) inputTensorsMap のリセット model.resetInputTensors() const data = {} data[inputLayerName] = ev.data.input Promise.resolve(model.predict(data)) .then(r => { const shape = model.modelLayersMap.get(outputLayerName) .output.tensor.shape return new KerasJS.Tensor(r[outputLayerName], shape) }) .then(detectLandmarks) .then(r => postMessage({type: ev.data.type, output: r}) ) .catch(err => { console.log(err) postMessage({type: ev.data.type, error: err.message}) }) break } }
動作確認
(1) 画像サイズ 128x128
(2) 画像サイズ 307x307
(3) 画像サイズ 100x128
(4) 画像サイズ 200x256
Keras.js によるランドマーク検出の Web アプリケーション化
前回の 「CNN でランドマーク検出」 の学習済みモデルを Keras.js を使って Web ブラウザ上で実行できるようにしてみます。
ソースは http://github.com/fits/try_samples/tree/master/blog/20190331/
準備
npm で Keras.js をインストールします。
Keras.js インストール
> npm install --save keras-js
Keras.js に含まれている encoder.py
スクリプトを使って、Python の Keras で学習したモデル(model/cnn_landmark_400.h5
)を Keras.js 用に変換します。
モデルファイル(HDF5 形式)を Keras.js 用に変換
> python node_modules/keras-js/python/encoder.py model/cnn_landmark_400.h5
生成された .bin ファイル(model/cnn_landmark_400.bin
)のパス(URL)を KerasJS.Model
へ指定して使う事になります。
ついでに、webpack もインストールしておきます。(webpack コマンドを使うには webpack-cli も必要)
webpack インストール
> npm install --save-dev webpack webpack-cli
Web アプリケーション作成
今回、作成する Web アプリケーションのファイル構成は以下の通りです。
- index.html
- js/bundle_app.js
- js/bundle_worker.js
- model/cnn_landmark_400.bin
処理は全て Web ブラウザ上で実行するようにし、Keras.js の処理(今回のランドマーク検出)はそれなりに重いので Web Worker として実行します。
index.html の内容は以下の通りです。
index.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <style type="text/css"> canvas { border: 1px solid; } canvas.dragging { border: 3px solid red; } table { text-align: center; border-collapse: collapse; } table th, table td { border: 1px solid; padding: 8px; } </style> </head> <body> <dialog id="load-dialog">loading model ...</dialog> <dialog id="detect-dialog">detecting landmarks ...</dialog> <dialog id="error-dialog">ERROR</dialog> <div> <canvas width="256" height="256"></canvas> </div> <br> <div> <div id="landmarks"></div> </div> <script src="./js/bundle_app.js"></script> </body> </html>
bundle_xxx.js を生成するため、以下のような webpack 設定ファイルを用意します。
fs: 'empty'
の箇所は Keras.js を webpack で処理するために必要な設定で、これが無いと Module not found: Error: Can't resolve 'fs'
のようなエラーが出る事になります。
webpack.config.js
module.exports = { entry: { bundle_app: __dirname + '/src/app.js', bundle_worker: __dirname + '/src/worker.js' }, output: { path: __dirname + '/js', filename: '[name].js', }, // Keras.js 用の設定 node: { fs: 'empty' } }
Web Worker では Actor モデルのようにメッセージパッシング(postMessage
で送信、onmessage
で受信)を使ってメインの UI 処理とのデータ連携を行います。
今回は、以下のようなメッセージを Web Worker(Keras.js を使った処理)とやり取りするようにします。
Web Worker(Keras.js の処理)とのメッセージ内容
処理 | 送信メッセージ | 受信メッセージ(成功時) | 受信メッセージ(エラー時) |
---|---|---|---|
初期化 | {type: 'init', url: <モデルファイルのURL>} |
{type: 'init'} |
{type: 'init', error: <エラーメッセージ>} |
ランドマーク検出 | {type: 'predict', input: <入力データ>} |
{type: 'predict', output: <ランドマーク検出結果>} |
{type: 'predict', error: <エラーメッセージ>} |
(a) ランドマーク検出処理(src/worker.js)
Web Worker として実装するため、postMessage
で UI 処理へメッセージを送信し、onmessage
でメッセージを受信するようにします。
Keras.js 1.0.3 における Dense 処理の問題
実は、Keras.js 1.0.3 では今回の CNN モデルを正しく処理できません。
というのも、Keras.js 1.0.3 における Dense の処理(GPU を使わない場合)は以下のようになっています。
node_modules/keras-js/lib/layers/core/Dense.js の問題個所
_callCPU(x) { this.output = new _Tensor.default([], [this.units]); ・・・ }
今回の CNN モデルでは Dense の結果が 3次元 (256, 256, 7)
になる必要がありますが、上記 Dense 処理では (7)
のように 1次元になってしまい正しい結果を得られません。 ※
※ ついでに、Keras.js の softmax 処理にも不都合な点があった
そこで、今回は(GPU を使わない事を前提に)Dense
の _callCPU
を実行時に書き換える事で回避しました。
処理内容としては、元の処理を 2重ループ内で実施するようにしています。
Dense 問題の回避措置(src/worker.js)
import KerasJS from 'keras-js' import { gemv } from 'ndarray-blas-level2' import ops from 'ndarray-ops' ・・・ // Dense の _callCPU を実行時に変更 KerasJS.layers.Dense.prototype._callCPU = function(x) { const h = x.tensor.shape[0] const w = x.tensor.shape[1] this.output = new KerasJS.Tensor([], [h, w, this.units]) for (let i = 0; i < h; i++) { for (let j = 0; j < w; j++) { const xt = x.tensor.pick(i, j) const ot = this.output.tensor.pick(i, j) if (this.use_bias) { ops.assign(ot, this.weights['bias'].tensor) } gemv(1, this.weights['kernel'].tensor.transpose(1, 0), xt, 1, ot) this.activationFunc({tensor: ot}) } } }
ランドマーク検出の実装
KerasJS.Model
の predict
へ入力データを渡したり結果を取り出すにはレイヤー名を指定する必要があり、これらのレイヤー名は iuputLayerNames
と outputLayerNames
でそれぞれ取得できます。
predict の結果は、各座標のランドマーク該当確率 (256, 256, 7)
となるので、ここではランドマーク毎 ※ に最も確率の高かった座標のみを結果として返すようにしています。
※ ランドマーク 0 はランドマークに該当しなかった場合なので結果に含めていない
src/worker.js
import KerasJS from 'keras-js' import { gemv } from 'ndarray-blas-level2' import ops from 'ndarray-ops' let model = null // モデルデータの読み込み const loadModel = file => { const model = new KerasJS.Model({ filepath: file }) return model.ready().then(r => model) } // Keras.js の Dense 問題への対応 KerasJS.layers.Dense.prototype._callCPU = function(x) { ・・・ } // predict の結果を処理(ランドマーク毎に最も確率の高い座標を抽出) const detectLandmarks = ts => { const res = {} for (let h = 0; h < ts.tensor.shape[0]; h++) { for (let w = 0; w < ts.tensor.shape[1]; w++) { const t = ts.tensor.pick(h, w) const wrkProb = {landmark: 0, prob: 0, x: w, y: h} for (let c = 0; c < t.shape[0]; c++) { const prob = t.get(c) if (prob > wrkProb.prob) { wrkProb.landmark = c wrkProb.prob = prob } } // ランドマーク 0 (ランドマークでは無い)は除外 if (wrkProb.landmark > 0) { const curProb = res[wrkProb.landmark] if (!curProb || curProb.prob < wrkProb.prob) { res[wrkProb.landmark] = wrkProb } } } } return res } // UI 処理からのメッセージ受信 onmessage = ev => { switch (ev.data.type) { case 'init': loadModel(ev.data.url) .then(m => { model = m postMessage({type: ev.data.type}) }) .catch(err => { console.log(err) postMessage({type: ev.data.type, error: err.message}) }) break case 'predict': const outputLayerName = model.outputLayerNames[0] const shape = model.modelLayersMap.get(outputLayerName) .output.tensor.shape const data = {} // 入力データの設定 data[model.inputLayerNames[0]] = ev.data.input Promise.resolve(model.predict(data)) .then(r => new KerasJS.Tensor(r[outputLayerName], shape)) // predict 実行結果の取り出し .then(detectLandmarks) .then(r => // UI 処理へ結果送信 postMessage({type: ev.data.type, output: r}) ) .catch(err => { console.log(err) postMessage({type: ev.data.type, error: err.message}) }) break } }
(b) UI 処理(src/app.js)
画像データの変換(入力データの作成)
KerasJS.Model で predict するために、今回のケースでは画像データを 256(高さ)× 256(幅)× 3(RGB)
サイズの 1次元配列 Float32Array
へ変換する必要があります。
今回は以下のように canvas を利用して変換を行いました。
- (1) canvas へドラッグアンドドロップした画像を canvas へ描画
- (2)
getImageData
で canvas からImageData
を取得 - (3)
ImageData.data
の内容を RGB 並びの Float32Array へ変換
ImageData.data は RGBA 並びの 1次元配列 Uint8ClampedArray
となっているので、RGB 部分のみを取り出して(A の内容を除外する)Float32Array
を生成しています。
ちなみに、今回の CNN モデル自体は画像サイズに依存しない(Fully Convolutional Networks 的な)構成になっています。
そのため、任意サイズの画像を処理する事もできるのですが、現時点の Keras.js ではそんな事を考慮してくれていないので、実現するにはそれなりの工夫が必要になります。(一応、実現は可能でした)
ここでは、単純に canvas へ描画した 256x256 範囲の内容だけ(つまりは固定サイズ)を使うようにしています。※
※ この方法では 256x256 以外のサイズで欠けや余白の入り込みが発生する
画像データ変換部分(src/app.js)
・・・ const imageTypes = ['image/jpeg'] const canvas = document.getElementsByTagName('canvas')[0] const ctx = canvas.getContext('2d') ・・・ // RGBA 並びの Uint8ClampedArray を RGB 並びの Float32Array へ変換 const imgToArray = imgData => new Float32Array( imgData.data.reduce( (acc, v, i) => { // RGBA の A 部分を除外 if (i % 4 != 3) { acc.push(v) } return acc }, [] ) ) // 画像の読み込み const loadImage = url => new Promise(resolve => { const img = new Image() img.addEventListener('load', () => { ctx.clearRect(0, 0, canvas.width, canvas.height) // 画像サイズが canvas よりも小さい場合の考慮 const w = Math.min(img.width, canvas.width) const h = Math.min(img.height, canvas.height) // canvas へ画像を描画 ctx.drawImage(img, 0, 0, w, h, 0, 0, w, h) // ImageData の取得 const d = ctx.getImageData(0, 0, canvas.width, canvas.height) resolve(imgToArray(d)) }) img.src = url }) ・・・ // モデルデータ読み込み完了時の処理 const ready = () => { canvas.addEventListener('dragover', ev => { ev.preventDefault() canvas.classList.add('dragging') }, false) canvas.addEventListener('dragleave', ev => { canvas.classList.remove('dragging') }, false) // ドロップ時の処理 canvas.addEventListener('drop', ev => { ev.preventDefault() canvas.classList.remove('dragging') const file = ev.dataTransfer.files[0] if (imageTypes.includes(file.type)) { ・・・ const reader = new FileReader() reader.onload = ev => { loadImage(reader.result) .then(img => { ・・・ }) } reader.readAsDataURL(file) } }, false) } ・・・
Web Worker との連携
Web Worker とメッセージをやり取りし、ランドマークの検出結果を描画する部分の実装です。
Web Worker との連携部分(src/app.js)
const colors = ['rgb(255, 255, 255)', 'rgb(255, 0, 0)', 'rgb(0, 255, 0)', 'rgb(0, 0, 255)', 'rgb(255, 255, 0)', 'rgb(0, 255, 255)', 'rgb(255, 0, 255)'] const radius = 5 const imageTypes = ['image/jpeg'] const modelFile = '../model/cnn_landmark_400.bin' // Web Worker の作成 const worker = new Worker('./js/bundle_worker.js') ・・・ // 検出したランドマークを canvas へ描画 const drawLandmarks = lms => { Object.values(lms).forEach(v => { ctx.fillStyle = colors[v.landmark] ctx.beginPath() ctx.arc(v.x, v.y, radius, 0, Math.PI * 2, false) ctx.fill() }) } ・・・ // 検出したランドマークの内容を table(HTML)化して表示 const showLandmarksInfo = lms => { ・・・ infoNode.innerHTML = ` <table> <tr> <th>landmark</th> <th>coordinate</th> <th>prob</th> </tr> ${rowsHtml} </table> ` } // モデルデータ読み込み完了後 const ready = () => { ・・・ canvas.addEventListener('drop', ev => { ・・・ if (imageTypes.includes(file.type)) { ・・・ reader.onload = ev => { loadImage(reader.result) .then(img => { detectDialog.showModal() // Web Worker へのランドマーク検出指示 worker.postMessage({type: 'predict', input: img}) }) } reader.readAsDataURL(file) } }, false) } // Web Worker からのメッセージ受信 worker.onmessage = ev => { if (ev.data.error) { ・・・ } else { switch (ev.data.type) { case 'init': ready() loadDialog.close() break case 'predict': const res = ev.data.output console.log(res) detectDialog.close() drawLandmarks(res) showLandmarksInfo(res) break } } } loadDialog.showModal() // Web Worker へのモデルデータ読み込み指示 worker.postMessage({type: 'init', url: modelFile})
(c) ビルド
webpack コマンドを実行し、js/bundle_app.js と js/bundle_worker.js を生成します。
webpack によるビルド
> webpack
(d) 動作確認
HTTP サーバーを使って動作確認を行います。 今回は http-server を使って実行しました。
http-server 実行
> http-server Starting up http-server, serving ./ Available on: ・・・ http://127.0.0.1:8080 Hit CTRL-C to stop the server
http://localhost:8080/index.html
へ Chrome ※ でアクセスして画像ファイルをドラッグアンドドロップすると以下のような結果となりました。
※ HTMLDialogElement.showModal() を使っている関係で 現時点では Chrome でしか動作しませんが、 dialog 以外の部分(Keras.js の処理等)は Firefox でも動作するようになっています
TypeScript で funfix を使用 - tsc, FuseBox
funfix は JavaScript, TypeScript, Flow の関数型プログラミング用ライブラリで、Fantasy Land や Static Land ※ に準拠し Scala の Option, Either, Try, Future 等と同等の型が用意されているようです。
※ JavaScript 用に Monoid や Monad 等の代数的構造に関する仕様を定義したもの
今回は Option を使った単純な処理を TypeScript で実装し Node.js で実行してみます。
ソースは http://github.com/fits/try_samples/tree/master/blog/20180730/
はじめに
Option
を使った下記サンプルをコンパイルして実行します。
サンプルソース
import { Option, Some } from 'funfix' const f = (ma, mb) => ma.flatMap(a => mb.map(b => `${a} + ${b} = ${a + b}`)) const d1 = Some(10) const d2 = Some(2) console.log( d1 ) console.log('-----') console.log( f(d1, d2) ) console.log( f(d1, Option.none()) ) console.log('-----') console.log( f(d1, d2).getOrElse('none') ) console.log( f(d1, Option.none()).getOrElse('none') )
ビルドと実行
上記ソースファイルを以下の 2通りでビルドして実行してみます。
- (a) tsc 利用
- (b) FuseBox 利用
(a) tsc を利用する場合
tsc コマンドを使って TypeScript のソースをコンパイルします。
まずは typescript と funfix モジュールをそれぞれインストールします。
typescript インストール
> npm install --save-dev typescript
funfix インストール
> npm install --save funfix
この状態で sample.ts
ファイルをコンパイルしてみると、型関係のエラーが出るものの sample.js
は正常に作られました。
コンパイル1
> tsc sample.ts node_modules/funfix-core/dist/disjunctions.d.ts:775:14 - error TS2416: Property 'value' in type 'TNone' is not assignable to the same property in base type 'Option<never>'. Type 'undefined' is not assignable to type 'never'. 775 readonly value: undefined; ~~~~~ node_modules/funfix-effect/dist/eval.d.ts:256:42 - error TS2304: Cannot find name 'Iterable'. 256 static sequence<A>(list: Eval<A>[] | Iterable<Eval<A>>): Eval<A[]>; ~~~~~~~~ ・・・
sample.js を実行してみると特に問題無く動作します。
実行1
> node sample.js TSome { _isEmpty: false, value: 10 } ----- TSome { _isEmpty: false, value: '10 + 2 = 12' } TNone { _isEmpty: true, value: undefined } ----- 10 + 2 = 12 none
これで一応は動いたわけですが、コンパイル時にエラーが出るというのも望ましい状態ではないので、エラー部分を解決してみます。
他にも方法があるかもしれませんが、ここでは以下のように対応します。
- (1)
Property 'value' in type 'TNone' ・・・ 'Option<never>'
のエラーに対して tsc 実行時に--strictNullChecks
オプションを指定して対応 - (2)
Cannot find name 'Iterable'
等のエラーに対して@types/node
をインストールして対応
strictNullChecks は tsc の実行時オプションで指定する以外にも設定ファイル tsconfig.json
で設定する事もできるので、ここでは tsconfig.json ファイルを使います。
(1) tsconfig.json
{ "compilerOptions": { "strictNullChecks": true } }
次に @types/node をインストールします。
@types/node には Node.js で実行するための型定義(Node.js 依存の API 等)が TypeScript 用に定義されています。
(2) @types/node インストール
> npm install --save-dev @types/node
この状態で tsc を実行すると先程のようなエラーは出なくなりました。(tsconfig.json を適用するため tsc コマンドへ引数を指定していない点に注意)
コンパイル2
> tsc
実行結果にも差異はありません。
実行2
> node sample.js TSome { _isEmpty: false, value: 10 } ----- TSome { _isEmpty: false, value: '10 + 2 = 12' } TNone { _isEmpty: true, value: undefined } ----- 10 + 2 = 12 none
最終的な package.json の内容は以下の通りです。
package.json
{ "name": "sample", "version": "1.0.0", "devDependencies": { "@types/node": "^10.5.4", "typescript": "^2.9.2" }, "dependencies": { "funfix": "^7.0.1" } }
(b) FuseBox を利用する場合
次に、モジュールバンドラーの FuseBox を使用してみます。(以降は (a) とは異なるディレクトリで実施)
なお、ここでは npm の代わりに yarn
を使っていますが、npm でも特に問題はありません。
yarn のインストール例(npm 使用)
> npm install -g yarn
(b-1) 型チェック無し
まずは typescript, fuse-box, funfix をそれぞれインストールしておきます。
typescript と fuse-box インストール
> yarn add --dev typescript fuse-box
funfix インストール
> yarn add funfix
FuseBox ではビルド定義を JavaScript のコードで記載します。 とりあえずは必要最小限の設定を行いました。
bundle で指定した名称が init の $name
に適用されるため、*.ts
のコンパイル結果と依存モジュールの内容をバンドルして bundle.js
へ出力する事になります。
なお、>
でロード時に実行する(コードを記載した)ファイルを指定します。
fuse.js (FuseBox ビルド定義)
const {FuseBox} = require('fuse-box') const fuse = FuseBox.init({ output: '$name.js' }) fuse.bundle('bundle').instructions('> *.ts') fuse.run()
上記スクリプトを実行してビルド(TypeScript のコンパイルとバンドル)を行います。
ビルド
> node fuse.js --- FuseBox 3.4.0 --- → Generating recommended tsconfig.json: ・・・\sample_fusebox1\tsconfig.json → Typescript script target: ES7 -------------------------- Bundle "bundle" sample.js └── (1 files, 700 Bytes) default └── funfix-core 34.4 kB (1 files) └── funfix-effect 43.1 kB (1 files) └── funfix-exec 79.5 kB (1 files) └── funfix 1 kB (1 files) size: 158.7 kB in 765ms
初回実行時にデフォルト設定の tsconfig.json が作られました。(tsconfig.json が存在しない場合)
tsc の時のような型関係のエラーは出ていませんが、これは FuseBox がデフォルトで TypeScript の型チェックをしていない事が原因のようです。
型チェックを実施するには fuse-box-typechecker
プラグインを使う必要がありそうです。
実行
> node bundle.js TSome { _isEmpty: false, value: 10 } ----- TSome { _isEmpty: false, value: '10 + 2 = 12' } TNone { _isEmpty: true, value: undefined } ----- 10 + 2 = 12 none
package.json の内容は以下の通りです。
package.json
{ "name": "sample_fusebox1", "version": "1.0.0", "main": "bundle.js", "license": "MIT", "devDependencies": { "fuse-box": "^3.4.0", "typescript": "^2.9.2" }, "dependencies": { "funfix": "^7.0.1" } }
(b-2) 型チェック有り
TypeScript の型チェックを行うようにしてみます。
まずは、(b-1) と同じ構成に fuse-box-typechecker
プラグインを加えます。
fuse-box-typechecker を追加インストール
> yarn add --dev fuse-box-typechecker
次に、fuse.js へ fuse-box-typechecker プラグインの設定を追加します。
TypeChecker で型チェックにエラーがあった場合、例外が throw されるようにはなっていないため、ここではエラーがあった場合に Error を throw して fuse.run() を実行しないようにしてみました。
ただし、こうすると tsconfig.json を予め用意しておく必要があります。(TypeChecker に tsconfig.json が必要)
fuse.js (FuseBox ビルド定義)
const {FuseBox} = require('fuse-box') const {TypeChecker} = require('fuse-box-typechecker') // fuse-box-typechecker の設定 const testSync = TypeChecker({ tsConfig: './tsconfig.json' }) const fuse = FuseBox.init({ output: '$name.js' }) fuse.bundle('bundle').instructions('> *.ts') testSync.runPromise() .then(n => { if (n != 0) { // 型チェックでエラーがあった場合 throw new Error(n) } // 型チェックでエラーがなかった場合 return fuse.run() }) .catch(console.error)
これで、ビルド時に (a) と同様の型エラーが出るようになりました。
ビルド1
> node fuse.js ・・・ --- FuseBox 3.4.0 --- Typechecker plugin(promisesync) . Time:Sun Jul 29 2018 12:40:47 GMT+0900 (GMT+09:00) File errors: └── .\node_modules\funfix-core\dist\disjunctions.d.ts | ・・・\sample_fusebox2\node_modules\funfix-core\dist\disjunctions.d.ts (775,14) (Error:TS2416) Property 'value' in type 'TNone' is not assignable to the same property in base type 'Option<never>'. Type 'undefined' is not assignable to type 'never'. Errors:1 └── Options: 0 └── Global: 0 └── Syntactic: 0 └── Semantic: 1 └── TsLint: 0 Typechecking time: 4116ms Quitting typechecker ・・・
ここで、Iterable
の型エラーが出ていないのは fuse-box-typechecker のインストール時に @types/node もインストールされているためです。
(a) と同様に strictNullChecks の設定を tsconfig.json へ追記して、このエラーを解決します。
tsconfig.json へ strictNullChecks の設定を追加
{ "compilerOptions": { "module": "commonjs", "target": "ES7", ・・・ "strictNullChecks": true } }
これでビルドが成功するようになりました。
ビルド2
> node fuse.js ・・・ Typechecker name: undefined Typechecker basepath: ・・・\sample_fusebox2 Typechecker tsconfig: ・・・\sample_fusebox2\tsconfig.json --- FuseBox 3.4.0 --- Typechecker plugin(promisesync) . Time:Sun Jul 29 2018 12:44:57 GMT+0900 (GMT+09:00) All good, no errors :-) Typechecking time: 4103ms Quitting typechecker killing worker → Typescript config file: \tsconfig.json → Typescript script target: ES7 -------------------------- Bundle "bundle" sample.js └── (1 files, 700 Bytes) default └── funfix-core 34.4 kB (1 files) └── funfix-effect 43.1 kB (1 files) └── funfix-exec 79.5 kB (1 files) └── funfix 1 kB (1 files) size: 158.7 kB in 664ms
実行結果
> node bundle.js TSome { _isEmpty: false, value: 10 } ----- TSome { _isEmpty: false, value: '10 + 2 = 12' } TNone { _isEmpty: true, value: undefined } ----- 10 + 2 = 12 none
package.json の内容は以下の通りです。
package.json
{ "name": "sample_fusebox2", "version": "1.0.0", "main": "index.js", "license": "MIT", "devDependencies": { "fuse-box": "^3.4.0", "fuse-box-typechecker": "^2.10.0", "typescript": "^2.9.2" }, "dependencies": { "funfix": "^7.0.1" } }
Kubernetes の Watch API とタイムアウト
Kubernetes の Watch API を下記クライアントライブラリを使って試してみました。
ソースは http://github.com/fits/try_samples/tree/master/blog/20180409/
はじめに
下記のコマンドを実行して Javascript Kubernetes Client をインストールしておきます。
Javascript Kubernetes Client のインストール
> npm install @kubernetes/client-node
Watch API による Pod の監視
Watch API で default
Namespace の Pod に関するイベントを監視して、イベントのタイプと Pod 名を標準出力する処理を実装してみます。
watch
の第一引数に Watch API のエンドポイント URL、第三引数でイベントハンドラを指定します。(第二引数はクエリパラメータ)
今回は Pod を監視していますが、default
Namespace の Deployment を監視する場合は endpoint
を /apis/apps/v1/namespaces/default/deployments
とします。
なお、$HOME/.kube/config
もしくは %USERPROFILE%\.kube\config
ファイルから Kubernetes への接続情報を取得するようにしています。
sample_watch_pod.js
const k8s = require('@kubernetes/client-node') // default Namespace の Pod const endpoint = '/api/v1/namespaces/default/pods' // Windows 環境用の設定 if (!process.env.HOME) { process.env.HOME = process.env.USERPROFILE } const conf = new k8s.KubeConfig() conf.loadFromFile(`${process.env.HOME}/.kube/config`) const w = new k8s.Watch(conf) w.watch( endpoint, {}, (type, obj) => { console.log(`${type} : ${obj.metadata.name}`) }, err => { if (err) { console.error(err) } else { console.log('done') } } )
動作確認
今回、Kubernetes の環境を minikube で用意します。
minikube コマンドを使って start を実行するとローカル用の Kubernetes 環境が立ち上がります。
その際に、%USERPROFILE%\.kube\config
ファイル等が作られます。
minikube 開始
> minikube start ・・・ Starting local Kubernetes v1.9.0 cluster... Starting VM... Getting VM IP address... Moving files into cluster... Setting up certs... Connecting to cluster... Setting up kubeconfig... Starting cluster components... Kubectl is now configured to use the cluster. Loading cached images from config file.
sample_watch_pod.js の実行
> node sample_watch_pod.js
下記 YAML ファイルを使って、Kubernetes 環境へ nginx 実行用の Deployment と Service を作成してみます。
nginx.yaml (nginx 用の Deployment と Service 定義)
apiVersion: v1 kind: Service metadata: name: nginx-service labels: app: nginx spec: ports: - name: http port: 80 nodePort: 30001 selector: app: nginx type: NodePort --- apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deploy spec: replicas: 2 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx ports: - containerPort: 80
kubectl を使って Deployment と Service を作成します。
Deployment と Service 作成
> kubectl create -f nginx.yaml service "nginx-service" created deployment "nginx-deploy" created
Watch の結果は以下のようになりました。
sample_watch_pod.js の結果1
> node sample_watch_pod.js ADDED : nginx-deploy-679dc9c764-r9ds5 MODIFIED : nginx-deploy-679dc9c764-r9ds5 ADDED : nginx-deploy-679dc9c764-54d5d MODIFIED : nginx-deploy-679dc9c764-r9ds5 MODIFIED : nginx-deploy-679dc9c764-54d5d MODIFIED : nginx-deploy-679dc9c764-54d5d MODIFIED : nginx-deploy-679dc9c764-r9ds5 MODIFIED : nginx-deploy-679dc9c764-54d5d
ここで、いつまでも接続が続くわけでは無く、minikube の環境では 40分程度(ただし、毎回異なる)で接続が切れ以下のようになりました。
sample_watch_pod.js の結果2 (一定時間経過後)
> node sample_watch_pod.js ・・・ done
タイムアウト時間の確認
Kubernetes と minikube のソースから、タイムアウトに関係していると思われる箇所 timeout = time.Duration(float64(minRequestTimeout) * (rand.Float64() + 1.0))
を見つけました。※
※ minikube では localkube 内で Kubernetes の API Server を実行しているようです
これだと、タイムアウトは 30 ~ 60分でランダムに決まる事になりそうなので、接続の切れる時間が毎回異なるという現象に合致します。
ソース kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/get.go
func ListResource(r rest.Lister, rw rest.Watcher, scope RequestScope, forceWatch bool, minRequestTimeout time.Duration) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { ・・・ if opts.Watch || forceWatch { ・・・ timeout := time.Duration(0) if opts.TimeoutSeconds != nil { timeout = time.Duration(*opts.TimeoutSeconds) * time.Second } if timeout == 0 && minRequestTimeout > 0 { timeout = time.Duration(float64(minRequestTimeout) * (rand.Float64() + 1.0)) } glog.V(2).Infof("Starting watch for %s, rv=%s labels=%s fields=%s timeout=%s", req.URL.Path, opts.ResourceVersion, opts.LabelSelector, opts.FieldSelector, timeout) ・・・ return } ・・・ } }
ソース minikube/pkg/localkube/apiserver.go
// defaults from apiserver command config.GenericServerRunOptions.MinRequestTimeout = 1800
get.go の処理ではログレベル 2 でタイムアウトの値をログ出力しているので(glog.V(2).Infof(・・・)
の箇所)ログから確認できそうです。
ただし、普通に minikube start
で実行してもログレベル 2 のログは見れないようなので、minikube を -v <ログレベル>
オプションを使って起動しなおします。
ログレベル 2 で miinkube 開始
> minikube start -v 2
sample_watch_pod.js の実行
> node sample_watch_pod.js ・・・
minikube logs
でログ内容を確認してみると、get.go が出力しているタイムアウトの値を確認できました。
ログ確認
> minikube logs ・・・ Apr 08 01:00:30 minikube localkube[2995]: I0408 01:00:30.533448 2995 get.go:238] Starting watch for /api/v1/namespaces/default/pods, rv= labels= fields= timeout=58m38.2420124s ・・・