AerospikeでUDFを作ってGoから呼び出す

www.aerospike.com

Aerospikeの話。
UDFを使う機会があったのでその基本的な操作方法とかをメモとして残しておく。

UDFとは何か?

User-Defined Functionsを略してUDF。
ユーザー定義関数でluaを使ってかく。
公式のドキュメントだとこのへんに書いてある。

AerospikeのUDFは2種類あってRecord UDFStream UDFがある。
Record UDFはkeyに対する処理でrecordをinsertしたりupdateしたり出来る。
Stream UDFはクエリの結果を分散して処理し集約してくれます。

今回はRecord UDFを使ってkeyに紐づくレコードの操作をしていきます。

準備

Aerospikeをまず始めに立てる必要があるのですが、公式がdocker imageを公開しているので、それを使います。

# デーモンとして起動しport3000をポートフォワーディングする
$ docker run -d --rm -p 3000:3000 aerospike/aerospike-server:3.14.0

後はコンテナIDを指定してコンテナに入ってaqlとかを叩けばshow namespacesとかクエリを発行出来るようになるはずです。

$ docker exec -it [コンテナID] bash -l 
$ aql
aql> show namespaces
+------------+
| namespaces |
+------------+
| "test"     |
+------------+
1 row in set (0.002 secs)
OK

UDFを準備する

Aerospike上で下記のluaのプログラムを用意します。
やってることはシンプルで引数として渡されたvalueが数値で100より下の場合にnumというbinにたいしてその値を入れます。すでにrecordが存在する場合はupdateを行って、無ければinsertしています。
今回は保存パスとして/root/udfs/save.luaにしたとします。

function saveIntValue(rec, value)
    if type(value) ~= "number" then
        local msg = string.format("%s<%s> not number", value, type(value))
        warn(msg)
        error(msg)
    end

    if value > 100 then
        local msg = string.format("large value %d", value)
        warn(msg)
        error(msg)
    end

    rec["num"] = value
    local rc = 0
    if aerospike:exists(rec) then
        rc = aerospike:update(rec)
    else
        rc = aerospike:create(rec)
    end

    if rc ~= nil and rc ~= 0 then
        local msg = string.format("save record failed. rc: %s", tostring(rc))
        warn(msg)
        error(msg)
    end
    return 0
end

UDFをAerospikeに登録する

登録はaql上からも行うことが出来て、ファイルのある絶対パス相対パスを指定して登録することが出来る。

aql> register module '/root/udfs/save.lua'
OK, 1 module added.

登録されたUDF一覧を見るときはSHOW MODULESで見ることが出来る。

UDFを呼び出してみる

aql

aqlでも呼び出すことは出来る。
詳しくは公式のdocを参照してください。

aql> execute save.saveIntValue(3) on test.hoge where PK = 1 
+--------------+
| saveIntValue |
+--------------+
| 0            |
+--------------+
1 row in set (0.001 secs)

# 100以上を指定するとerrorが返ってくる
aql> execute save.saveIntValue(300) on test.hoge where PK = 1
Error: (100) /opt/aerospike/usr/udf/lua/save.lua:11: large value 300

Go

たぶんaqlから呼び出すことって開発中とかしかなくて実際に使用する時はGoとかRubySDKを使うことが多いと思うので今回はGoから呼び出すことにする。

package main

import (
    "flag"
    "fmt"

    . "github.com/aerospike/aerospike-client-go"
)


func main() {
    var pk = flag.Int("pk", 0, "primary key")
    var val = flag.Int("val", 0, "set value")
    flag.Parse()
    if *pk == 0 {
        panic("please set -pk")
    }
    if *val == 0 {
        panic("please set -val")
    }

    client, err := NewClient("127.0.0.1", 3000)
    if err != nil {
        panic(err)
    }
    key, err := NewKey("test", "fuga", *pk)
    if err != nil {
        panic(err)
    }
    _, err = client.Execute(nil, key, "save", "saveIntValue", NewValue(*val))
    if err != nil {
        panic(err)
    }
    fmt.Println("SUCCESS")
}

pkやvalを実行時に指定したりAerospikeのクライアントを作ったりしてるけど重要なのは最後のほうの

_, err = client.Execute(nil, key, "save", "saveIntValue", NewValue(*val))

ここでUDFを呼び出している。
第1引数はWritePolicyで今回はnilをいれている。
第2引数はKeyをいれており、Goの場合はNewKey(namespace, set, pk)で生成したものを使う 第3引数で今回作成したUDFの名前(パッケージ名)を指定する
第4引数で関数名を指定する 第5以降は関数のrecより後の引数を渡す。今回はvalueなので1つだけ

UDF内でerrorになった時はGo側のerrorとして返ってくるので今回はそれをハンドリングしてpanicで処理をしている。

$ go build -o main
$ ./main -pk=3 -val=88
SUCCESS
$ ./main -pk=3 -val=1000
panic: /opt/aerospike/usr/udf/lua/save.lua:11: large value 1000

最後に

今使っている限りだとAerospikeはメモリ上にレコードを保持していけるのでメモリ使用量がどんどん増えることはあるけど、
CPUはあまり使ってないので、こういった処理をAerospikeに任せるというのも1つの手だなと思った。

SSHをちょっとだけ楽にする

~/.ssh/configを次のように書いているとする。
実際開発していると複数台アプリケーションサーバーがあってミドルウェアが入っているサーバーがあってとか気づくとたくさんのサーバーのconfigが追加されている。

Host bastion
  HostName www.xxx.yyy.zzz
  User hoge
  IdentityFile ~/.ssh/hoge.pem

Host server1
  HostName xxx.xxx.xxx.xxx
  User fuga
  IdentityFile ~/.ssh/fuga.pem
  ProxyCommand ssh -CW %h:%p bastion

Host server2
  HostName yyy.yyy.yyy.yyy
  User piyo
  IdentityFile ~/.ssh/piyo.pem
  ProxyCommand ssh -CW %h:%p bastion

だいたいはhistoryからgrepしたりするんだけど、あのサーバーなんだっけなぁーとかたまにしか使用しないのって忘れてしまって~/.ssh/configを見たりする。
その作業が意外と多いので止めたいと思った。

使用するのはおなじみのpeco!!
「pecoは標準入力から受けた行データをインクリメンタルサーチして選択した行を標準出力に返す」
ただこれだけのシンプルなものです。

github.com

今回はHostだけを取り出したいので次のようなコマンドを実行する。
* を外しているのはHost 127.*のようなものを外すためです。

$ grep "^Host " ~/.ssh/config | sed s/"^Host "// | grep -v "*"
bastion
server1
server2

あとはこれをpecoに突っ込んで選択したものをsshにかける。

ssh $(grep "^Host " ~/.ssh/config | sed s/"^Host "// | grep -v "^\*$" | peco)

最後に任意のaliasをはってあげれば完了!!
これで少し便利になった。

最後に

peco便利だ

Goでファイル系のリソースも一緒にビルドして配布しちゃう!

例えば下記のようなディレクトリ構成のGoプロジェクトがあったとする。

.
├── files
│   └── sample.txt
└── main.go

sampleの中身は

sample!
sample!!
sample!!!

main.goの中身は

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    f, err := os.Open("files/sample.txt")
    if err != nil {
        panic(err)
    }
    scanner := bufio.NewScanner(f)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
    f.Close()
}

実行するとfiles/sample.txtの中身がコンソールに出力される。単純なもの。

$ go run main.go
sample!
sample!!
sample!!!

例えばこれをbuildしてGithubのReleaseとかで配布することを考える。

$ go build -o /tmp/print_file  main.go

/tmp/print_fileでビルドされた成果物が置かれるので、これを実行する。

$ /tmp/print_file                                                                                                                                                                                                               
panic: open files/sample.txt: no such file or directory

go buildでは指定されたパッケージの依存関係を解決していきながらビルドしていくのでGoで書かれていれば含まれるのですが、今回のファイルのようなリソースは含まれないため起きたようです。

flagパッケージを使ってコマンドラインからフィアルパスを渡してあげるとかにすると思うのですが、GithubのReleasesとかでバイナリファイルして置くだけで実行したい時ってあると思うんですよ!

go-bindata

github.com

go-bindataは指定されたリソースをバイト列でGoのソースコードに記載してそのデータにアクセスできるメソッドをつけてくれます。
例えば今回はfiles配下のファイルを対象としたいので下記のようにコマンドを実行します。

$ go-bindata files

このコマンドを実行することでbindata.goが生成されます。

.
├── bindata.go
├── files
│   └── sample.txt
└── main.go

今生成したデータに対してアクセスするにはREADME.mdにも記載されていますがAsset を使用します。
今回のファイルの中身を出力するものを書き換えると

package main

import (
    "fmt"
)

func main() {
    b, err := Asset("files/sample.txt")
    if err != nil {
        panic(err)
    }
    s := string(b)
    fmt.Print(s)
}

Assetでは[]byteで返ってきます。
先程と同じようにビルドします。今回は先程go-bindataで作成したbindata.goも一緒にビルドに含めます。

$ go build -o /tmp/print_file main.go bindata.go
$ /tmp/print_file
sample!
sample!!
sample!!!

おー

最後に

今回はGoのビルドを行う際にGoファイル以外のリソースを含める方法を書いた。
大きすぎるファイルはビルドにも時間がかかるし配布も大変だろうけど小さい設定ファイル的なものだったらgo-bindataを使用して一緒にビルドしてしまうという手も良さそう。
使っていきたい

とりあえずAIスピーカー試したいならGoogle Home mini買えば良さそう!特にエンジニアは楽しめる

最近LINEのClova WAVEがあったりAppleのHomePodとかAmazonAmazon Echoとか色々出てきている。
ただどれも1万超えたりするからちょっと買うの躊躇してしまう。

GoogleからもGoogle Homeが出ていて値段は1万5000円くらいとこれも1万を超えてくる。

store.google.com

ただ今年の10月23日にGoogle Home miniという6500円くらいで買えるものが出た!!

store.google.com

とりあえず買った

お手頃な値段だし買った。
本当はテレビとか電球を連携したかったけど対応家電が1つもない。
ただ軽い話相手にはなるし天気は教えてくれて音楽もかけてくれる。

IFTTTが使えるらしい!

調べてみるとGoogle Home miniの中で動いているGoogleアシスタントがIFTTTのトリガーで使えるらしい

f:id:hatappi1225:20171107231703p:plain

トリガーには何かの言葉に反応させたり特定パターンで数字とかテキストを認識したものをトリガーに出来たりする。

f:id:hatappi1225:20171107231901p:plain

例えば何かの言葉に反応するSay a simple phraseで「ほげ」に対して「ほげ」を返すようにするには次のような設定になる。

f:id:hatappi1225:20171107232202p:plain

このトリガーからのアクションはFacebookTwitterなどのSNSの投稿からTrelloとかSlack、Googleカレンダーと様々なサービスに対してのアクションを設定出来る。

qiita.com

このアクションの中には任意のURLにリクエストが出来るWebhooksがある。
これが使えると自分で作ったサービスとかにリクエストが出来るので色々出来る幅が広がる。

色々とは

hatappi.hateblo.jp

以前も書いたけど僕は自宅に学習リモコンをおいて外から操作出来るようにしたものをRailsで組んでいる。
今回はそのエンドポイントをIFTTTのWebhooksに登録してあげるだけ。
対応家電買わなくても家帰ったら「OK! Google 電気をつけてー」で電気をつけられるのは中々良い感じ!

なぜDialogflowを使わなかったのか

Google Homeでこういったハックをしている方法を探すとIFTTTの他にDialogflowを使ってる例を目にする。
Dialogflowを使うと自然言語処理機能を提供してくれてIFTTTの時よりもより柔軟に設定が出来るのと例えば自前のAPIと連携して指定のフォーマットのJSONを返してあげればそれをGoogle Home miniで発音してもらえる!
ただこのDialogflowはあくまでもアプリの開発という形なので「OK! Google! 〜〜と話す」というようにまずアプリを呼び出してあげないといけない。
たしかにこれをつければ家電操作アプリ的なのを作って「OK!Google! 家電操作アプリと話す」みたいな感じでいけるかもしれない。 でも何か違う!もっとスムーズにやりたかった。なのでレスポンスとか少し自由度は低くなるけどIFTTTを使った。

まとめ

Google Home miniでお手軽AIスピーカーを購入してIFTTTで連携して色々ハックして遊べる!