tokuhirom's blog

undertow と graceful shutdown と

undertow を利用する予定は特にないのだが、undertow だと graceful shutdown はどのように実現可能なのだろうか、ということが気になったので調べてみました。 ここでいう graceful shutdown は listen socket を close したうえで、処理をすべて正常に終了し、終了後にプロセスを exit するようなものを指しています。

いくつかハマりどころがあるので注意。

undertow はドキュメントが貧弱

どうも、貧弱ですね。。ソース読めないと厳しい。ソースか javadoc 眺めて探すとかしないと見つからない。利用方法ものってないからテストから探すとかしないといけない。

DeploymentManager.stop() を呼ばないと Servlet#destroy が呼ばれない

これは、Java EE になれた人だと常識なんだろうけど、DeploymentManager.stop() を明示的に呼ばないと Servlet#destroy が呼ばれないんでクリーンアップ処理が正常に処理されません。shutdown まわりの正しい呼び出し手順が undertow のマニュアルには載ってないので割と困る。

frsyuki++ に教えてもらいました。

GracefulShutdownHandler を利用することで graceful shutdown が可能。

可能なのだが、ドキュメントがないのでテストコードとソースコードから把握する必要がある。

まず、GracefulShtudownHandler#shutdown を呼ぶと shutdown 状態になります。shutdown 状態になるとすべてのリクエストに 503 を返すようになります。 GracefulShutdownHandler#awaitShutdown を呼ぶと、すべてのリクエストが終了するまで待ってくれます。すべてのリクエストが止まったあとにサーバーを止めればOK、ということっぽいです。

しかし、HTTP レベルでのレスポンスを返してしまうので load balancer になんとなくバランスさせるとかは難しい。明示的に load balancer から外すようにしないとダメでしょう(そして、明示的に外すのは nginx だとちょっと面倒なことしないとできない)。health check モジュール入れて、health check の結果に false を返したあとで GracefulShutdownHandler#shutdown 呼んで、、とかすればできる。

ちょっと頑張れば graceful shutdown できる

  • AcceptingChannel.close()
    • listen channel を閉じる
  • GracefulShutdownHandler.shutdown()
    • 新規のリクエストには 503 を返す
  • GracefulShutdownHandler.awaitShutdown()
    • 実行中のすべてのリクエストが処理完了するまで待つ
  • XnioWorker.shutdown()
    • 処理ワーカーを閉じる

という順番で閉じていけばOK。

Undertow.stop() は AcceptingChannel を close して worker を shutdown するところまで一気にやってしまうので使えない。 もともと Undertow クラスは、便利クラスってだけなので無理して利用しなくても良い。

サンプルコードは以下。

package com.example;

import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.UndertowOptions;
import io.undertow.connector.ByteBufferPool;
import io.undertow.server.DefaultByteBufferPool;
import io.undertow.server.handlers.GracefulShutdownHandler;
import io.undertow.server.handlers.PathHandler;
import io.undertow.server.protocol.http.HttpOpenListener;
import io.undertow.servlet.Servlets;
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.api.DeploymentManager;
import io.undertow.servlet.api.InstanceHandle;
import lombok.extern.slf4j.Slf4j;
import org.xnio.*;
import org.xnio.channels.AcceptingChannel;

import javax.servlet.AsyncContext;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Slf4j
public class UndertowApp {
    public static void main(String[] args) throws ServletException, InterruptedException, IOException {
        new UndertowApp().run();
    }

    private void run() throws ServletException, InterruptedException, IOException {
        CountDownLatch latch = new CountDownLatch(1);

        DeploymentInfo servletBuilder = Servlets.deployment()
                .setClassLoader(UndertowApp.class.getClassLoader())
                .setContextPath("/")
                .setDeploymentName("async.war")
                .addServlets(
                        Servlets.servlet("MessageServlet", AsyncResponseServlet.class, () -> new InstanceHandle<Servlet>() {
                            @Override
                            public Servlet getInstance() {
                                log.info("Getting instance");
                                return new AsyncResponseServlet(latch);
                            }

                            @Override
                            public void release() {
                                /* nop */
                            }
                        })
                                .setAsyncSupported(true)
                                .addInitParam("message", "Hello World")
                                .addMapping("/*")
                );

        DeploymentManager manager = Servlets.defaultContainer()
                .addDeployment(servletBuilder);
        manager.deploy();
        PathHandler path = Handlers
                .path()
                .addExactPath("/", manager.start());

        int ioThreads = Math.max(Runtime.getRuntime().availableProcessors(), 2);
        int workerThreads = ioThreads * 8;

        Xnio xnio = Xnio.getInstance(Undertow.class.getClassLoader());

        XnioWorker worker = xnio.createWorker(OptionMap.builder()
                .set(Options.WORKER_IO_THREADS, ioThreads)
                .set(Options.CONNECTION_HIGH_WATER, 1000000)
                .set(Options.CONNECTION_LOW_WATER, 1000000)
                .set(Options.WORKER_TASK_CORE_THREADS, workerThreads)
                .set(Options.WORKER_TASK_MAX_THREADS, workerThreads)
                .set(Options.TCP_NODELAY, true)
                .set(Options.CORK, true)
                .getMap());

        OptionMap socketOptions = OptionMap.builder()
                .set(Options.WORKER_IO_THREADS, ioThreads)
                .set(Options.TCP_NODELAY, true)
                .set(Options.REUSE_ADDRESSES, true)
                .set(Options.BALANCING_TOKENS, 1)
                .set(Options.BALANCING_CONNECTIONS, 2)
                .set(Options.BACKLOG, 1000)
                .getMap();

        OptionMap serverOptions = OptionMap.builder()
                .set(UndertowOptions.NO_REQUEST_TIMEOUT, 60000000)
                .getMap();

        ByteBufferPool buffers = new DefaultByteBufferPool(true, 1024 * 16, -1, 4);

        GracefulShutdownHandler gracefulShutdownHandler = Handlers.gracefulShutdown(path);

        OptionMap undertowOptions = OptionMap.builder().set(UndertowOptions.BUFFER_PIPELINED_DATA, true).addAll(serverOptions).getMap();
        HttpOpenListener openListener = new HttpOpenListener(buffers, undertowOptions);
        openListener.setRootHandler(gracefulShutdownHandler);
        ChannelListener<AcceptingChannel<StreamConnection>> acceptListener = ChannelListeners.openListenerAdapter(openListener);
        AcceptingChannel<? extends StreamConnection> server = worker.createStreamConnectionServer(new InetSocketAddress(Inet4Address.getByName("localhost"), 18080), acceptListener, socketOptions);
        server.resumeAccepts();

        log.info("Send request");
        Thread clientThread = new Thread(this::startClient);
        clientThread.setName("http client");
        clientThread.start();

        log.info("Waiting request");
        latch.await();

        log.info("Stopping listening channel");
        IoUtils.safeClose(server);

        log.info("Entering shutdown state");
        gracefulShutdownHandler.shutdown();

        log.info("Await all requests(7sec)");
        gracefulShutdownHandler.awaitShutdown(7 * 1000);

        log.info("Shutdown workers");
        worker.shutdown();

        log.info("Stopped");

        manager.stop();
        manager.undeploy();

        log.info("Undeployed");

        log.info("joining client thread");
        clientThread.join();
    }

    private void startClient() {
        try (Socket clientSocket = new Socket("localhost", 18080);
             DataOutputStream outToServer = new DataOutputStream(clientSocket.getOutputStream())
        ) {
            outToServer.write("GET / HTTP/1.0\015\012Content-Length: 0\015\012\015\012".getBytes(StandardCharsets.UTF_8));
            log.info("Sent request");
            clientSocket.shutdownOutput();

            StringBuilder builder = new StringBuilder();
            while (true) {
                byte[] buf = new byte[1024];
                int read = clientSocket.getInputStream().read(buf);
                if (read == -1) {
                    log.info("Got response: {}, {},{},{}, {}",
                            builder.toString(),
                            clientSocket.isConnected(),
                            clientSocket.isBound(),
                            clientSocket.isInputShutdown(),
                            clientSocket.isClosed());
                    break;
                }
                builder.append(new String(buf, 0, read)).append("\\n");
            }
        } catch (IOException e) {
            log.error("IOException", e);
            throw new UncheckedIOException(e);
        }
    }

    @Slf4j
    public static class AsyncResponseServlet extends HttpServlet {
        private final ExecutorService pool = Executors.newFixedThreadPool(10);
        private final CountDownLatch latch;

        public AsyncResponseServlet(CountDownLatch latch) {
            this.latch = latch;
        }


        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            AsyncResponseServlet.log.info("Got request: {}", req.getPathInfo());
            this.latch.countDown();

            AsyncContext asyncContext = req.startAsync();

            pool.submit(() -> {
                AsyncResponseServlet.log.info("Sleeping...");
                try {
                    Thread.sleep(3L * 1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupted();
                }

                AsyncResponseServlet.log.info("Sending response");
                resp.setStatus(200);
                try {
                    resp.getWriter().print("OK\n");
                } catch (IOException e) {
                    AsyncResponseServlet.log.error("Can't send response", e);
                } finally {
                    AsyncResponseServlet.log.info("complete async thread");
                    asyncContext.complete();
                }
            });
        }

        @Override
        public void destroy() {
            AsyncResponseServlet.log.info("Shutdown servlet");
            this.pool.shutdown();
        }
    }
}
Created: 2016-07-27 00:52:26 +0000
Updated: 2016-07-27 00:52:26 +0000

hs_err_pidの生成タイミング

hserrpid は、jvm がクラッシュしたタイミングで生成されるが、実際どのようなタイミングで生成されるのかという話。

http://hg.openjdk.java.net/jdk9/jdk9/hotspot/file/tip/src/share/vm/utilities/vmError.cpp#l285 の reportanddie で生成されているとのこと。

VMError::resetsignalhandlers で crashhandler が以下の signals に対して設定される。see VMError::resetsignal_handlers

static const int SIGNALS[] = { SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGTRAP }; // add more if needed
static const int NUM_SIGNALS = sizeof(SIGNALS) / sizeof(int);
Created: 2016-07-22 23:18:34 +0000
Updated: 2016-07-22 23:18:34 +0000

mybatis の mapper を groovy で書くぐらいなら kotlin でも良いのではないか

mybatis の xml は painful なので、groovy で書く、というライフハックがあるようです。 これはとても良いハックなのですが、最近の情勢を考えると、groovy よりも kotlin で書いておいたほうが良いのかなという気がしなくもない。 kotlin の方が勢いがあり、IDEA のサポートも今後 kotlin のほうが受けやすそうですし。

というわけで、kotlin で書いてみるデモコードを書いてみました。

package com.example.dao

import com.example.entity.Blog
import org.apache.ibatis.annotations.Mapper
import org.apache.ibatis.annotations.Param
import org.apache.ibatis.annotations.Select

@Mapper
interface BlogDao {
    @Select("""
        SELECT * FROM blog
    """)
    fun findAll(): List<Blog>

    @Select("""
        SELECT * FROM blog WHERE id=#{id}
    """)
    fun findById(@Param("id") id: Long): Blog
}

メソッドの定義にやや癖があるものの、今後 kotlin を利用するケースが増えることを考えれば、許容範囲かなといったところ。

フルのサンプルコードはこちら: https://github.com/tokuhirom/java-samples/tree/master/spring-boot-mybatis-kotlin

Created: 2016-07-20 00:30:13 +0000
Updated: 2016-07-20 00:30:13 +0000

jailing を golang に移植した

なぜ golang に移植したかというとまあ以下のような理由です。

https://github.com/tokuhirom/jailingo/

  • capabilities で制御したかった
    • jailing は perl で記述されているために、setuid/setcap などで制御することが難しい
    • 今時 suid-perl 使うのもなあ、という。
  • C で書くと getopt の処理とかだるい
  • C++ で書くと、ビルドとかで悩むのがめんどい
  • golang は segv しにくくて楽

golang で記述することにより、良い面もあるが一方で、普通に golang で記述すると、clone() したあとに処理を挟むことができないので、CLONE_NEWPID | CLONE_NEWNS とかしたあとに mount したいという要望が叶えられない。ので、いったん /proc/self/exec を実行しなおして、procfs をマウントしてから子プロセスを起動する、みたいな処理が必要になってしまう。ちょっと面倒。

と、一応書いてみたが、実際には proot を利用しているのだった。


↑↑ 結局、proot 遅すぎたので jailingo 使うようにした。

Created: 2016-07-17 10:45:52 +0000
Updated: 2016-07-17 10:45:52 +0000

OSS版 Ansible Tower alternative である Ansible Semaphore を試したぞ!

Ansible Tower の OSS Alternative であるところの Ansible Semaphore を試した。 Ansible を利用してデプロイするのの web ui である。

installation guide 通りにやれば利用可能。

docker run -d --name=mysql -p 127.0.0.1:3306:3306 -e MYSQL_ROOT_PASSWORD=my-secret-pw mysql

などとして mysqld を立ち上げておく(もちろん、別に docker じゃなくてもいい)。

https://github.com/ansible-semaphore/semaphore/releases から最新版のバイナリを取得(golang なので single binary)。

semaphore -setup

として、起動。セットアップが始まる。セットアップは以下の様な感じ。

$ ~/Downloads/semaphore_darwin_amd64 -setup

 Hello! You will now be guided through a setup to:

 1. Set up configuration for a MySQL/MariaDB database
 2. Set up a path for your playbooks (auto-created)
 3. Run database Migrations
 4. Set up initial seamphore user & password

 > DB Hostname (default 127.0.0.1:3306):
 > DB User (default root):
 > DB Password: my-secret-pw
 > DB Name (default semaphore):
 > Playbook path:

 Generated configuration:
 {
        "mysql": {
                "host": "127.0.0.1:3306",
                "user": "root",
                "pass": "my-secret-pw",
                "name": "semaphore"
        },
        "port": "",
        "bugsnag_key": "",
        "tmp_path": "/tmp/semaphore",
        "cookie_hash": "3aIKJIwVCvXH6vLGb8xhQHMr90OeVZ1SimYMNJBWD+A=",
        "cookie_encryption": "TDFElfq2JZN5PfTfPedRjgMZ+MZUY9VgNTZCS/sGJTY="
 }

 > Is this correct? (yes/no): yes
 Running: mkdir -p /tmp/semaphore..
 Configuration written to /tmp/semaphore/semaphore_config.json..
 Pinging database..

 Running DB Migrations..
Creating migrations table
Executing migration v0.0.0 (at 2016-06-25 08:44:13.784368138 +0900 JST)...
 [11/11]
Executing migration v1.0.0 (at 2016-06-25 08:44:14.340635482 +0900 JST)...
 [7/7]
Executing migration v1.1.0 (at 2016-06-25 08:44:14.752282549 +0900 JST)...
 [1/1]
Executing migration v1.2.0 (at 2016-06-25 08:44:14.861796122 +0900 JST)...
 [1/1]
Executing migration v1.3.0 (at 2016-06-25 08:44:14.918184638 +0900 JST)...
 [3/3]
Executing migration v1.4.0 (at 2016-06-25 08:44:15.103055625 +0900 JST)...
 [2/2]
Executing migration v1.5.0 (at 2016-06-25 08:44:15.220915164 +0900 JST)...
 [1/1]
Executing migration v0.1.0 (at 2016-06-25 08:44:15.279912624 +0900 JST)...
 [6/6]

 > Username: foobar
 > Email: foobar@example.com
 > Your name: Foo Bar
 > Password: *********

 You are all setup Foo Bar!
 Re-launch this program pointing to the configuration file

./semaphore -config /tmp/semaphore/semaphore_config.json

 To run as daemon:

nohup ./semaphore -config /tmp/semaphore/semaphore_config.json &

 You can login with foobar@example.com or foobar.

設定ファイルが生成されるんで、メッセージどおりにそれを使って起動すれば OK.

ここまでは極めて簡単にできる。

現時点の最新版である v2.0.2 には致命的な問題がある(全くデプロイできない)ので、困ったなーって思って git log 見たら HEAD では治ってたので、リリースしてよ、って言ったら音速でリリースされた。

が、v2.0.2 でエンバグしていたので、パッチを送ってみたが、そうじゃないって言われたので様子を見ている。たぶんためそうと思ってる人はv2.0.3まで待つのが良い。 https://github.com/ansible-semaphore/semaphore/issues/142

開発

patch 送ったりするにはどーすりゃいいか。golang の開発環境はすでにあるとして、、

circle.yml の以下の部分を参考に、手でインストールする。。 https://github.com/ansible-semaphore/semaphore/blob/master/circle.yml#L7-L17

そしたらば、./make.sh watch とすると、ファイル変更すると自動で更新がかかる感じで、プロセスが起動する。

ってだけ。

利用手順

まず、Dashboard を見て、プロジェクトを追加します。 https://gyazo.com/b696178e7abea2ed4297ba65197f4698

次に、Key Store に ssh key を登録します。ここに登録した ssh key は、ログインと git の clone の両方に利用されます。 https://gyazo.com/8eaefe8f15641469d535517226c81d5b

playbook 登録してるレポジトリを登録しまうす。git clone されるだけなので、file:// でもなんでもOK. https://gyazo.com/e2e5d675faa41fbaffd2b0b5636075ca

インベントリを登録します。これは ansible コマンドでいうところの ansible-playbook -i www playbook.yml の www にあたる部分ですね。 https://gyazo.com/adb05fbf1c02e19057635dd3ed3110a4

Environment を登録します。ansible 内で variable として利用可能になる。のかな?よくわかってない。 https://gyazo.com/27cc47602aed92ce743cb438ab936649

最後にこれらの要素を利用して、タスクを登録。 https://gyazo.com/7d49c11134e177bbb8f98f0471dfb97c

run ボタン押すと、タスク実行画面が起動する。ここで、変数をいじったりとかできるので便利。 https://gyazo.com/6c3ab1ea2f72261161533e67258e208f

実行すると、普通に動く。 https://gyazo.com/7046532dee6aab89dab50290a9759b97

まとめ

v2 が出たばかりで v2 は荒削りだが、アーキテクチャは綺麗なので良さそう。 go+ansible みたいな構成になってていじりやすい。コードは綺麗。インストールも楽。 今後に期待がモテる。

Created: 2016-06-25 01:06:38 +0000
Updated: 2016-06-25 01:06:38 +0000

circleci_ikachan を書いた

https://github.com/tokuhirom/circleci_ikachan

circle ci の webhook を受け取って、ikachan に forward するやつです。 ちょっと必要だったので書きました。

今回は、go で書いてみました。

Created: 2016-06-24 23:33:06 +0000
Updated: 2016-06-24 23:33:06 +0000

Promgen talk - Prometheus casual talks

I promoted promgen, a prometheus configuration management console web app at Prometheus Casual Talks.

I'll be release it as OSS application.

Created: 2016-06-17 03:13:52 +0000
Updated: 2016-06-17 03:13:52 +0000

How do I display custom element with IncrementalDOM?

Incremental DOM is great library to build dynamic DOM tree.

If you want to build DOM tree contains custom tags, you need to call IncrementalDOM.skip() to skip patching DOM elements inside custom element.

<!doctype html>
<html>
  <head>
    <script type="text/javascript" src="node_modules/incremental-dom/dist/incremental-dom.js"></script>
  </head>
<body>

<div id="container"></div>

<script>
document.registerElement('x-bar', (function () {
  var proto = Object.create(HTMLElement.prototype);
  proto.createdCallback = function () {
    IncrementalDOM.patch(this, function () {
      IncrementalDOM.elementOpen('div');
      IncrementalDOM.text('hello');
      IncrementalDOM.elementClose('div');
    });
  };
  return {prototype:proto};
})());

const container = document.getElementById('container');
for (let i=0; i<3; ++i) {
  IncrementalDOM.patch(container, function () {
    IncrementalDOM.elementOpen('x-bar');
    IncrementalDOM.skip();
    IncrementalDOM.elementClose('x-bar');
  });
}

</script>

</body>
</html>
Created: 2016-06-14 06:25:16 +0000
Updated: 2016-06-14 06:25:16 +0000

x-tag で delegate しているときに root element を取得する方法

http://stackoverflow.com/questions/30895067/x-tag-event-delegation-accessing-the-root-element

xtag.register('x-foo', {
  content: '<input /><span></span>',
  events: {
    focus: function(e){
      // e.currentTarget === your x-foo element
    },
    'tap:delegate(span)': function(e){
      // e.currentTarget still === your x-foo element
      // 'this' reference set to matched span element
    }
  }
});

のように e.currentTarget を見れば取得可能。

Created: 2016-06-06 23:28:25 +0000
Updated: 2016-06-06 23:28:25 +0000

spring-data-elasticsearch について

ES 2 が出てから半年以上経過しているにもかかわらず 2 対応がされていない。 https://www.elastic.co/blog/elasticsearch-2-0-0-released https://github.com/spring-projects/spring-boot/issues/4339

クエリをすべてラップしているために 2 対応がなかなか難しいのだと思う。

Elasticsearch の場合、1系用のクライアントライブラリで2系にアクセスすると接続拒否されるし、なかなか厳しい。。

_score の取得が一筋縄ではいかない。 http://stackoverflow.com/questions/35175319/spring-data-elasticsearch-metadata-score

もともと elasticsearch 自体の java client は良く出来ているので、あまりラッパをかます必要がないのになぜかましてしまったのか。。

「楽になっている部分 < 負担になっている部分」という感じで、採用メリットが薄いし、後々負債になる可能性が高いと思う。 (この手の「便利なラッパークラス」にありがちだが)

Created: 2016-06-03 22:52:45 +0000
Updated: 2016-06-03 22:52:45 +0000

[prometheus][java] Added Spring Boot Metrics integration to Prometheus' simpleclient_java

https://github.com/prometheus/client_java/pull/114

I sent p-r for clientjava repository ... I want to export spring boot metrics to simpleclientservlet. The p-r was merged in master branch. The patch will include in next release.

Created: 2016-05-26 05:02:30 +0000
Updated: 2016-05-26 05:02:30 +0000

[golang] json_path_scanner 書いた

https://github.com/tokuhirom/json_path_scanner

JSON を読み取ったデータ構造を食わせると、「JSON Path」と「値」のペアのリストを得られるというやつ。 すでにありそうだったけど見当たらなかったので書いた。

Created: 2016-05-22 00:19:58 +0000
Updated: 2016-05-22 00:19:58 +0000

[prometheus] apache_exporter なおした

https://github.com/neezgee/apache_exporter Prometheus で apache を監視するための agent として apache_exporter があるが、これが全く動いてなかったので、動くようになおしておきました。

Created: 2016-05-18 00:18:37 +0000
Updated: 2016-05-18 00:18:37 +0000

[ruby] 10s 10m みたいな文字列から秒数を求めるには chronic_duration を使う

https://github.com/hpoydar/chronic_duration

なにかの実行間隔のような設定がある場合、chronicduration を利用してパースすることができる。 実行例は以下の通り。integer をそのまま渡すとエラーになるので、integer が渡ってきそうなケースでは tos して渡すようにするのがよさそう。 パースできない場合には nil が返ってくるので、nil もケアしてあげる必要があります。

[1] pry(main)> require 'chronic_duration'
=> true
[2] pry(main)> ChronicDuration.parse(3)
NoMethodError: undefined method `downcase' for 3:Fixnum
from /Users/tokuhirom/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/chronic_duration-0.10.6/lib/chronic_duration.rb:181:in `cleanup'
[3] pry(main)> ChronicDuration.parse(3.to_s)                                                                                                  
=> 3
[4] pry(main)> ChronicDuration.parse('3')                                                                                                      
=> 3
[5] pry(main)> ChronicDuration.parse('3s')
=> 3
[6] pry(main)> ChronicDuration.parse('3m')                                                                                                     
=> 180
[7] pry(main)> ChronicDuration.parse('3h')                                                                                                     
=> 10800
[8] pry(main)> 3*60*60
=> 10800
[9] pry(main)> ChronicDuration.parse("carrot")
=> nil
Created: 2016-05-17 01:21:38 +0000
Updated: 2016-05-17 01:21:38 +0000

sequel の migration に関するメモ

Sequel には migration 機能がついている。これを利用するには以下のようなファイルを作成する。

Sequel.migration do
  change do
    create_table(:artists) do
      primary_key :id
      String :name, :null=>false
    end
  end
end

ファイル名は db/migrations/001_init.rb とかにする。

rakefile に以下のように書く。DATABASE_URL=sqlite://hoge rake db:migrate とかすればマイグレーションされる。 namespace :db do desc "Run migrations" task :migrate, [:version] do |t, args| require "sequel" Sequel.extension :migration db = Sequel.connect(ENV.fetch("DATABASE_URL")) if args[:version] puts "Migrating to version #{args[:version]}" Sequel::Migrator.run(db, "db/migrations", target: args[:version].to_i) else puts "Migrating to latest" Sequel::Migrator.run(db, "db/migrations") end end end

と、公式のマニュアルに書いてあった。 https://github.com/jeremyevans/sequel/blob/master/doc/migration.rdoc

Created: 2016-05-15 09:32:54 +0000
Updated: 2016-05-15 09:32:54 +0000

個人的な grafana に対する不満トップ3

Editable をオフにすると戻せない

https://github.com/grafana/grafana/issues/2554

You can make it editable again using the Save As... feature and enter the same name. I agree though, there should be an easier way to make it editable.

えぇ~ そんなことってある? ッて感じのISSUE。

This is done and available in nightly and soon grafana 3.0 とのことなので 3.0 を正座して待つしかない

ダッシュボードの設定変更履歴が把握できないし、誰かがいじっても戻せない

https://github.com/grafana/grafana/issues/4638 誰かがいじっちゃったとしてそれを把握したり復元したりできないと辛い。。

4.0 まででなさそうで辛い。

Home Dashboard のロゴ変えられない

変えたい。

Created: 2016-05-10 10:21:23 +0000
Updated: 2016-05-10 10:21:23 +0000

CPU の system が妙に高いぞって時に犯人さがすには pidstat 使えば良い。

pidstat -h -u | sort -nr -k 5,5 | head すれば良い。

sudo yum install -y sysstat してインストール。

簡単に誰が system 消費してるかわかる [tokuhirom@centos-1gb-sgp1-01 ~]$ pidstat -h -u | sort -nr -k 5,5 | head 1462874412 0 27 0.00 0.22 0.00 0.22 0 kswapd0 1462874412 0 19281 0.16 0.09 0.00 0.25 0 cadvisor 1462874412 0 342 0.01 0.02 0.00 0.03 0 systemd-journal 1462874412 0 377 0.00 0.01 0.00 0.01 0 auditd 1462874412 0 262 0.00 0.01 0.00 0.01 0 jbd2/vda1-8 1462874412 0 16958 0.02 0.01 0.00 0.02 0 tuned 1462874412 0 13775 0.00 0.01 0.00 0.01 0 xfsaild/dm-9 1462874412 0 11 0.00 0.01 0.00 0.01 0 rcuos/0 1462874412 0 10 0.00 0.01 0.00 0.01 0 rcu_sched

Created: 2016-05-10 10:01:11 +0000
Updated: 2016-05-10 10:01:11 +0000

blog のバグを直した

ブログシステムをうっかり書き直したらちょっとバグってたので直した。

  • ctime/mtime を timestamp で保持していたが git なので保持されてなくて死んでたのでファイルの中に ctime/mtime を入れるようにした
  • entry の個別ページの title タグがちゃんと入ってなかったので入れた
Created: 2016-05-09 14:40:25 +0000
Updated: 2016-05-09 14:40:25 +0000

[java][linux] Java の各プロセスのスレッド数を得るワンライナー

jps -l | ruby -ne 'pid, cmd = $_.chomp.split(/ /); puts pid + "\t" + File.read("/proc/#{pid}/stat").split(/ /)[19] + "\t" + cmd'

linux の procfs から取得していくスタイル

Created: 2016-05-09 10:08:27 +0000
Updated: 2016-05-09 10:08:27 +0000

javaagent として動く java のサンプラを書いた

java アプリケーションのサンプリングを簡単にやるための簡単なライブラリを書いた。javaagent の習作的なやつです。

https://github.com/tokuhirom/java-samples/tree/master/nanojsampler

たんに、定期的に Thread.getAllStackTraces() して、よしなに集計した結果を jmx に流し込んでいる。

package me.geso.nanojsampler;

import javax.management.*;
import java.lang.instrument.Instrumentation;
import java.lang.management.ManagementFactory;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

public class Nanojsampler {
    private final Map<StackTraceElement, Integer> map = new ConcurrentHashMap<>();

    public static void premain(String agentArgs, Instrumentation inst) throws MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanRegistrationException {
        int sleepMillisec = Integer.parseInt(agentArgs);
        Nanojsampler nanojsampler = new Nanojsampler();
        nanojsampler.initJmx();

        Thread thread = new Thread(() -> {
            try {
                while (true) {
                    nanojsampler.poll();
                    Thread.sleep(sleepMillisec);
                }
            } catch (InterruptedException ignored) {
                Thread.currentThread().interrupt();
            }
        });
        thread.setName("nanojsampler");
        thread.start();

        System.out.println("done");
    }

    public void initJmx() throws MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanRegistrationException {
        MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
        ObjectName objectName = new ObjectName("nanojsampler:type=report");
        NanojsamplerReport nanojsamplerMBean = new NanojsamplerReport(map);
        mbs.registerMBean(nanojsamplerMBean, objectName);
    }

    public void poll() {
        Map<Thread, StackTraceElement[]> allStackTraces = Thread.getAllStackTraces();
        for (Map.Entry<Thread, StackTraceElement[]> threadEntry : allStackTraces.entrySet()) {
            process(threadEntry);
        }
    }

    private void process(Map.Entry<Thread, StackTraceElement[]> threadEntry) {
        Thread thread = threadEntry.getKey();
        StackTraceElement[] traceElements = threadEntry.getValue();

        // ignore waiting threads
        if (thread.getState() == Thread.State.TIMED_WAITING || thread.getState() == Thread.State.WAITING) {
            return;
        }

        for (StackTraceElement traceElement : traceElements) {
            map.compute(traceElement,
                    (key2, oldValue) -> oldValue == null ? 1 : oldValue + 1);
        }
    }

    public interface NanojsamplerReportMBean {
        List<String> getReport();
    }

    public static class NanojsamplerReport implements NanojsamplerReportMBean {
        private final Map<StackTraceElement, Integer> map;

        public NanojsamplerReport(Map<StackTraceElement, Integer> map) {
            this.map = map;
        }

        @Override
        public List<String> getReport() {
            return this.map.entrySet().stream()
                    .sorted(Comparator.comparing(Map.Entry::getValue, Comparator.reverseOrder()))
                    .map(NanojsamplerReport::convertEntry)
                    .collect(Collectors.toList());
        }

        private static String convertEntry(Map.Entry<StackTraceElement, Integer> stackTraceElementIntegerEntry) {
            StackTraceElement element = stackTraceElementIntegerEntry.getKey();
            Integer value = stackTraceElementIntegerEntry.getValue();

            return value + "\t" + element.getClassName() + "." + element.getMethodName() + ":" + element.getFileName() + ":" + element.getLineNumber();
        }
    }
}

build.gradle に以下のように記載して、Premain-class を定義する。src/main/resources/META-INF/MANIFEST.MF に直接ファイル置いても上書きされるので注意。 jar { manifest { attributes( "Agent-Class": "me.geso.nanojsampler.Nanojsampler", "Premain-Class": "me.geso.nanojsampler.Nanojsampler") } }

集計処理をインプロセスでやっているので重くなりがちかもしれない。が、現実的にはそれほど重くはならないか。 いずれにせよサンプルコードなのでまあ良し。

Created: 2016-05-09 00:18:59 +0000
Updated: 2016-05-09 00:18:59 +0000

[docker] alpine linux を使うなら gosu じゃなくて su-exec で良い

gosu という setuidgid みたいなツールがあるが、よく README を読むと

When using Alpine, it's probably also worth checking out su-exec (apk add --no-cache su-exec), which since version 0.2 is fully gosu-compatible in a fraction of the file size.

と記載されており、su-exec を使えば良いとのこと(su-exec のほうが C なので読みやすいし)。

Created: 2016-05-08 21:10:30 +0000
Updated: 2016-05-08 21:10:30 +0000
Next page