tokuhirom's blog

コンテナ型の内部にラッパー型が入っている時にmockitoのverifyメソッドのエラー表示がわかりにくいのをなおした

たとえば以下のケース。Map の中に long が入っていることを検証しているのですが、実際にはいっているのは Integer。

@Test
public void foo() {
    Foo m = mock(Foo.class);
    m.foo(new HashMap<String, Object>(){{
        put("hoge", 4);
    }});
    verify(m).foo(new HashMap<String, Object>(){{
        put("hoge", 4L);
    }});
}

public static class Foo {
    void foo(Map<String, Object> map) {
    }
}

この場合、以下のような表示となり、「違わないやんけ!!」となり血管が破裂しそうになります(なりません)。

Argument(s) are different! Wanted:
foo.foo(() {hoge=4});
-> at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
Actual invocation has different arguments:
foo.foo(() {hoge=4});
-> at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

これが以下のように L suffix がついていれば問題点が歴然となり、血管に優しいのではないか、と考えました。

Argument(s) are different! Wanted:
foo.foo({"hoge"=4L});
-> at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
Actual invocation has different arguments:
foo.foo({"hoge"=4});
-> at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

https://github.com/mockito/mockito/pull/571#issuecomment-241416329 そういうわけで、p-r を送ったところ、取り込まれましたので mockito 2.0 では問題点が修正されることかと思います(いつでるんや〜)

Created: 2016-08-24 21:53:05 +0000
Updated: 2016-08-24 21:53:05 +0000

spring boot で logging の設定のやり方まとめ

spring boot にはデフォルトでロギング機構が付いている。spring-boot-starter-web の依存に spring-boot-starter-logging 入ってるので、web 有効にしてたら自動でロギング機構も依存に入っている。デフォルトのバックエンドは logback である。

パッケージごとの loglevel の設定は application.yml で出来る。

logging.level.org.springframework.web.servlet.PageNotFound: ERROR

しかし、例えば logback の appender を追加したいなどの場合、もはや logback.xml を設定するしかない。XML で記述するのは苦行だが、耐え忍ぶしかない。(XML ではなく groovy でも設定できるが、groovy の方が情報量が少なく、辛い)。なお、設定ファイルは logback.xml ではなく logback-spring.xml という名前で置くのが spring 流。

logback-spring.xml を置かない場合、デフォルトだと以下の logback-spring.xml を設置されてるのと同じことである。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml" />
    <property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml" />
    <include resource="org/springframework/boot/logging/logback/file-appender.xml" />
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
    </root>
</included>

最近の実装では、実際には org.springframework.boot.logging.logback.DefaultLogbackConfiguration で設定されている。 https://github.com/spring-projects/spring-boot/blob/master/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java

Java コードで記述することで 100msec ほど起動速度を削減できているとのこと。 https://github.com/spring-projects/spring-boot/issues/1796

springProfile

さて、spring boot では logback-spring.xml の中で springProfile というタグを記述することで、「特定の profile 下でのみ動作する」みたいな条件つけられるので、これを利用すると良い。

<springProfile name="staging">
    <!-- 'staging' のときに有効 -->
</springProfile>

<springProfile name="dev, staging">
    <!--'dev' または 'staging' のときに有効 -->
</springProfile>

<springProfile name="!production">
    <!--'production' じゃない時に有効 -->
</springProfile>

ファイルにログを出力する設定

src/main/resources/logback/appender/logback-file.xml とかに以下のようなファイルを設置する。

ファイルを出力するディレクトリは、logback の設定ファイルには記述しない。記述したとしてもデフォルト値を設定し、system property などで外から変更できるようにしておくのが望ましい。jar の外から変更できるのが本筋だと思う。logging.path というプロパティから、LOG_PATH という変数が spring.boot により設定されているので、これをベースのディレクトリとして利用している。

java 起動時に -Dlogging.path=/path/to/logs のように指定するのが良いと思う。

<included>
  <!-- ファイルに出力するロガー -->

  <!--
   LOG_PATH は spring boot が設定している。logging.path から取得される値。
   project 名は -Dproject= で java 起動時に指定。
   -->
  <property name="APP_LOG_FILENAME" value="${LOG_PATH}/application/${project}.log"/>

  <appender name="APPLICATION_LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${APP_LOG_FILENAME}</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${APP_LOG_FILENAME}.%d{yyyyMMdd}.gz</fileNamePattern>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder>
      <charset>UTF-8</charset>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%t]%X{request_thread} %logger{45}:%L - %msg %n</pattern>
    </encoder>
  </appender>

  <appender name="ASYNC_APPLICATION_LOG_FILE" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="APPLICATION_LOG_FILE"/>
  </appender>
</included>

jetty のアクセスログを取る

jetty のアクセスログをファイルに書いておく設定がこれです。

src/main/resources/logback/appender/logback-jettyaccesslog.xml とかに置くと良いでしょう。

<included>
  <!-- jetty の access log を出力する -->

  <!--
   LOG_PATH は spring boot が設定している。logging.path から取得される値。
   project は -Dproject= で設定。supervisord.ini で設定している。
   -->
  <property name="JETTY_LOG_FILE" value="${LOG_PATH}/jetty/${project}-request.log"/>

  <appender name="REQUEST_LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${JETTY_LOG_FILE}</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${JETTY_LOG_FILE}.%d{yyyyMMdd}.gz</fileNamePattern>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder>
      <pattern>%msg %X%n</pattern>
    </encoder>
  </appender>

</included>

最終的な logback-spring.xml の設定

最終的に logback-spring.xml を以下のように設定すると良い。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <!-- http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-logging.html -->

  <!-- spring boot の設定を読む -->
  <include resource="org/springframework/boot/logging/logback/defaults.xml" />
  <include resource="org/springframework/boot/logging/logback/console-appender.xml" />

  <!--ローカル環境及びテスト環境では、INFO レベル以上を console に出力する -->
  <springProfile name="test,local">
    <root level="INFO">
      <appender-ref ref="CONSOLE"/>
    </root>
  </springProfile>

  <springProfile name="release, staging">
    <!-- release,staging 環境では、アプリケーションログはログファイルに書く。ログレベルは INFO -->
    <include resource="logback/appender/logback-file.xml"/>
    <root level="INFO">
      <appender-ref ref="ASYNC_APPLICATION_LOG_FILE"/>
    </root>

    <!-- release, staging 環境では、jetty のアクセスログをファイルに書いておく。fluentd とかでログ飛ばす用 -->
    <include resource="logback/appender/logback-jetty_access_log.xml"/>
    <logger name="org.eclipse.jetty.server.RequestLog" level="INFO" additivity="false">
      <appender-ref ref="REQUEST_LOG_FILE"/>
    </logger>
  </springProfile>

</configuration>

環境ごとの特定パッケージごとのログレベルの変更は、application-release.yml などの YAML ファイルに以下のように記述するようにし、logback-spring.xml には必要以上の設定は書かないほうが良いかと思う。

logging.level.org.springframework.web.servlet.PageNotFound: ERROR
Created: 2016-08-19 03:18:19 +0000
Updated: 2016-08-19 03:18:19 +0000

俺がレビューコメントでよくつけるやつ

基本的にメンバーは良いコードを書くという前提で暮らしています。

  • 良いと思います。テストがあるともっと良いと思います。
    • 変更が軽微で、まあテストなくてもいいけど、あったほうがより良いよねえ、という時に使う
  • 動いてるんならいいと思います
    • 処理が複雑なので、実際に動かしてみないとうまく動くかどうかコードをパッと見ただけだったらよくわからないけど、動いてるんならちゃんと動いてるんだろうな、という気がするときに使います。
  • lgtm
    • シフトキーをオスのもめんどくさいなって時には小文字で書きます。
  • コメントが無いとあとでなんだかわからなくなっちゃうかも?
    • ちょっと複雑な処理なのにコメントないときとか
  • wiki へのリンクを貼ってくだされ~
    • 仕様がちょっと複雑だったり、後から経緯おえないと死ぬな、という時に言います
  • 日本語でOK
    • 英語でコメントついてるけど、コメントの内容が端折り過ぎで説明不足なときにいいます
Created: 2016-08-04 02:48:15 +0000
Updated: 2016-08-04 02:48:15 +0000

spring boot + embedded tomcat で async request を graceful shutdown する

graceful shutdown 厨みたいになってますが、まあいいとして。。 embedded tomcat はちょっと見た感じ async request を綺麗に graceful shutdown する方法が見当たらなかった。

結局、各コントローラなりサーブレットなりで個々に graceful shutdown するのが良さそう。 例えば以下のように。

package com.example;

import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.Connector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;

import javax.annotation.PreDestroy;
import java.util.concurrent.*;

@SpringBootApplication
@Slf4j
public class SpringBootTomcatApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootTomcatApplication.class, args);
    }

    @RestController
    public static class MyController {
        private final ExecutorService pool = Executors.newFixedThreadPool(10);

        @GetMapping("/")
        public String ok() {
            return "OK";
        }

        @GetMapping("/sleep")
        public DeferredResult<String> sleep() {
            DeferredResult<String> objectDeferredResult = new DeferredResult<>();
            pool.submit(() -> {
                try {
                    log.info("Sleeping");
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                log.info("Send result");
                objectDeferredResult.setResult("OK");
            });
            return objectDeferredResult;
        }

        /**
         * Graceful shutdown.
         */
        @PreDestroy
        public void stop() {
            log.info("STOP");
            pool.shutdown();
            try {
                if (!pool.awaitTermination(7, TimeUnit.SECONDS)) {
                    log.info("shutdown failed");
                    pool.shutdownNow();
                } else {
                    log.info("shutdown succeeded");
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

jetty や undertow なら全体で処理できるからそれでもいいけど、まあ基本的には各コントローラでやるほうが綺麗かも。 あるいは処理が okhttp 等の場合だと、各 Service なりなんなりで @PreDestroy でやるのがいいかも。

【追記】

ThreadPoolTaskExecutor 使えばいいと @making さんと @kazuki43zoo さんから聞いたので、そうしてみたらすっきりしました。 (やってることは内部的には同じ)

package com.example;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;

import javax.annotation.PreDestroy;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@SpringBootApplication
@Slf4j
public class SpringBootTomcatApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootTomcatApplication.class, args);
    }

    @Bean(name = "async1")
    public AsyncTaskExecutor mvcAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setMaxPoolSize(10);
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(7);
        return executor;
    }

    @RestController
    public static class MyController {
        @Autowired
        @Qualifier("async1")
        AsyncTaskExecutor asyncTaskExecutor;

        @GetMapping("/")
        public String ok() {
            return "OK";
        }

        @GetMapping("/sleep")
        public DeferredResult<String> sleep() {
            DeferredResult<String> objectDeferredResult = new DeferredResult<>();
            asyncTaskExecutor.submit(() -> {
                try {
                    log.info("Sleeping");
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                log.info("Send result");
                objectDeferredResult.setResult("OK");
            });
            return objectDeferredResult;
        }
    }
}
Created: 2016-07-30 01:18:03 +0000
Updated: 2016-07-30 01:18:03 +0000

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().interrupt();
                }

                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
Next page