株式会社クリアコード > ククログ

ククログ


RubyKaigi Takeout 2020 - Goodbye fat gem #rubykaigi

RubyKaigi Takeout 2020の1日目にGoodbye fat gemという話をする須藤です。今年もクリアコードはスポンサーとしてRubyKaigiを応援します。

関連リンク:

内容

RubyKaigiではRuby関連の自分の活動を自慢しているのですが、今年はfat gemまわりの活動を自慢します。

fat gemまわりの状況は2019年、fat gemをやめるとしてすでにまとめていて、今回の内容もこの記事の内容をベースにしています。ちなみに、私はRubyKaigiに応募する前に応募する内容を記事にして興味がある人はいそうかということを確認しています。fat gemの内容は少しは興味がある人はいそうで、去年のRubyKaigi 2019のcsvの話Ruby 2.6.0とより高速なcsvの反響をみる感じではそれなりにいそうで、RubyKaigi 2015のテスティングフレームワークの話Rubyのテスティングフレームワークの歴史(2014年版)の反響をみる感じではけっこういそうでした。

fat gemは私自身がかなり活用していたこともあり、rake-compilerなどfat gem関連のツールのメンテナンスをしています。しかし、RubyInstaller for Windowsの改良によりfat gemを使わなくてもよい状況になりつつあります。私が認識している最後の課題はBundlerにあったのですが、それも私が直しておきました。(どーん!けっこう難しかったんだよ!)Bundler 2.2.0がリリースされればfat gemを使わなくてもよい状況になるはずです。

ただ、この状況を知らずにfat gemを提供しつづけているgem開発者はまだそれなりにいるように私にはみえます。そのため、RubyKaigiの場でfat gem関連の最新情報を共有して、fat gemを提供しつづけるかどうか考える機会にして欲しいと思っています。fat gemをやめられればfat gemのメンテナンスコスト(それなりにあるはず)がなくなり、メンテナンスのためのリソースを別のことに使えます。これはgem開発者にとってよいことのはずです。

fat gemを提供しているgem開発者に届くといいなぁ。

動画作成方法

RubyKaigi Takeout 2020は事前録画した発表内容をストリーミングするスタイルです。私はDebian GNU/Linux上でOpen Broadcaster Softwareを使って録画しました。Shotcutを使って音量を調整したりとか動画編集もしてみたかったのですが、時間の関係でできませんでした。クリアコードのYouTubeチャンネルを作ってみたので、動画編集できたらアップロードしようと思っています。

2020年10月30日にはSciPy Japan 2020でApache Arrowの紹介をする予定で、それも事前録画なので、そのときは動画編集もしてみたいです。

まとめ

fat gemを提供しているgem開発者はぜひ聴いてみてね。

私がBundlerを直したように「手元で問題を回避するよりも問題が発生している原因を直すスタイル」で仕事をしたい人はクリアコードという会社がオススメなので会社説明会に応募してね。

タグ: Ruby
2020-09-03

fluent-plugin-mongoでTTLをサポートした話

はじめに

クリアコードはFluentdの開発に参加しています。

Fluentdにはプラグインというしくみがあり、たくさんのプラグインが開発されています。
今回は筆者畑ケがfluent-plugin-mongoというMongoDBにFluentdからログを流すプラグインでTTL(Time To Live)の機能をサポートした話を書きます。

MongoDBでのTTL

TTL(Time To Live)とは、あるデータが破棄されるまでの有効期限のことです。

MongoDBでは、このTTLはコレクションではなく、コレクションに対して貼るインデックスで設定します。

MongoDBのRuby Driverのマニュアルページを見ると、MongoDBのコレクションに対して貼るインデックスのオプションにexpire_afterパラメーターがいる事がわかります。

MongoDBのRuby DriverではMongoDBのコレクションに対して、以下のようにレコードのキーの指定とexpire_afterオプションの指定でTTLの指定ができる事がわかります。

client[:test_name].indexes.create_one(
  {"time": 1}, expire_after: 120
)
実際に組み込んでみる

fluent-plugin-mongoでは、時刻に関するキーは@inject_config.time_keyにて設定されます。
また、プラグインに組み込む時に設定ファイルでTTLの長さを設定できるとより便利です。

これらの要素を入れたパッチが以下のようになります。

diff --git a/lib/fluent/plugin/out_mongo.rb b/lib/fluent/plugin/out_mongo.rb
index fe9109f..4727762 100644
--- a/lib/fluent/plugin/out_mongo.rb
+++ b/lib/fluent/plugin/out_mongo.rb
@@ -49,6 +49,9 @@ module Fluent::Plugin
     desc "Remove tag prefix"
     config_param :remove_tag_prefix, :string, default: nil,
                  deprecated: "use @label instead for event routing."
+    # expire indexes
+    desc "Specify expire after seconds"
+    config_param :expire_after, :time, default: 0
 
     # SSL connection
     config_param :ssl, :bool, default: false
@@ -270,6 +273,13 @@ module Fluent::Plugin
       unless collection_exists?(name)
         log.trace "Create collection #{name} with options #{options}"
         @client[name, options].create
+        if @expire_after > 0 && @inject_config
+          log.trace "Create expiring index with key: \"#{@inject_config.time_key}\" and seconds: \"#{@expire_after}\""
+          @client[name].indexes.create_one(
+            {"#{@inject_config.time_key}": 1},
+            expire_after: @expire_after
+          )
+        end
       end
       @collections[name] = true
       @client[name]

このパッチを組み込み、設定ファイルを作成します。

<match **>
  @type mongo
  connection_string mongodb://localhost:27017/testDb
  collection test1
  expire_after 120
  # ...
</match>

とすると、testDbデータベースへtest1コレクションを作成し、このコレクションに入るレコードのTTLは120秒に設定されます。

まとめ

fluent-plugin-mongoを題材にして普段どのようにFluentdプラグインのメンテナンスをしているかを解説しました。

当社では、お客さまからの技術的なご質問・ご依頼に有償にて対応するFluentdサポートサービスを提供しています。Fluentd/Fluent Bitをエンタープライズ環境において導入/運用されるSIer様、サービス提供事業者様は、お問い合わせフォームよりお問い合わせください。

タグ: Fluentd
2020-09-04

リンカーフラグ -E の意味と使い方

プログラムをビルドする際には、色々なオプションをつけて実行することが多いですが、先日 -lluajit-5.1 -Wl,-E というオプションを見ました。
この、-Wl,-E というオプションがなんなのかわからなかったため、調査しました。この記事ではその調査結果を記載しています。

調査対象はコマンドのオプションなので、まずは、コマンドのマニュアルを見てみましょう。
このオプションを使ってビルドするときに使ったコマンドは gcc だったので、gcc のマニュアルを確認しました。
私の環境(Debian GNU/Linux 10.5)のマニュアルでは、 -Wl,-E というオプションは以下のように説明されていました。

       -Wl,option
              オプション option をリンカに渡します。option がコンマを含む場合は、それらのコンマで複数のオプションとして分割されます。

つまり、-Wl,-Eは、-Wl,optionというオプションでoptionの部分にリンカーへのオプションを指定します。
したがって、-Wl,-Eはリンカーに-Eというオプションを渡すという意味になります。

ということで、次は、リンカーのマニュアルを参照してみましょう。
このときのリンカーは、GNU ld を使っていたので、GNU ld のマニュアルを参照します。

私の環境(Debian GNU/Linux 10.5)のマニュアルでは、 -E オプションは以下のように説明されていました。
(英語のマニュアルのほうが情報が多かったので、英語のマニュアルを参照しました。)

       -E
       --export-dynamic
       --no-export-dynamic
           When creating a dynamically linked executable, using the -E option or the --export-dynamic option causes the linker to add all symbols to the
           dynamic symbol table.  The dynamic symbol table is the set of symbols which are visible from dynamic objects at run time.

           If you do not use either of these options (or use the --no-export-dynamic option to restore the default behavior), the dynamic symbol table will
           normally contain only those symbols which are referenced by some dynamic object mentioned in the link.

           If you use "dlopen" to load a dynamic object which needs to refer back to the symbols defined by the program, rather than some other dynamic
           object, then you will probably need to use this option when linking the program itself.

           You can also use the dynamic list to control what symbols should be added to the dynamic symbol table if the output format supports it.  See the
           description of --dynamic-list.

           Note that this option is specific to ELF targeted ports.  PE targets support a similar function to export all symbols from a DLL or EXE; see the
           description of --export-all-symbols below.

注目するのは以下の説明です。

When creating a dynamically linked executable, using the -E option or the --export-dynamic option causes the linker to add all symbols to the dynamic symbol table.
The dynamic symbol table is the set of symbols which are visible from dynamic objects at run time.

(動的にリンクされた実行ファイルを作成する時に、すべてのシンボルを動的シンボルテーブルに加えます。
動的シンボルテーブルは、実行時に動的オブジェクトから見えるシンボルのテーブルです。)


If you use "dlopen" to load a dynamic object which needs to refer back to the symbols defined by the program, rather than some other dynamic object,
then you will probably need to use this option when linking the program itself.

(他の動的オブジェクトではなく、そのプログラムで定義されているシンボルを参照し返す必要のある動的オブジェクトを
dlopen で ロードする場合は、おそらくプログラム自身をリンクする時にこのオプションを 使う必要があるでしょう。)

あまり、ピンときませんね!
なので、実際に小さなプログラムを作って実験してみましょう。


では、「他の動的オブジェクトではなく、そのプログラムで定義されているシンボルを参照し返す必要のある動的オブジェクト」を考えてみましょう。
これは、例えば以下のようなケースです。

まず、extendable-programというプログラムがあるとします。
このプログラム用のプラグインとしてextendable-program-plugin.soという動的ライブラリーがあるとします。

以下のように、extendable-program-plugin.soextendable-programに動的にロードして使われることを前提としています。

画像の説明

そして、extendable-program-plugin.soは、extendable-programのバージョンを取得する必要があるとします。
(例えば、extendable-programのバージョンによって提供する機能を切り替えたりするのに使います。)

バージョンの取得は、extendable-programに自身のバージョンを返す関数があり、extendable-program-plugin.soからそれを呼び出すことで実現します。

このときに、-Eオプションをつけてextendable-programがビルドされていると、以下のように、extendable-programのシンボルが動的シンボルテーブルに登録されるので、extendable-program-plugin.soからextendable-programのシンボルが参照できるようになります。

画像の説明

では、実際にプログラムを作って上記の例を実験してみましょう。
実験用のプログラムは以下の通りです。

extendable-program
  • extendable-program.h

    #pragma once
    
    typedef void (*ep_plugin_init_func)(void);
    
    extern int ep_get_major_version(void);
    extern int ep_get_minor_version(void);
    extern int ep_get_micro_version(void);
    
  • extendable-program.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <dlfcn.h>
    
    #include "extendable-program.h"
    
    enum Verion {
      MAJOR = 1,
      MINOR = 0,
      MICRO = 2
    };
    
    int
    ep_get_major_version(void)
    {
      return MAJOR;
    }
    
    int
    ep_get_minor_version(void)
    {
      return MINOR;
    }
    
    int
    ep_get_micro_version(void)
    {
      return MICRO;
    }
    
    int
    main(int argc, char **argv)
    {
      void *plugin = dlopen("./extendable-program-plugin.so", RTLD_NOW | RTLD_LOCAL);
      printf("plugin: %p\n", plugin);
      if (!plugin) {
        return EXIT_FAILURE;
      }
    
      ep_plugin_init_func ep_plugin_init = dlsym(plugin, "ep_plugin_init");
      printf("ep_plugin_init: %p\n", ep_plugin_init);
      if (!ep_plugin_init) {
        dlclose(plugin);
        return EXIT_FAILURE;
      }
    
      ep_plugin_init();
      dlclose(plugin);
    
      return EXIT_SUCCESS;
    }
    
extendable-program-plugin
  • extendable-program-plugin.c

    #include <stdio.h>
    
    #include "extendable-program.h"
    
    extern void ep_plugin_init(void);
    
    void
    ep_plugin_init(void)
    {
      printf("ep-plugin-init: %d.%d.%d\n",
             ep_get_major_version(),
             ep_get_minor_version(),
             ep_get_micro_version());
    }
    
Makefile
all: extendable-program-with-E
all: extendable-program-without-E
all: extendable-program-plugin.so

clean:
	rm -f extendable-program-with-E
	rm -f extendable-program-without-E
	rm -f extendable-program-plugin.so

extendable-program-with-E: extendable-program.c extendable-program.h
	$(CC) -o $@ -Wl,-E extendable-program.c -ldl

extendable-program-without-E: extendable-program.c extendable-program.h
	$(CC) -o $@ extendable-program.c -ldl

extendable-program-plugin.so: extendable-program-plugin.c extendable-program.h
	$(CC) -shared -o $@ extendable-program-plugin.c

以上が実験用のプログラムです。
さっそく実行してみましょう。

$ make
cc -o extendable-program-with-E -Wl,-E extendable-program.c -ldl
cc -o extendable-program-without-E extendable-program.c -ldl
cc -shared -o extendable-program-plugin.so extendable-program-plugin.c

$ ./extendable-program-without-E
plugin: (nil)

$ ./extendable-program-with-E
plugin: 0x55eead4df290
ep_plugin_init: 0x7f2d5f4a4135
ep-plugin-init: 1.0.2

-Eをつけずにビルドしたプログラムextendable-program-without-Eでは、plugin: (nil)となっていて、extendable-program-plugin.soのロードに失敗しています。
(dlopenのオプションにRTLD_NOWが指定されているため、extendable-program-plugin.so内で参照しているシンボルが解決できない場合はdlopenが失敗します。したがって、これはextendable-program-plugin.soで使用しているシンボルが解決できなかったことになります。)

つまり、-Eオプションをつけないと以下のような状態になります。

画像の説明

-Eオプションの説明には、「全てのシンボルを動的シンボルテーブルに追加する」と書かれていました。
つまり、-Eオプションをつけることで、extendable-programのシンボルが全て動的シンボルテーブルに追加され、extendable-program-pluginから参照できるようになります。

実際に、-Eオプションをつけてビルドしたextendable-program-with-Eでは、extendable-program-plugin.soがロードされ正常にextendable-programのバージョンが取得できていることが確認できます。

このように、実行プログラム側で提供しているシンボルを動的ライブラリー側から参照する必要がある場合に-Eオプションが必要だということがわかりました。

-Eオプションの応用

-Eオプションは「全てのシンボルを動的シンボルテーブルに追加する」とマニュアルに記載されていました。これは、静的ライブラリーをリンクして使われることを想定しているLuaJITのようなプログラムにも応用できます。

どう応用できるのかを、先程の実験に使用したプログラムを改造して説明します。
まず、新しく静的ライブラリーlibadd.aを追加します。これは、2値の整数を可算する関数(add_int())と2値の実数を加算する関数(add_double())を提供しています。

今回の例ではextendable-programはこのlibadd.aを静的にリンクしていることを前提とし、libadd.aで提供する関数をextendable-program-plugin.soからも使うこととします。

画像の説明

つまり、以下のような実装になります。

libadd.a
  • add.c

    int add_int(int a, int b);
    int add_int(int a, int b) {
      return a + b;
    }
    
    double add_double(double a, double b);
    double add_double(double a, double b) {
      return a + b;
    }
    
extendable-program
  • extendable-program.h

    #pragma once
    
    typedef void (*ep_plugin_init_func)(void);
    typedef void (*ep_plugin_aggregate_func)(void);
    
    extern int ep_get_major_version(void);
    extern int ep_get_minor_version(void);
    extern int ep_get_micro_version(void);
    
    extern int add_int(int a, int b);
    
extendable-program
  • extendable-program.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <dlfcn.h>
    
    #include "extendable-program.h"
    
    enum Verion {
      MAJOR = 1,
      MINOR = 0,
      MICRO = 2
    };
    
    int
    ep_get_major_version(void)
    {
      return MAJOR;
    }
    
    int
    ep_get_minor_version(void)
    {
      return MINOR;
    }
    
    int
    ep_get_micro_version(void)
    {
      return MICRO;
    }
    
    int
    main(int argc, char **argv)
    {
      int sum = add_int(3, 4);
      printf("sum=%d\n", sum);
    
      void *plugin = dlopen("./extendable-program-plugin.so", RTLD_NOW | RTLD_LOCAL);
      printf("plugin: %p\n", plugin);
      if (!plugin) {
        return EXIT_FAILURE;
      }
    
      ep_plugin_init_func ep_plugin_init = dlsym(plugin, "ep_plugin_init");
      printf("ep_plugin_init: %p\n", ep_plugin_init);
      if (!ep_plugin_init) {
        dlclose(plugin);
        return EXIT_FAILURE;
      }
    
      ep_plugin_aggregate_func ep_plugin_aggregate = dlsym(plugin, "ep_plugin_aggregate");
      printf("ep_plugin_aggregate: %p\n", ep_plugin_aggregate);
      if (!ep_plugin_aggregate) {
        dlclose(plugin);
        return EXIT_FAILURE;
      }
    
      ep_plugin_init();
      ep_plugin_aggregate();
      dlclose(plugin);
    
      return EXIT_SUCCESS;
    }
    
extendable-program-plugin
  • extendable-program-plugin.c

    #include <stdio.h>
    
    #include "extendable-program.h"
    
    extern void ep_plugin_init(void);
    extern double add_double(double a, double b);
    extern void ep_plugin_aggregate(void);
    
    void
    ep_plugin_init(void)
    {
      printf("ep-plugin-init: %d.%d.%d\n",
             ep_get_major_version(),
             ep_get_minor_version(),
             ep_get_micro_version());
    }
    
    void ep_plugin_aggregate(void) {
      printf("ep-plugin-aggregate: %g\n",
             add_double(1.5, 2.2));
    }
    
Makefile
all: libadd.a
all: extendable-program-plugin.so
all: extendable-program-with-E
all: extendable-program-without-E

clean:
	rm -f extendable-program-with-E
	rm -f extendable-program-without-E
	rm -f extendable-program-plugin.so
	rm -f libadd.a

libadd.a: add.c
	$(CC) -c add.c && ar rcs $@ add.o

extendable-program-with-E: extendable-program.c extendable-program.h
	$(CC) -o $@ -Wl,-E extendable-program.c libadd.a -ldl

extendable-program-without-E: extendable-program.c extendable-program.h
	$(CC) -o $@ extendable-program.c libadd.a -ldl

extendable-program-plugin.so: extendable-program-plugin.c extendable-program.h
	$(CC) -shared -o $@ extendable-program-plugin.c

では、さっそく実行してみましょう。

$ make
cc -c add.c && ar rcs libadd.a add.o
cc -shared -o extendable-program-plugin.so extendable-program-plugin.c
cc -o extendable-program-with-E -Wl,-E extendable-program.c ./libadd.a -ldl
cc -o extendable-program-without-E extendable-program.c ./libadd.a -ldl

$ ./extendable-program-without-E
sum=7
plugin: (nil)

$ ./extendable-program-with-E
sum=7
plugin: 0x55c12f1636a0
ep_plugin_init: 0x7f800c18c145
ep_plugin_aggregate: 0x7f800c18c17e
ep-plugin-init: 1.0.2
ep-plugin-aggregate: 3.7

-Eをつけずにビルドしたプログラムでは、sum=7と出力されているので、add_int関数は正常に呼び出すことができています。
静的にリンクしたライブラリーの関数は正常に使えました。
extendable-program-plugin.soはシンボルの解決ができないのでロードに失敗します。

つまり、以下のような状態になります。

画像の説明

-Eオプションをつけてビルドしたプログラムでは、ep-plugin-aggregateが実行できています。
-Eは「全てのシンボルを動的シンボルテーブルに追加する」ので、静的にリンクされたライブラリーが持っているシンボルも動的シンボルテーブルに追加されるため、libadd.aのシンボルをextendable-program-plugin.soから参照できるのです。

つまり、以下のような状態になります。

画像の説明

このように、-Eをつけてビルドすることで、静的なライブラリーで実装されている関数を動的にロードされたライブラリーから参照できるということもわかりました。
特定の静的ライブラリーがリンクされることが前提のプログラムなら、-Eを用いて静的ライブラリーが持つ機能を動的ライブラリーで使うことができます。

2020-09-07

macOSのインストーラーを作成するには

はじめに

クリアコードはFluentdの開発に参加しています。

Fluentdプロジェクトではtd-agentという名前でパッケージやインストーラーを提供しています。
td-agentはtd-agent 3からmacOS向けのインストーラーが試験的に提供されています。
macOS用のtd-agentのインストーラーについてはmacOSでのtd-agentのユーザーが少ないということもあり提供が滞っていました。

この記事では筆者畑ケがmacOSのインストーラー作成に関する事項をtd-agent4の事例を元に解説します。

macOSのインストーラー

macOSではインストーラーと呼ばれるものには形式がいくつかあります。

  1. distribution形式の.pkg単体での配布物
  2. distribution形式の.pkgをさらに同梱したディスクイメージ
  3. macOSのアプリケーション形式(.app)のアプリケーションを同梱したディスクイメージ

これらにはアンインストール機能はありません。 *1
この記事ではアンインストールの機能に関しては詳述しないことにします。

この記事では2.のdistribution形式のpkgを同梱したディスクイメージを作成する方法を解説します。

macOS向けのインストーラーを作成する

以下の手順はmacOS環境を前提とします。

2.の形式のインストーラーを選ぶ理由

この記事では2.の形式のインストーラーの作り方を解説することにします。
td-agent4では以下の理由があったためです。

  • launchctlでサービス化を行いたい

    • 3.の形式では要件を満たせない。launchctlで探索するパスにサービス定義を含めることが出来ない。 *2
  • インストーラーの設定を柔軟に行いたい

    • 1.のdistribution形式のpkgでは背景画像を設定したり、見栄えをよくしたり、最初にpkgの他にREADMEを提示したりするカスタマイズ性が弱い
    • macOSの暗黙的な慣習ではdistribution形式のpkg単体で配布するよりも、distribution形式のpkgをディスクイメージにさらに同梱して配布することが好まれる

同様のニーズを持つプロジェクトでは2.のdistribution形式のpkgを同梱したディスクイメージをインストーラー形式として採用することをお勧めします。

インストーラー作成の手順

ここからは手順の説明をします。
手順は以下の三段階です。

  1. flat pkgの定義を作成
  2. flat pkgを作成
  3. distribution pkgの定義を作成
  4. distribution pkgを作成
  5. ディスクイメージ(.dmg)を作成する
1. flat pkgの定義を作成

インストーラーの機能のみを持つflat pkgの定義ファイル(.plist)を作成します。
一般的に定義を作成するpkgbuildコマンドは以下のように使用します。

$ pkgbuild --analyze --root /path/to/staging-path sample.plist

これを実行するとsample.plistという定義ファイルが作成されます。
sample.plistはflat pkgの動作を変える場合、編集する必要があります。この記事では編集の手順は省略します。

td-agent4のインストーラー作成では以下の通りにしています。

$ pkgbuild --analyze --root td-agent/staging td-agent.plist
pkgbuild: Inferring bundle components from contents of td-agent/staging
pkgbuild: Writing new component property list to td-agent.plist

このケースではtd-agent/stagingというディレクトリに必要なファイルが入っており、
plistの編集が必要がないためtd-agent4ではこの定義ファイルはそのまま使用しています。

2. flat pkgを作成

pkgbuildコマンドに与えるコマンドライン引数を変えてもう一度実行し、
構成ファイルをインストールする機能のみを持つインストーラーを作成します。
このコマンドは一般的には以下のように使用します。

$ pkgbuild --root /path/to/rootDir --component-plist /path/to/sample.plist --scripts /path/to/scriptDir --identifier com.distributor.app.identifier --version version --install-location location PackageName.pkg

これで、構成ファイル群をflat pkg形式のPackageName.pkgに固めることができます。

td-agent4のインストーラー作成では以下の通りにしています。

$ pkgbuild --root td-agent/staging --component-plist td-agent.plist --identifier com.treasuredata.tdagent --version 4.0.1 --install-location / td-agent.pkg [--scripts /path/to/installation-scripts-dir]
pkgbuild: Reading components from td-agent.plist
pkgbuild: Wrote package to td-agent.pkg

インストール時に作成することが必要なディレクトリがあるため、実際のtd-agent4のflat pkgには--scriptsにはpostinstallが入ったディレクトリを指定しています。
--scripts引数の指定がない場合にはインストーラーを実行した際にインストールスクリプトの実行する候補がありません。この時にはインストールスクリプトは実行されません。

3. distribution pkgの定義を作成

前段で、flat pkgを作成しました。このインストーラーは、背景画像や、welcomeテキストや、使用許諾表示をユーザー指定のものに任意に差し替える機能がありません。
distribution pkgはこれらの機能を備えている形式のインストーラーです。また、この形式のインストーラーは複数のflat pkgを内包できます。

前段で作成したflat pkgを用いてdistribution pkgの定義の雛形を作成します。
一般的に定義を作成するproductbuildコマンドは以下のように使用します。

$ productbuild --synthesize --package /path/to/PackageName.pkg Distribution.xml

作成されたDistribution.xmlは以下のような内容になっています。

<?xml version="1.0" encoding="utf-8"?>
<installer-gui-script minSpecVersion="1">
    <!-- 中略 -->
</installer-gui-script>

Distribution.xmlを必要に応じて編集します。
例えば以下の要領です。

<?xml version="1.0" encoding="utf-8"?>
<installer-gui-script minSpecVersion="1">
    <!-- 中略 -->
    <title>Distribution pkgを実行した時のタイトル</title>
    <background file="背景画像.png" alignment="bottomleft" mime-type="image/png"/>
    <license file="使用許諾.rtf" mime-type="text/html"/>
    <welcome file="welcomeテキスト.html" mime-type="text/html"/>
</installer-gui-script>

この記事ではDistribution.xmlの編集手順は詳述しません。

td-agent4のインストーラー作成では以下の通りにしています。

$ productbuild --synthesize --package td-agent.pkg Distribution.xml 
productbuild: Wrote synthesized distribution to Distribution.xml

これにてproductbuildコマンドで使用する定義ファイルのDistribution.xmlが作成できました。
td-agent4の場合、作成されたファイルの内容は以下の通りです。

<?xml version="1.0" encoding="utf-8"?>
<installer-gui-script minSpecVersion="1">
    <pkg-ref id="com.treasuredata.tdagent"/>
    <options customize="never" require-scripts="false"/>
    <choices-outline>
        <line choice="default">
            <line choice="com.treasuredata.tdagent"/>
        </line>
    </choices-outline>
    <choice id="default"/>
    <choice id="com.treasuredata.tdagent" visible="false">
        <pkg-ref id="com.treasuredata.tdagent"/>
    </choice>
    <pkg-ref id="com.treasuredata.tdagent" version="4.0.1" onConclusion="none">td-agent.pkg</pkg-ref>
</installer-gui-script>
4. distribution pkgを作成

続いて、productbuildコマンドを実行します。
このコマンドは一般的には以下のように実行します。

$ productbuild  --distribution "Distribution.xml" --package-path /path/to/PackageName-0.0.0.pkg --resources "/path/to/resourceDir" PackageName.pkg

td-agent4のインストーラー作成では以下の通りにしています。

$ productbuild --distribution Distribution.xml --package-path td-agent.pkg [--resources /path/to/resources such as welcome.html and license.html etc.] td-agent-4.0.1.pkg
productbuild: Wrote product to td-agent-4.0.1.pkg

これで、distribution pkgが作成できました。

5. ディスクイメージ(.dmg)を作成する

macOSでは、distribution pkgを更にディスクイメージ(.dmg)に包んで配布することが多く見かけられます。

ディスクイメージを作成するには、ディスクイメージへ格納したいファイルを配置するディレクトリを作成し、そこにファイルをコピーします。

$ mkdir dmg
$ cp /path/to/PackageName-0.0.0.pkg dmg
$ hdiutil create -srcfolder dmg -fs HFS+ -format UDZO -volname SamplePackage PackageName-0.0.0.dmg
.......................................................................................................................................................
created: /path/to/PackageName-0.0.0.dmg

td-agent4のインストーラー作成に当てはめると以下のコマンドになります。
(ただし、実際にはディスクイメージのカスタマイズを行うため追加の手順があります。)

$ mkdir dmg
$ cp td-agent-4.0.1.pkg dmg
$ hdiutil create -srcfolder dmg -fs HFS+ -format UDZO -volname Td-Agent td-agent-4.0.1.dmg
.......................................................................................................................................................
created: ~/GitHub/td-agent-builder/td-agent-4.0.1.dmg

これにて2.の形式のdistribution形式のpkgを同梱したディスクイメージが作成できました。

ディスクイメージを更にカスタマイズしたい場合

ここまでの手順ではディスクイメージをhdiutil create -fs HFS+ -format UDZO ...で作成していますが、これで作成されるディスクイメージは読み込みのみ可能なディスクイメージです。

td-agent4では書き込みと読み込み可能な一時ディスクイメージを作成してディスクイメージの見た目とFinderで開いた時のサイズ調整を行っています。
td-agent4におけるmacOS向けのインストーラーを作成する作業は fluent-plugins-nursery/td-agent-builder#192 にて実施しました。この記事では解説されなかった細かな箇所に関してはリンク先のpull requestをご覧ください。

まとめ

以上、macOS向けのインストーラーの作成のやりかたを解説しました。

筆者は別の案件でdistribution形式のmacOSのインストーラーの作成を実施したことがありました。
td-agent4の前のバージョンのtd-agent3ではインストーラーを更にディスクイメージに格納しているやりかたでインストーラーを作成していました。td-agent3でのmacOSのインストーラーと形式が大きく異ならないように調整や調査をしながらtd-agent4でのmacOSインストーラーを作成する作業を実施しました。

読み込みと書き込みが可能なディスクイメージの作成が必要ということに中々気付けず、ディスクイメージのカスタマイズに難航しました。また、ディスクイメージのカスタマイズは基本的にGUI経由で行われる操作です。macOSはGUI Scripting環境(AppleScript)も提供しており、CI環境でもディスクイメージのカスタマイズを実施することができました。

当社では、お客さまからの技術的なご質問・ご依頼に有償にて対応するFluentdサポートサービスを提供しています。Fluentd/Fluent Bitをエンタープライズ環境において導入/運用されるSIer様、サービス提供事業者様は、お問い合わせフォームよりお問い合わせください。

*1 ユーザー自身がこのコマンドを使用してmacOSのインストーラー形式である.pkgがインストールしたファイルをシェルスクリプトなどの手段で削除できます。インストールされたファイルは`pkgutil --files `の形式でインストーラーが登録している識別子に紐づいているファイルを取得できます。

*2 macOSではlaunchctl/launchdを用いてサービスの起動・終了を行うことが推奨されます。macOSではlaunchctlでサービスを扱いますが`/Application`配下の.app内のlaunchctl用のサービス定義は読み込んでくれないからです。launchctl向けのサービス定義は.plistと言う拡張子で、`/Library/LaunchDaemons`配下に置くことが推奨されます。

タグ: Fluentd
2020-09-08

Apache Arrowコンサルティングサポートを開始

Apache Arrowの開発に参加している須藤です。

2020年7月24日にApache Arrowの最初のメジャーバージョン1.0.0がリリースされたので、クリアコードとして正式にApache Arrowのコンサルティングサポートを開始することにしました!詳細はプレスリリースApache Arrowコンサルティングサポートを開始を参照してください!

前から個別に声をかけてくれた方々には提供していたのですがコンサルティングサポートを提供していることを表明しました。

2年前からデータ処理ツールの開発事業を立ち上げようと取り組んでいましたが、それの1つの形になります。感慨深いです。

コンサルティングサポートを利用したい方はもちろん、ビジネスパートナーとして一緒にApache Arrowを活用したい方からのお問い合わせをお待ちしています!また、クリアコードに入社して一緒にコンサルティングサポートに取り組みたい人も募集しています!

2020-09-09

Serverspec(Specinfra)でWindows向けに不足している機能を追加するには

はじめに

クリアコードでは、Fluentdの開発に参加しています。Fluentdにはtd-agentと呼ばれるディストリビューションがあり、各種プラグインをまとめてパッケージやインストーラーが提供されています。

今回は、td-agentのテストに使っているServerspec(Specinfra)で、Windows向けのテストに不足している機能を一部追加する機会があったのでその紹介をします。

機能不足に気づいたきっかけ

td-agentでは、ビルドしたパッケージの動作を確認するために、Serverspec(Specinfra)によるテストを実行するようにしています。
Windowsのインストーラーにバンドルしているgemが期待通りインストールされているかをテストしようとしたところ、以下のエラーが発生することに気づきました。*1

check_is_installed_by_gem is not implemented in Specinfra::Command::Windows::Base::Package

これは、以下のようなテストコードで発生していました。特定のgemの指定したバージョンのものがインストールされていることを確認するテストです。

describe package("#{spec.name}") do
  it { should be_installed.by("gem").with_version(spec.version) }
end

エラーメッセージから明らかなように、特定のgemがインストールされているかどうかをチェックする機能がまだWindowsではサポートされていないことがわかりました。

Specinfraにcheck_is_installed_by_gemを追加する

Windowsでも同じコードでバンドルしているgemのチェックができると嬉しいので、不足している機能を追加することにしました。

必要なのは、前述のエラーメッセージが示すように、Specinfra::Command::Windows::Base::Packagecheck_is_installed_by_gem を実装してあげることです。

def check_is_installed_by_gem(name, version=nil, gem_binary="gem")
  version_selection = version.nil? ? "" : "-gemVersion '#{version}'"
  Backend::PowerShell::Command.new do
    using 'find_installed_gem.ps1'
    exec "(FindInstalledGem -gemName '#{name}' #{version_selection}) -eq $true"
  end
end

gemの名前とバージョンを受け取り、比較して一致するか否かを返す FindInstalledGemfind_installed_gem.ps1に実装し、lib/specinfra/backend/powershell/support/find_installed_gem.ps1 として配置することで実現しました。

実際のPull RequestはWindows: support check_is_installed_by_gemです。

まとめ

今回は、Serverspec(Specinfra)の未実装の機能を追加して、Windowsでも他の環境同様に it { should be_installed.by("gem") } という記述で指定したgemがインストールされていることを確認できるようにフィードバックした話を紹介しました。

当社では、お客さまからの技術的なご質問・ご依頼に有償にて対応するFluentdサポートサービスを提供しています。Fluentd/Fluent Bitをエンタープライズ環境において導入/運用されるSIer様、サービス提供事業者様は、お問い合わせフォームよりお問い合わせください。

*1 td-agentはWindowsのインストーラに環境に合わせてWindows向けのgemをバンドルするようにしています。

2020-09-11

SciPy Japan 2020 - Apache Arrow 1.0 - A cross-language development platform for in-memory data #SciPyJapan #ApacheArrow

SciPy Japan 2020の1日目に「Apache Arrow 1.0 - A cross-language development platform for in-memory data」という話をする須藤です。

SciPy Japan 2020は2020年10月30日で約1ヶ月後なのですが、事前録画した動画を使ったオンラインイベントで、私の話は録画済みなのでここで先に公開します。SciPy Japan 2020に参加する人とこのブログを読む人は違う層だろうからです。Apache Arrowをよく知らない人がどうやってApache Arrowを使えばよいかがわかることを狙った内容にしました。Apache Arrowをよく知らない人に観てもらって(動画ではなくてスライドでもよいです)感想を https://twitter.com/ktou に教えて欲しいです!

関連リンク:

内容

Apache Arrowに関する情報は毎年まとめている次の情報で網羅的にカバーできているはずですが、多岐にわたっていてApache Arrowを調べ始めた人には情報量が多すぎるんじゃないかと感じていました。

これまでいろいろな機会でApache Arrowを紹介してきましたが「すごいよさそうですね!で、どう使い始めればよいですか!?」という反応をもらうことが多かったです。

そこで、今回はApache Arrowをよく知らない人が知りたそうだろうことにフォーカスして情報を削ってまとめてみました。ただ、私はApache Arrowをよく知らない人ではないのでうまくいっているかどうかよくわかりません。ということで、Apache Arrowをよく知らない人の感想を聞きたいのです!ぜひ https://twitter.com/ktou に感想を教えてください!

2020-09-24

ノータブルコード12 データ構造を分かりやすくドキュメントする技

第12回目のノータブルコードでは、データ構造を分かりやすくするドキュメントのテクニックを紹介します。

C言語とシリアライズ処理

C言語でよくあるパターンとして、データをバイト列にシリアライズするという作業があります。多くの場合、この作業はデータフォーマットの仕様と密接に絡み合っています。フォーマット仕様書にある「最初の1バイトは8ビット識別用の固定値。次はフォーマットを表す3バイトの符号で埋める。その次が...」という指示に基づいて配列を埋めていくのが典型です。

シリアライズの例

この処理は、もちろん正確に実装する必要があるのですが、一方でヒューマンエラーの入り込みやすい部分でもあります。例えば、二つの隣り合うフィールドを入れ違いで埋めてしまった、データが実は1バイトずれていたというのは、実にありがちなミスです。最初は慎重に実装しても、フォーマットの拡張に対応するために、何度か手を加えるうちにいつのまにか壊れていた、というケースもままあります。できれば、こういったミスは未然に防げるようにしたいものです。

データ構造を明示するテクニック

私の好きなRFCに、GZIPのRFCがあります。その説明から少し引用すると、例えば以下のようなものです。

2.3. Member format

      Each member has the following structure:

         +---+---+---+---+---+---+---+---+---+---+
         |ID1|ID2|CM |FLG|     MTIME     |XFL|OS | (more-->)
         +---+---+---+---+---+---+---+---+---+---+
      ...

      2.3.1. Member header and trailer

         ID1 (IDentification 1)
         ID2 (IDentification 2)
            These have the fixed values ID1 = 31 (0x1f, \037), ID2 = 139
            (0x8b, \213), to identify the file as being in gzip format.

         CM (Compression Method)
            This identifies the compression method used in the file.  CM
            = 0-7 are reserved.  CM = 8 denotes the "deflate"
            compression method, which is the one customarily used by
            gzip and which is documented elsewhere.

私は、データ構造をドキュメントする方法としては、このような図*1による視覚的な説明が最も優れていると思います。形式言語や自然言語の文章による説明よりもずっと分かりやすいと感じます。この主たる理由は、おそらくプログラミングで言うところのデータフォーマットが、私たちの住む物理的な世界とは根本的に異質な概念だからだと思います。

実際、このRFCの作者であるL. ピーター・ドイチュは、プログラミングの本質的な難しさについて「(現実世界では)見回したところでアドレスやポインタみたいなものを目にすることはない」*2と説明しています。メモリアドレスやバイト列のような概念は、私たちの住む日常的な世界には存在しないので、これを視覚的なイメージで置き換えることが有効なのです。

実際のコード例

このような理由で、私はバイナリのデータフォーマットを扱う時は図でドキュメントするようにしています。例えば、Fluent BitのGELFフォーマットを扱うプラグインについては、次のようにドキュメントを付しました。

/*
 * A GELF header is 12 bytes in size. It has the following
 * structure:
 *
 * +---+---+---+---+---+---+---+---+---+---+---+---+
 * | MAGIC |           MESSAGE ID          |SEQ|NUM|
 * +---+---+---+---+---+---+---+---+---+---+---+---+
 *
 * NUM is the total number of packets to send. SEQ is the
 * unique sequence number for each packet (zero-indexed).
 */
#define GELF_MAGIC "\x1e\x0f"
#define GELF_HEADER_SIZE 12

static void init_chunk_header(uint8_t *buf, uint8_t count)
{
    uint64_t msgid = message_id();

    memcpy(buf, GELF_MAGIC, 2);
    memcpy(buf + 2, &msgid, 8);
    buf[10] = 0;
    buf[11] = count;
}

コミットメッセージにも書いていますが、実は、この改修前のバージョンのフォーマット実装には、いくつかの不具合がありました(コードレビューをすり抜けて、2年以上バグが残っていたという部分にこの種の実装の難しさを感じます)。このような誰にでも起こることが分かっているミスは、様々な工夫をこらしてぜひとも防ぎたいものです。

これと似たテクニックとして、Brubeckのコメントの付け方の記事があります。興味のある方はこちらも参照ください。

*1 この図の詳しい読み方はRFCの「2.1. Overall Convention」に説明があります。この記事の範囲では、一マスが一バイトを表していると理解すればOKです。

*2 ピーター・サイベル著、青木靖訳「Coders at work - プログラミングの技をめぐる探求」(2011年、オーム社) p415

2020-09-25

Debian Developerになるには

はじめに

最近Debian Developerになった林です。
今回は、どうするとDebian Developerになれるのかについて紹介します。

Debian Maintainerになろう

まずはDebian Maintainerである必要があります。いきなりDebian Developerになることはできません。*1
Debian Developerには任意のパッケージをアップロードできる権限が与えられているからです。

Debian Maintainerとは、特定のパッケージについて、アップロードすることを許可されている人のことです。
もしまだDebian Maintainerでないなら段階を踏んでDebian Maintainerになりましょう。

以前 Debian Maintainerになるには という記事を書いたのでそれが参考になるかもしれません。

Debian Developerになるための申請をする

Debian Maintainerになったら次はDebian Developerになるための申請をしましょう。
申請は https://nm.debian.org/ から行ないます。

その際には、申請フォームから(gpgでクリアテキスト署名されている)次のような文面を送ります。

For nm.debian.org, at 2020-05-17:
I agree to uphold the Social Contract, the Debian Free Software Guidelines,
and the Debian Code of Conduct, in my Debian work.
I have read the Debian Machine Usage Policy and I accept them.

これ以降は、次のステップを順に踏んでいく必要があります。

  • Declaration of Intent
  • SC/DFSG/DMUP agreement
  • Advocate
  • Key consistency checks
  • Application Manager report
  • Front Desk or DAM approval

Declaration of Intent

なぜDebian Developerになりたいのかを表明するのがDeclaration of Intentというプロセスです。
こんなことをやりたいんだという熱い思いを記述します。

投稿した内容は、debian-newmaint@lists.debian.orgというメーリングリストへ送られます。

SC/DFSG/DMUP agreement

各種規定に同意することを表明するプロセスです。各種規定とは以下を指します。

  • Debian Social Contract (SC)
  • Debian Free Software Guidelines (DFSG)
  • Debian Code of Conduct (CoC)
  • Debian Machine Usage Policies (DMUP)

これも署名して送ります。

Advocate

既存のDebian Developerに推薦してもらうプロセスです。
あらかじめ推薦してもらえそうなあてがないとこのプロセスで詰んでしまうので、事前に根回ししておきましょう。

最近は東京Debian勉強会関西Debian勉強会が合同でオンラインの勉強会を開催しているので、ここに参加してみるとよいかもしれません。
次回はおそらく10月の中旬開催で、そのうちconnpassDebian JPにてイベントの案内がでるはずです。

Key consistency checks

申請者のGPG鍵に問題ないかチェックするプロセスです。
従来は既存のDebian Developer二人から署名(キーサイン)してもらっていないといけませんでしたが、昨今COVID-19の影響によりこの条件は緩和されました。

コミュニティでの活動で充分な成果をあげているとDebian Developerに認められ、推薦してもらえた場合もOKという条件になるようです。
もちろん従来のようにキーサインすることを推奨していますが、必須ではありません。

このあたりの詳細については、DAM Key and identity requirementsという投稿が参考になるでしょう。

Application Manager report

申請者がDebian Developerとしてふさわしいかをチェックするプロセスです。

試験官が割り当てられ、出題された問題に答えます。内容としては以下の2つの試験を受けます。

  • Debian Developer Philosophy and Procedures Test
  • Debian Developer Task and Skills Test

Debian Developer Philosophy and Procedures Testは申請者がDebianについて正しく理解しているかを確認する試験です。
こんなときはDebian Developerとしてどう振る舞うか、などが問われます。
試験官とはメールでやりとりを行い、充分な理解があると判断されたら次のステップに進みます。

Debian Developer Task and Skills Testは申請者のパッケージングに関する知識やツールを使いこなすスキルがあるかを問う試験です。
実際にバグを修正したりすることでその能力を示す必要があります。

上記の試験を終えると、試験官による合否のレポートが提出されます。

Front Desk or DAM approval

Application Manager reportの結果をもとに、Debianのアカウントを管理している人によるチェックを受けるプロセスです。
問題がなければDebian Developerとして認められ、アカウントの発行手続きが取られます。

こうしてDebian Developerになることができます。*2

まとめ

今回は、どうするとDebian Developerになれるのかについて紹介しました。
Debian Developerであるやまねさんによるなれる!Debian開発者という資料も参考になるでしょう。

あまり知られていないかもしれませんが、Debian DeveloperになるとMemberBenefitsにあるように各種優遇も受けられます。
LWNを購読できたり、Gandiで[ドメインをちょっとお安く維持(Level "E"扱いになる)できたりします。

Debianを好きな人は、ぜひDebian Developerになって活動してみませんか?

*1 よっぽど経験豊富でなんでこの人いまだに申請していないんだというケースだったら別かもしれません。

*2 Debian Developerになるのにどれくらいの期間かかるかは、人によって様々です。試験官や申請者が忙しかったりなどの要因があるからです。私の場合はちまちま進めていたので4ヶ月くらいでした。

2020-09-28

Groongaのダンプから不具合の再現に必要なカラムのデータのみを抽出するには

Groongaのサポートをしていると、Groongaのデータベースのダンプを提供いただいて、不具合を再現させることが多いです。
その際、特定のカラムに問題があることがわかっている場合、問題のカラムだけをダンプから抽出できたほうが効率が良いです。

例えば、データのロード中に問題が発生するなら、問題が発生するカラムのデータだけを抜き出して、再現しながらデバッグを進められれば、余計な処理が走らないのでデバッグしやすくなります。
ただ、データベースのダンプは巨大なことが多いです。巨大なファイルから該当のカラムのデータだけを見つけ出して抽出するのは困難です。

このような時に、groonga-command-filterというツールが使えます。
このツールはオプションに指定したカラムのデータだけをデータベースのダンプから抽出するツールです。

このツールの引数に問題のカラムを指定して実行すれば、簡単に問題のカラムのデータを抽出できます。
具体的には以下のように使います。

インストール方法

groonga-command-filterは以下のようにgroonga-command-parserをインストールすると使えるようになります。

$ gem install groonga-command-parser
使い方

例えば、以下のようなテーブルをダンプしたファイルがあるとします。(groonga-command-filterの動作をわかりやすくするために小さいデータを使います。)

table

ダンプファイルの中身は以下のようになっています。

table_create Blog TABLE_HASH_KEY ShortText
column_create Blog message COLUMN_SCALAR ShortText
column_create Blog title COLUMN_SCALAR ShortText

table_create IndexBlog TABLE_PAT_KEY ShortText --default_tokenizer TokenBigram --normalizer NormalizerAuto

load --table Blog
[
["_key","message","title"],
["grn1","Groonga message","Groonga test"],
["grn2","rakutan eggs 4 - 4 Groonga moritars","baseball result"],
["grn3","none","Groonga message"]
]

column_create IndexBlog index_message COLUMN_INDEX|WITH_POSITION Blog message
column_create IndexBlog index_title COLUMN_INDEX|WITH_POSITION Blog title

この中で、titleカラムのデータのみを抽出したい場合は、以下のようにgroonga-command-filterを実行します。

$ groonga-command-filter --include-column Blog.title ダンプファイル名

実行結果は以下のようになります。

table_create --flags "TABLE_HASH_KEY" --key_type "ShortText" --name "Blog"
column_create --flags "COLUMN_SCALAR" --name "title" --table "Blog" --type "ShortText"
load --table "Blog"
[
["_key","title"],
["grn1","Groonga test"],
["grn2","baseball result"],
["grn3","Groonga message"]]

下図の枠線内のデータ(titleカラムのデータ)が抽出できてきることが確認できます。
(このテーブルは、TABLE_HASH_KEYなので、_keyカラムの値は自動的に抽出されます。)

select-column

また、複数のカラムのデータを抽出するには、以下のように--include-columnオプションを複数回使用して実現します。

$ groonga-command-filter --include-column Blog.title --include-column Blog.message ダンプファイル名

実行結果は以下のようになります。

table_create --flags "TABLE_HASH_KEY" --key_type "ShortText" --name "Blog"
column_create --flags "COLUMN_SCALAR" --name "message" --table "Blog" --type "ShortText"
column_create --flags "COLUMN_SCALAR" --name "title" --table "Blog" --type "ShortText"
load --table "Blog"
[
["_key","message","title"],
["grn1","Groonga message","Groonga test"],
["grn2","rakutan eggs 4 - 4 Groonga moritars","baseball result"],
["grn3","none","Groonga message"]]

下図の枠線内のデータ(titleカラムとmessageカラムのデータ)が抽出できてきることが確認できます。
(このテーブルは、TABLE_HASH_KEYなので、_keyカラムの値は自動的に抽出されます。)

select-columns

上記のようにgroonga-command-filterを使うと特定のカラムの値を簡単に抽出することができます。
Groongaのデータベースのダンプから特定のカラムのデータを抽出したいケースがあったら、ぜひこのツールをお使いください。

タグ: Groonga
2020-09-29

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|