経験は何よりも饒舌

10年後に真価を発揮するかもしれないブログ 

mackerel-agent configtest によるチェック強化の実装について

Mackerel Advent Calendar 2022 15日目です。

mackerel-agent configtestによるチェック機能を強化したので実装を軽く紹介します。
github.com
mackerel.io

何が変わったのか

次のようなmackerel-agent-sample.confを作成し、mackerel-agent configtestを実行する。

apikey = "abcdefg"
podfile = "/path/to/pidfile"

[foo]
command = "bar"
 
[plugin.checks.incorrect1]
command = "test command"
action = { command = "test command", user = "test user", en = { TEST_KEY = "VALUE_1" } }

[plugins.checks.incorrect2]
command = "test command"

[plugin.check.incorrect3]
command = "test command"

強化前は、Syntax OKとなる。

$ mackerel-agent configtest -conf /usr/local/etc/mackerel-agent-sample.conf
/usr/local/etc/mackerel-agent-sample.conf Syntax OK

強化後は、各設定項目typoで無効になっていたら警告を出してくれる。

$ mackerel-agent configtest -conf /usr/local/etc/mackerel-agent-sample.conf
[WARNING] foo is unexpected key. Did you mean root ?
[WARNING] plugin.check.incorrect3 is unexpected key. Did you mean plugin.checks.incorrect3 ?
[WARNING] plugin.checks.incorrect1.action.en is unexpected key. Did you mean plugin.checks.incorrect1.action.env ?
[WARNING] plugins is unexpected key. Did you mean plugin ?
[WARNING] podfile is unexpected key. Did you mean pidfile ?

実装

候補の列挙

候補とは、Did you mean ******のことで、各設定項目に該当する。
コードではconfig/config.goConfig構造体に列挙されている。

type Config struct {
	Apibase       string
        ...
        DisplayName   string        `toml:"display_name"`
        ...
        HostStatus    HostStatus    `toml:"host_status" conf:"parent"`
        ...
        HostIDStorage   HostIDStorage   `conf:"ignore"`
        ...
}
  • Apibaseのように、タグがついていない場合、フィールド名の先頭を小文字にして追加する。
  • DisplayNameのように、tomlタグがついている場合、その値を追加する。
  • HostStatusのように、フィールドの型が構造体の場合、parentタグをつけて候補を再帰的に取得する(ここだとon_start, on_stop)。
  • HostIDStorageのように、設定ファイルに書き込まれることがない場合、ignoreタグをつけてスキップする。

実装: mackerel-agent/validate.go at 5a2668daac02a5e2a406d22ddd23af6cca32e7be · mackerelio/mackerel-agent · GitHub

typoキーの検出

typoキーは、BurntSushi/toml#MetaData.Undecodedを用いて検出できる。

上記の設定ファイルの例では、次のように検出できている。ソートしているのは後の処理のため。

mackerel-agent/validate.go at 5a2668daac02a5e2a406d22ddd23af6cca32e7be · mackerelio/mackerel-agent · GitHub

func ValidateConfigFile(file string) ([]UnexpectedKey, error) {
    config := &Config{}
    md, err := toml.DecodeFile(file, config)
    ...
    undecodedKeys := md.Undecoded()
    sort.Slice(undecodedKeys, func(i, j int) bool {
	return undecodedKeys[i].String() < undecodedKeys[j].String()
    })

    // [foo foo.command
    //  plugin.checks.incorrect1.action.en plugin.checks.incorrect1.action.en.TEST_KEY
    //  plugins.checks.incorrect2 plugins.checks.incorrect2.command
    //  podfile]
    fmt.Printf("%+v", undecodedKeys)
typoキーの判定
  • fooの場合、fooは候補として列挙されていないので、typoキーはfoo
  • plugin.checks.incorrect1.action.enの場合、pluginが候補として列挙されているので、typoキーはen
  • plugins.checks.incorrect2の場合、pluginsは候補として列挙されていないので、typoキーはplugins
  • podfileの場合、podfileは候補として列挙されていないので、typoキーはpodfile
  • foo.commandは、fooが既に判定されているのでスキップ。
  • plugin.checks.incorrect1.action.en.TEST_KEYは、plugin.checks.incorrect1.action.enが既に判定されているのでスキップ。
  • plugins.checks.incorrect2.commandは、plugins.checks.incorrect2が既に判定されているのでスキップ。

スキップしないとContents of suggestion when executing configtest may not be correct · Issue #829 · mackerelio/mackerel-agent · GitHubのような不具合が生じてしまう。

実装: mackerel-agent/validate.go at 5a2668daac02a5e2a406d22ddd23af6cca32e7be · mackerelio/mackerel-agent · GitHub
Config構造体にparentタグがあるかで判定するより、候補として列挙されているかどうかで判定する方が良かったというPR:
refactor(config/validate): use `candidates` instead of `parentConfKeys` by wafuwafu13 · Pull Request #839 · mackerelio/mackerel-agent · GitHub

plugin.typo.fooの検出

プラグインの設定項目は、checks, metrics, metadataでないといけないが、Config構造体ではstringであれば良いので、MetaData.Undecodedでは検出できない。
上記の設定ファイルの例だと、plugin.check.incorrect3が検出されていない。

type Config struct {
   ...
   Plugin map[string]map[string]*PluginConfig `conf:"parent"`
   ....
}

そのため、config.Pluginを別途調べる必要がある。

実装: mackerel-agent/validate.go at 5a2668daac02a5e2a406d22ddd23af6cca32e7be · mackerelio/mackerel-agent · GitHub

結果の表示

TOMLの形式がおかしいなど致命的なエラーは赤、typoの警告は黄で表示する。
\x1b[31m *** \x1b[0mのように文字列で指定するとWindowsでうまく表示できないのでfatih/colorを使用した。

展望

今はtypoキーをソートして表示しているけど、余裕があれば設定ファイルの行数を取得してその順番通りにtypoの行数も表示したい。

Rust学習進捗2022

フロントエンド界隈のOSSで目立ってきたり、Linuxに取り込まれたり、放ってはおけない言語、それがRust。

1年前くらいに入門したこの本に再び目を通した。
gihyo.jp

実践で書いてみたくなったから awesome-alternatives-in-rust に目を通してテストが足りてなかったjqlにコミット。
github.com
github.com

Denoにもちょこっとコミット。
github.com
wafuwafu13.hatenadiary.com

自分でも何か作りたくなったから mkr の Rust実装 mkrust を作成。基本的なことを一通り終えたから開発が止まっている。
github.com
github.com

Cと比較されることが多いのでC版の mkrc を作成。
github.com

メモリ管理の良さを実感できなかったのでガベージコレクションアルゴリズムと実装を読んだ。
tatsu-zine.com

GCが搭載されてないといかに大変かがわかった本。
book.mynavi.jp


Do'er Advent Calendar 2022 の1日目でした。

deno/std/nodeのfs.DirentでisBlockDeviceの判定ができないのはRustに実装がされていないから...ではない

追記: Rustにis_block_deviceがあって、Denoにはなかった
github.com


fs.readdir(path, options, callback)optionswithFileTypes: trueを指定すると、fs.Direntが返ってくる。
fs.Direntdirent.isBlockDevice()を使った例が以下。

$ node
Welcome to Node.js v16.13.1.
Type ".help" for more information.
> const files = fs.readdirSync(".", {withFileTypes: true})
undefined
> files[0].isFile()
true
> files[0].isBlockDevice()
false

しかし、Deno(std/node)では使えない。

$ deno
Deno 1.25.4
exit using ctrl+d or close()
> import {readdirSync} from "https://deno.land/std@0.157.0/node/fs.ts"
undefined
> const files = readdirSync(".", {withFileTypes: true})
undefined
> files[0].isFile()
false
> files[0].isBlockDevice()
Uncaught Error: Not implemented: Deno does not yet support identification of block devices
    at notImplemented (https://deno.land/std@0.157.0/node/_utils.ts:23:9)
    at Dirent.isBlockDevice (https://deno.land/std@0.157.0/node/_fs/_fs_dirent.ts:8:5)
    at <anonymous>:2:10

deno_std/node/_fs/_fs_dirent.tsを見るとnotImplementedとされている。

export default class Dirent {
  constructor(private entry: Deno.DirEntry) {}

  isBlockDevice(): boolean {
    notImplemented("Deno does not yet support identification of block devices");
    return false;
  }
  ...

std/nodeのreaddirSyncは内部的にはDeno.readDirSyncが使われているのでDenoのコードを見にいくとそれっぽい箇所があった。
is_fileがあるから上記の例でfiles[0].isFile()が使えている。

  FsStat {
    is_file: metadata.is_file(),
    is_directory: metadata.is_dir(),
    is_symlink: metadata.file_type().is_symlink(),
...

metadataの参照先はRustなのでRustのコードを見にいくとそれっぽい箇所があった。

impl Metadata {
    #[must_use]
    #[stable(feature = "file_type", since = "1.1.0")]
    pub fn file_type(&self) -> FileType {
        FileType(self.0.file_type())
    }

    #[must_use]
    #[stable(feature = "rust1", since = "1.0.0")]
    pub fn is_dir(&self) -> bool {
        self.file_type().is_dir()
    }

    #[must_use]
    #[stable(feature = "rust1", since = "1.0.0")]
    pub fn is_file(&self) -> bool {
        self.file_type().is_file()
    }

    #[must_use]
    #[stable(feature = "is_symlink", since = "1.58.0")]
    pub fn is_symlink(&self) -> bool {
        self.file_type().is_symlink()
    }
...

ここにis_block_deviceを生やせば解決するはず...だけどメタ的な意味でRustわからん

JSのオブジェクトの分割代入で既定値が割り当てられるのはundefinedの場合のみ

「JSのオブジェクトの分割代入で既定値が割り当てられるのはundefinedの場合のみ」ということは、MDNのAssigning to new variable names and providing default valuesAssigned a default value in case the unpacked value is undefined.と書いてあることや、13 ECMAScript Language: Expressionsの13.15.5.3 Runtime Semantics: PropertyDestructuringAssignmentEvaluation4. If Initializeropt is present and v is undefined, thenとあることから分かる。

ここからNode.jsのfs.readを使ってみていく。

node/lib/fs.js#L605~ に以下のコードがある。

function read(fd, buffer, offsetOrOptions, length, position, callback) {
...
      } else if (arguments.length === 3) {
      // This is fs.read(fd, bufferOrParams, callback)
        if (!isArrayBufferView(buffer)) {
        // This is fs.read(fd, params, callback)
          params = buffer;
          ({ buffer = Buffer.alloc(16384) } = params ?? kEmptyObject);
        }
        ...
...
   ({
      offset = 0,
      length = buffer.byteLength - offset,
      position = null,
    } = params ?? kEmptyObject);
...
   validateBuffer(buffer);
...

例えばfs.read(fd, {offset: 1}, callback)というように呼び出すと、
params = {offset: 1}
({ buffer = Buffer.alloc(16384) } = {offset: 1}
buffer = Buffer.alloc(16384)
というように処理される。

もしfs.read(fd, {buffer: null}, callback)というように呼び出すと、
params = {buffer: null}
({ buffer = Buffer.alloc(16384) } = {buffer: null}
buffer = null
というように処理される。
この場合、validateBufferが呼びされる前にlength = buffer.byteLength - offsetbuffer = nullが参照されてしまう。

これを解決したのがこのPR。
github.com

ちなみにdeno_stdは現時点でNodeのv18.8.0との互換を保っているため、以下のように意図的にnullを参照するようにしている。
github.com

// @ts-ignore: Intentionally create TypeError for passing test-fs-read.js#L87
length = opt.buffer.byteLength;

RFC8259 の「Implementations MUST NOT add a byte order mark to the beginning of a networked-transmitted JSON text」について

プログラマのための文字コード技術入門』の p.216 にある、
JSONでは、データ先頭にBOMをつけないことが求められています(RFC8259)」
についてちょっとだけ詳しく調べる。

RFC8259の該当箇所は8.1. Character Encodingで、
「Implementations MUST NOT add a byte order mark (U+FEFF) to the beginning of a networked-transmitted JSON text」
とある。
続いて
「In the interests of interoperability, implementations that parse JSON texts MAY ignore the presence of a byte order mark rather than treating it as an error」
とあるように、相互運用性の観点から、BOMがあればエラーとして扱うのではなく、無視してもいいらしい。

BOM(Byte Order Mark)とは、ビッグエンディアンかリトルエンディアンのどちらのバイト順を採用しているかを示すために、データの先頭に付ける印のこと。
U+FEFFの符号位置を用い、ビッグエンディアンではFE FF、リトルエンディアンではFF FEという2バイトの列になる。
符号位置は、文字コード表の中の位置のこと。
ビッグエンディアンは、上位8ビットが先頭にくるバイト順、リトルエンディアンは、下位8ビットが先頭にくるバイト順のこと。
U+FEFFU+は、Unicodeの符号位置を表すのに付ける接頭辞。

16ビットのデータを8ビット単位のバイト列にするときには、ビッグエンディアンかリトルエンディアンのどちらを採用するかの問題がある。
Unicodeの符号化方式の中の1つであるUTF-16は、16ビット単位であるためこの問題が発生し、解消するためにBOMを付けることがある。

JSONでBOMを付けないとなると、バイト順の区別はどうするのだろうと思ったところ、同じ RFC8259 8.1. Character Encoding にこう書いてあった。
JSON text exchanged between systems that are not part of a closed ecosystem MUST be encoded using UTF-8
UTF-16を扱わないのにRFCなぜBOMに言及しているのだろうと思ったところ、『プログラマのための文字コード技術入門』の p.154 にこう書いてあった。
「バイト順が問題になることのないUTF-8にはBOMは本来関係がありません。ところが、BOMをUTF-8で表現した3バイトの値(EF BB BF)が、UTF-8のデータ列の先頭についていることがあります。バイト順の印という元々の意味を離れて、UTF-8のデータであることの印として利用価値があるという考えもあります。」

ネットワーク転送されるJSONにはその利用価値は必要なく、相互運用性に支障をきたすから、「Implementations MUST NOT add a byte order mark to the beginning of a networked-transmitted JSON text」とされている。



BOMに関する実際の問題の例として、node-fetchのHandling a BOM with .json() #541というissueにある、レスポンスをJSONとして取得する際にエラーが起こるという問題がある。
node-fetchの`.json()`には内部的に`JSON.parse`が用いられており、引数のtextにBOMが含まれていれば、そこでエラーが生じてしまう。JSONの構文はRFC8259に準拠しているからだ。
ユーザー側の解決例としてはここにあるように、textとして取得してから、BOMに当たる0xFEFFを除去し、JSONにする例がある。
node-fetch側のリリースされている解決法は、fix: handle bom in text and json #1482にあるように、内部でbuffer.toString()ではなくTextDecoder().decode()を使うようにしている。
buffer.toString()では、BOMに当たる0xFEFFがあったとしてもそのまま文字列化してしまい、それをJSON.parseするためエラーが生じる。
TextDecoder()のパラメータであるutfLabelのデフォルトは"utf-8"
そして重要なのがデフォルトがfalseTextDecoder.ignoreBOM
命名がややこしいが、TextDecoder.prototype.ignoreBOM not working as expectedの回答に
「If on the other hand you want it to be removed from the output, then leave it as false, the parser will treat it specially, and remove it from the output」
とあるように、TextDecoder().decode(buffer)の結果にはBOMが含まれなくなり、問題なくJSON.parse(text)ができる。


このように目に見えないBOMと戦ってJSONが生成される例がある。

野良コミッターがOSSの仕様を決める

OSS活動を始めて今まで157PRがCloseされた。翻訳やtypoの修正やテスト、ドキュメントの整備も含まれるけど、野良コミッターでも仕様を決める、もしくはそれに近いことができたのでいくつかまとめておく。

新しい仕様を追加する

このスライドに詳しく書いた

github.com


配列操作のreducecurrentIndexを使えるようにした

github.com

既存の仕様を変更する

いわゆるbreaking changeに当たるが、注意しないとバグを埋め込むことになる
wafuwafu13.hatenadiary.com

github.com

開発のプロセスを改善する

lintが通らなかったらマージできないようにした
github.com

ややこしいけどこれも開発のプロセス改善
wafuwafu13.hatenadiary.com

開発の方針を決める

denoland/deno_std/node/_tools/testに生成されるNodeのバージョンを上げた
github.com

denoland/deno_std/pathdenoland/deno_std/node/pathの関係を整理して進めていった
github.com


こんな感じでorgに所属してない、コミッター認定されてない野良コミッターでも頑張れば割と大きい部分の仕様に関わることができる。
GitHubでの活動はGitHubの登場はコミッタを特権階級から追い落とす革命だった - Hello, world! - s21gのタイトル通り、何の資格も持っていない文系大学生にとっては飛び道具だった。

日本の方のSocket migration for SO_REUSEPORTという発表を知って、こういうことができるようになりたいと思った。
こういうこととはどういうことかを考えると、世界の仕様を決めるということで、今まで自分が何をできてきたのかをまとめたくなって書いた。
逆に何が足りないかを再認識できてきたので、それを踏まえて活動を進めていく。

機械学習の知識・2022春

  • 数IA,IIBはセンター試験8~9割とってた記憶
  • 数IIIは以下のように独学した

wafuwafu13.hatenadiary.com

  • 以下の本は何周もして理解を進めてきた

wafuwafu13.hatenadiary.com

qiita.com
qiita.com

  • OSS活動の指標の一つとしてInterested in AI Frameworksを掲げ、まずはGiNZAにコミットした

github.com
wafuwafu13.hatenadiary.com

scrapbox.io

  • 統計検定1級に落ちたことがある

wafuwafu13.hatenadiary.com

  • コンペに参加する予定

www.nishika.com

  • 英語: 論文読んだりはまだできていない スピーキングはDMM英会話で修行中

wafuwafu13.hatenadiary.com

  • 以下の本は何度か理解しようと試みてきたが進捗はイマイチ

  • その他読んで面白かった本

人工知能のための哲学塾

人工知能のための哲学塾

Amazon

  • 積読: 数学力とKaggleでメダルを取る力をつけたい 卒論の一環でSNSのテキストアナリティクスをする予定