その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

Email::MIME::ContentType が build_content_type と build_content_disposition を提供するようになっていた

Perl の話です.

metacpan.org

Email::MIME::ContentType 1.023 (なお本バージョンは TRIAL Release となっています) 以降から build_content_typebuild_content_disposition という関数が追加されています.それぞれ名前の通り Content-TypeContent-Disposition を構築する責務を担っています.

テストコードから一部拝借すると,

use Email::MIME::ContentType;

my $content_type = build_content_type({
    type => 'text',
    subtype => 'plain',
    attributes => {
        charset => 'us-ascii'
    }
}); # => 'text/plain; charset=us-ascii'

my $content_disposition = build_content_disposition({
    type => 'attachment',
    attributes => {
        filename => 'genome.jpeg',
        'modification-date' => 'Wed, 12 Feb 1997 16:29:51 -0500'
    }
}); # => 'attachment; filename=genome.jpeg; modification-date="Wed, 12 Feb 1997 16:29:51 -0500"'

というふうに使えるようになっています,ウオー便利ですね!


github.com
github.com

実に7年越しの feature request が結実した形になります *1.ありがとうございます!

*1:今となってはなぜこれが必要だったかを思い出せませんが……

Redis の Slave (Replica) の Expire は 4.0 RC3 以降信用して良くなっている

maedama.hatenablog.com
trapezoid.hatenablog.com

上記のブログには今から6年ほど前の当時の情報が記されていますが,Redis 4.0 RC3 以降の Slave (replica) の Expire は信用して良くなっているようです.
Redis の公式ドキュメント (Replication – Redis) を参照すると,

However note that writable replicas before version 4.0 were incapable of expiring keys with a time to live set. This means that if you use EXPIRE or other commands that set a maximum TTL for a key, the key will leak, and while you may no longer see it while accessing it with read commands, you will see it in the count of keys and it will still use memory. So in general mixing writable replicas (previous version 4.0) and keys with TTL is going to create issues.

Redis 4.0 RC3 and greater versions totally solve this problem and now writable replicas are able to evict keys with TTL as masters do, with the exceptions of keys written in DB numbers greater than 63 (but by default Redis instances only have 16 databases).

とあり,どうやらExpire が信用できない挙動は Redis 4.0 RC3 から修正されているようです.
どういうことかと言うと,「Slave 側ではデータは Purge しない。Master で データの Purge の Replication をまつ」という挙動が解消され,slave (replica) 側でも能動的に TTL の判断ができるようになっています.
つまり,slave (replica) に対して TTL 切れのアイテムに問い合わせたとしても,正しく expire することとなります.


というわけで検証してみます.Dockerを使ってやってみましょう.なお検証に用いたバージョンは 4.0.14 および 5.0.7 です (記載上は 4.0.14 を使用します).

まずは master (primary) 側の Redis server の Docker container を用意します.

FROM redis:4.0.14
COPY primary.redis.conf /usr/local/etc/redis/redis.conf
CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ]

この時の primary.redis.conf は以下の通り:

bind 0.0.0.0

コンテナ間通信をさせる必要があるので network を作っておきます.

$ docker network create -d bridge redis-test

作った master (primary) の Redis server を起動しましょう.

$ docker run --net=redis-test -p 6379:6379 primary-redis:latest

コンテナの IP アドレスを確認しておきます:

$ docker network inspect redis-test
[
    {
        "Name": "redis-test",
        "Id": "379ae24b35454bc119d80f35d3dd40e3db6e1ec72579ddbba32ca322cec4293a",
        "Created": "2020-05-08T02:53:19.955015821Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "e75cbce6bce2e1a797780da5c7500aaebbc1b4cf0e28865854e6ba5c46e78b80": {
                "Name": "amazing_greider",
                "EndpointID": "a3ddb49a5406447b3f52757b7b9c6be91baa2d5d3a221ec3ef27af697860f595",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

172.18.0.2 了解.

それでは slave (replica) 側の Redis server の Docker container も作ってしまいましょう.

FROM redis:4.0.14
COPY replica.redis.conf /usr/local/etc/redis/redis.conf
CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ]

この時の replica.redis.conf は以下の通り:

bind 0.0.0.0
slaveof 172.18.0.2 6379

slaveof に先ほど取得した master (primary) の Redis server の IP アドレスを入れておきます.

そしてこのコンテナも起動.

$ docker run --net=redis-test -p 16379:6379 replica-redis:latest
$ docker ps
CONTAINER ID        IMAGE                  COMMAND                  CREATED             STATUS              PORTS                     NAMES
f3efc8e79267        replica-redis:latest   "docker-entrypoint.s…"   3 seconds ago       Up 2 seconds        0.0.0.0:16379->6379/tcp   interesting_wright
52e66b665fe2        primary-redis:latest   "docker-entrypoint.s…"   4 seconds ago       Up 3 seconds        0.0.0.0:6379->6379/tcp    wonderful_tereshkova

上記のようにホストマシンのポート 6379 は master (primary) に,そして 16379 は slave (replica) の Redis server に繋がっていることとなります.


それでは Expire を利用するコードをホストマシンから流し込んで検証してみましょう.

$ redis-cli -p 6379 SET foo 1 EX 5 && \
  redis-cli -p 6379 GET foo && \
  redis-cli -p 16379 GET foo && \
  sleep 6 && \
  redis-cli -p 16379 GET foo && \
  redis-cli -p 6379 GET foo
  • master (primary) にセット
  • master (primary) と slave (replica) でゲット
  • expire duration が経過するまで sleep
  • slave (replica) 側から再度ゲット (master (primary) が先ではないのが重要)
  • master (primary) 側から再度ゲット

というシナリオです.

結果としては

OK
"1"
"1"
# 6 秒経過
(nil)
(nil)

というふうになり,slave (replica) 側から GET を試みたとしてもちゃんとデータが purge されています.したがって「Slave 側ではデータは Purge しない。Master で データの Purge の Replication をまつ」という挙動は解消していることがわかります.

よかったですね!


おまけ

さて,前述のブログで id:maedama さんが指摘している,replication 遅延が生じた時の挙動についても見てみましょう.

tc を使って擬似的なパケット遅延を表現するので,master (primary) 側の Docker file に手を加えて iproute2 をインストールしておきます.

FROM redis:4.0.14
COPY primary.redis.conf /usr/local/etc/redis/redis.conf
RUN apt-get -y update && apt-get install -y iproute2
CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ]

そしてコンテナを立ち上げてみます.tc で制御するためには --cap-add NET_ADMIN で権限を与える必要あり.

$ docker run --net=redis-test -p 6379:6379 --cap-add NET_ADMIN primary-redis:latest

次に tc で擬似的に master (primary) から slave (replica) 宛て (この場合 172.18.0.3/32) のパケットを 2000ms 遅延させる設定を加えてみます (678a54eb5d91 は primary-redis の container ID).

docker exec 678a54eb5d91 tc qdisc add dev eth0 root handle 1: prio
docker exec 678a54eb5d91 tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32 match ip dst 172.18.0.3/32 flowid 2:1
docker exec 678a54eb5d91 tc qdisc add dev eth0 parent 1:1 handle 2: netem delay 2000ms

ホストマシンから redis-cli で検証コマンドを流し込んでみましょう.

redis-cli -p 6379 SET foo 1 EX 3 && \
  sleep 2 && \
  redis-cli -p 6379 EXPIRE foo 3 && \
  sleep 2 && \
  redis-cli -p 6379 INCR foo && \
  redis-cli -p 6379 GET foo && \
  redis-cli -p 16379 GET foo

このときの出力は以下のようになります:

OK
# 2秒後
(integer) 1
# 2秒後
(integer) 2
"2"
"1"

slave (replica) 側では EXPIRE foo 3 までは適用されている一方,2000ms の遅延によって INCR foo が反映されていないことがわかります.

さらに2秒待ってみましょう

redis-cli -p 6379 SET foo 1 EX 3 && \
  sleep 2 && \
  redis-cli -p 6379 EXPIRE foo 3 && \
  sleep 2 && \
  redis-cli -p 6379 INCR foo && \
  redis-cli -p 6379 GET foo && \
  redis-cli -p 16379 GET foo && \
  sleep 2 && \
  redis-cli -p 6379 GET foo && \
  redis-cli -p 16379 GET foo

出力はこのような感じ

OK
# 2秒後
(integer) 1
# 2秒後
(integer) 2
"2"
"1"
# 2秒後
(nil)
"2"

INCR foo まで slave (replica) に反映されたことが見て取れます.slave (replica) 側は自前で TTL を持っているような感じに見えますね.この場合は素朴なレプリケーション遅延が起きるような感じがします.

Spotless を sbt から利用するための plugin: sbt-spotless をリリースしました

github.com

Spotlesssbt から利用するためのプラグインであるところの sbt-spotless をリリースしました.

Spotless は意欲的に開発されている Java (JDK 言語) のプロジェクト向けのコードフォーマッターで,実に様々なフォーマットに対応しています *1
Spotless 自体がフォーマッターそのものを実装しているわけではなく,対象が Java であれば Eclipse JDT あるいは google-java-formatter,Scala であれば scalafmt というふうに既存のフォーマッターを動的に取ってきて *2 いい感じに適用するという動きをするので,「フォーマッター」そのものというよりは「フォーマッターハブ」と表現したほうが適切かもしれません.

この Spotless は maven plugin と gradle plugin がそれぞれオフィシャルにサポートされているのですが,sbt から利用する plugin がどうにも見当たらなかったので今回はそれを書いたという次第です.気合でsbt 0.13 系と 1.3 系の両方をサポートしています *3


使い方は至って簡単.この plugin は Maven Central にすでにリリースされているので,お手元の plugins.sbt

addSbtPlugin("net.moznion.sbt" % "sbt-spotless" % "0.1.2")

と書いて,build.sbt に設定を書く (例えば以下のように) とすぐにご利用いただけます.

import net.moznion.sbt.spotless.config._

lazy val root = (project in file("."))
  .settings(
    name := "Example",
    spotlessJava := JavaConfig(
      googleJavaFormat = GoogleJavaFormatConfig("1.7")
    ),
  )

そして sbt spotlessCheck を実行すると設定に応じてコードフォーマットのチェックが走りますし,sbt spotlessApply を走らせるとコードフォーマッターが実際にファイルに対して適用されます.

なお,sbt 1.3 系限定ですが,

lazy val root = (project in file("."))
  .settings(
    name := "Example",
    spotless := SpotlessConfig(
      checkOnCompile = true,
      // applyOnCompile = true,
    ),
  )

というふうに spotless.checkOnCompile もしくは spotless.checkOnApplytrue として設定をすると,sbt プロジェクトのコンパイル時にチェックあるいはフォーマットの適用を自動的に走らせることも可能です.

その他の詳細な設定については以下のドキュメントをご参照ください.
Configurations · moznion/sbt-spotless Wiki · GitHub


現状,sbt-spotless は Java, Scala, Kotlin, Groovy, C++ そして SQL のフォーマットに対応しています *4.Spotless 本体はそれよりも多くのフォーマットの種類を対応しているので,もし sbt-spotless でも対応すべきフォーマットがあれば issue や pull-request などでお知らせいただけると助かります.


以上です.ぜひお試しください!

*1:とにかくあらゆるものをフォーマットしようとする気概が見える: > Spotless can format <java | kotlin | scala | sql | groovy | javascript | flow | typeScript | css | scss | less | jsx | vue | graphql | json | yaml | markdown | license headers | anything> using <gradle | maven | anything>

*2:この動的に取ってくる部分が鬼門でいくつかknown issueがある…… https://github.com/moznion/sbt-spotless#known-issues

*3:世の中 0.13 が案外現存している……

*4:https://github.com/moznion/sbt-spotless#supported-formatters

zabbix-internal-checks-exporterをリリースしました

github.com

表題の通りリリースいたしました.

これはなに

Zabbix internal checks と呼ばれる Zabbix プロセスの状態を表現するメトリクスを定期的に収集し,Prometheus のメトリクスフォーマット (i.e. OpenMetrics) で export する Prometheus exporter です.愉快ですね.皆さんも愉快に思っていただけると嬉しいです.

どんなときに使うの

生きていると色々ありますね.




以上です,よろしくお願いします.最近の prometheus/client_golang はめちゃめちゃ便利で良いですね.

jackson-databind で int の取りうる値を超えた場合の挙動

Javaの話題です.

jackson-databind を使って JSON のデシリアライズを行っていて,数値を int にマッピングしている場合,その値が int (32bit) の取りうる値を超えた時の挙動が「バージョンによって異なって」います.
以下に挙げる挙動は 2.9.3 から 2.9.4 へのアップグレードで変更されています.


jackson-databind-2.9.3 を利用している場合,値が int の範囲を超過するとその値を int にキャストしたもの *1 にデシリアライズされます.

コード:


一方,jackson-databind-2.9.4 で同様のシチュエーションになった時にどうなるかというと,このバージョンからは例外が上げられることになります.

コード:

新たに導入された _convertNumberToInt というメソッドの内部で値が int の取りうる範囲内かどうかを判断し,範囲内でない場合には reportOverflowInt で例外を送出するようになっています.


この変更がどこで入ったのかというと,どうやらこのコミットのようです: Fix #1729 · FasterXML/jackson-databind@6a1152c · GitHub

このコミットログに記されている #1729 がどのようなレポートかと言うと

github.com

「int が範囲外だった時,従来の実装では意図しない値になってしまうので明示的になるように例外を上げてほしい.jackson-core の ParserBase では int が範囲外の際に例外を上げている」という内容のようです.
Author の方も「挙動に一貫性を持たせるためにも,範囲外のときには例外を上げるようにしたほうがよい」と返答をしており,これについては賛成するところです (暗黙的な wraparound は脆弱性の元にもなり得るので).


さて一方で uint32 の範囲でおさまる値 (uint32 の範囲内だと値を負に rewind しても一意になる) を取り扱っているときに,従来の挙動を意識せず利用している場合,予期せぬクラッシュが起こります.
手っ取り早く直すためには int ではなく long として取り扱うというのが良いでしょうが,既存データとの兼ね合いなどの色々な事情によってそうもゆかないこともあるでしょう.そのような際にはカスタムシリアライザ・デシリアライザを書いて乗り切ることになると思います.

import com.fasterxml.jackson.databind.util.StdConverter;

public static class Uint32JacksonSerializer extends StdConverter<Integer, Long> {
    @Override
    public Long convert(Integer n) {
        if (n == null) {
            return 0L; // as you like
        }
        return n.longValue();
    }
}
import com.fasterxml.jackson.databind.util.StdConverter;

public static class Uint32JacksonDeserializer extends StdConverter<Long, Integer> {
    private static final long MAX_UINT32_VALUE = 4294967295L;

    @Override
    public Integer convert(Long n) {
        if (n == null) {
            return 0; // as you like
        }
        if (n > MAX_UINT32_VALUE) {
            throw new IllegalArgumentException("out of the boundary of uint32 value: " + n);
        }
        return n.intValue();
    }
}

このようにカスタム (デ) シリアライザを書いて,

public class JsonClass {
    @JsonSerialize(converter = Uint32JacksonSerializer.class)
    @JsonDeserialize(converter = Uint32JacksonDeserializer.class)
    private Integer foo;
}

などとしてあげると,入れる時・出す時によしなに値を変換して取り扱ってくれるようになります.



しかしこれがパッチバージョンとして入ってくるレベルの変更かというと……大変ですね.まあリリースノートにはしっかり書いてあるのですが……jackson-databind/VERSION-2.x at ae9c91d254954a963cc525e941564c5348181eac · FasterXML/jackson-databind · GitHub

教訓としては「bit長にゆとりを持ってデータ設計をしましょう」「リリースノートをちゃんと読もう」ということです,現場からは以上です.

*1:例えば `2147483648` が来た場合に rewind されて `-2147483648` になる

pcapng ファイルを pcap ファイルに変換する

基本的に Wireshark を使うことで得られるパケットキャプチャファイルは pcapng と呼ばれるフォーマットになっており,これは pcap フォーマットとは異なります.

例えば複数のパケットキャプチャファイルを時系列に従ってマージできるコマンドラインツールである mergecap コマンドは Wireshark が提供しているソフトウェアであるため,これを使って出力される merged なパケットキャプチャファイルはデフォルトでは pcapng フォーマットとなります (ちなみに,これについては mergecap -F pcap というふうに -F オプションでフォーマットを指定すると pcap フォーマットで出力できる).


pcap を前提としたプログラムでこのpcapngファイルを読み込もうとすると往々にしてエラーが起きる (例えば Unknown magic a0d0d0a のようなエラーが上がる) ので,pcapng なファイルを pcap なファイルに変換したくなることがあります.というわけでこうです

tcpdump -r file.pcapng -w file.pcap

こうすることで pcapng ファイルを読み込んで pcap ファイルに変換することができます.簡単ですね.


なおインターネット上を検索してみると tshark を使った方法 (tshark -F pcap -r file.pcapng -w file.pcap) がヒットするのですが,この方法は元のファイルサイズが大きいと Killed という無慈悲なエラーを吐いてコマンドが完遂しないので,特別な理由がない限りは tcpdump を使うのが良いと思います.

AWS CodeBuild で AWS CDK を実行する時に IAM Role に S3 の権限を与えないとハマる

AWS CodeBuild から AWS CDK を呼び出し,いい感じで継続的に構成を更新し続けるパイプラインのようなものを作っておくと何かと便利です.
さてこの時,AWS CodeBuild を実行する IAM Role の権限がそのまま AWS CDK の実行に影響するので,その IAM には

  • CloudFormation Stacks の操作に必要な権限
  • CloudFormation が実際に影響を与える対象の操作 (つまり本当にやりたいこと) に必要な権限

という権限を与えておく必要がありますが,これに加えて

  • CDK の中間生成物をアップロードする S3 Bucket: arn:aws:s3:::cdktoolkit-stagingbucket-* に対する権限
    • s3:*Object
    • s3:ListBucket

も併せて与えないと ❌ YourStack failed: Forbidden: <snip> などというエラーを吐いて死にます.
AWS CDK Forbidden, arn:aws:s3:::cdktoolkit-stagingbucket-* などで検索すると,公式ドキュメントを含めていろいろな情報がヒットするので既知の情報なのでしょう.しかしちょっとハマってしまったのでメモとして記すこととします.

なおタイトルにはこう書いてありますが,これは別に CodeBuild に限った話ではなく,任意の IAM Role で CDK を利用する時には共通して必要です.