systemd で zfs legacy mount の問題を解決する

f:id:masm11:20211020212947p:plain

こんにちは、masm11 です。

ZFS に関連した mount で苦労したのでご紹介したいと思います。

ZFS の mountpoint 設定とは

ZFS は自前で mountpoint を管理しています。

zfs set mountpoint=/home/service zroot/svc

などとして mountpoint を設定しておくと、OS 起動時に

zfs mount -a

で自動的に mount してくれます。

ZFS の legacy mount とは

ですが、この機能にも限界があります。

私の場合、ZFS 内のディレクトリを bind mount しようとして、できませんでした。

bind mount ってのは、

mount --bind /home/service/storage/music /exports/music

とすることで、/home/service/storage/music が /exports/music でも見える、というやつです (シンボリックリンクと違って、cd .. でちゃんと元の場所に戻れます)。

そこで、ZFS に mountpoint を設定するのをやめました。つまり、以下のようにします。

zfs set mountpoint=legacy zroot/svc

こうすることで、/etc/fstab に記述して mount する方法に切り替えることができます。

うまくいかない

で、やってみました。/etc/fstab には以下のように書きました。

zroot/svc  /home/service  zfs  defaults 0 0

しかし、うまく起動してくれません。

10月 20 20:23:45 mike2 systemd[1]: Failed to mount /home/service.

なにかがうまく行ってないようです。

10月 20 20:23:46 mike2 systemd[1]: Starting Import ZFS pools by cache file...
10月 20 20:23:49 mike2 systemd[1]: Finished Import ZFS pools by cache file.
10月 20 20:23:49 mike2 systemd[1]: Reached target ZFS pool import target.
10月 20 20:23:49 mike2 systemd[1]: Starting Mount ZFS filesystems...
10月 20 20:23:49 mike2 systemd[1]: Starting Wait for ZFS Volume (zvol) links in /dev...
10月 20 20:23:49 mike2 systemd[1]: Finished Wait for ZFS Volume (zvol) links in /dev.
10月 20 20:23:49 mike2 systemd[1]: Reached target ZFS volumes are ready.
10月 20 20:23:49 mike2 systemd[1]: Reached target ZFS startup target.
10月 20 20:23:49 mike2 systemd[1]: Finished Mount ZFS filesystems.

ZFS はちゃんと処理されてるようです。

何がまずいんでしょう?

解決

ふと気づきました。

mount 失敗の方が、ログの上の方にある…??

なるほど、mount しようとした時に、ZFS がまだ準備できてないんですね。

ZFS の準備ができるまで mount を待たせることはできないでしょうか…

闇雲に systemd.mount(5) のマニュアルを探したところ、ありました!

x-systemd.requires=
    Configures a Requires= and an After= dependency between the created
    mount unit and another systemd unit, such as a device or mount
    unit. ...

これを使うと、自動生成される home-service.mount に Requires= が 追加されるようです。そこに zfs.target を追加できそうです。

さっそく /etc/fstab に追加してみました。

zroot/svc  /home/service  zfs  x-systemd.requires=zfs.target  0 0

これで再起動したところ、

10月 20 20:39:03 mike2 systemd[1]: Mounting /home/service...
10月 20 20:39:03 mike2 systemd[1]: Mounted /home/service.

めでたく mount できるようになりました!

まとめ

さすが systemd。fstab にまで拡張を加えるとは、やりすぎです。

そして、journal は読みづらいです。 journal の最後の方を見ても、OS 起動に失敗した理由は載ってません。 上の方を見て赤い文字を探さないといけません。 並列実行の弊害ですね。 (この辺は、プログラムのビルドに使う make コマンドも同様ですが)

自動生成される *.mount に助けられる日が来るとは思ってませんでした…

というか、legacy を使うということは、何か面倒ごとを抱え込んでいるということなので、 その場合は x-systemd.requires は必須なのかもしれませんね。

弊社ではエンジニアを募集しています。詳細は以下のページへどうぞ。

https://ingage.co.jp/recruit/

ではまた!

Ruby の Timeout.timeout の実装を読む

f:id:masm11:20211006192904p:plain

こんにちは、masm11 です。

Ruby の Timeout モジュールは便利で、

gs = TCPServer.open(0)
Timeout.timeout(5) do
  gs.accept
end

このように自由にタイムアウトを設定できます。

今回はこの実装について見ていきたいと思います。

実装を見る

Ruby のバージョンは以下のとおりです。

% ruby --version
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-linux]

では実装を見ていきます。

  def timeout(sec, klass = nil, message = nil)   #:yield: +sec+
    return yield(sec) if sec == nil or sec.zero?

sec が nil の場合、または 0 の場合は、そのままブロックを呼び出しています。 この場合はタイムアウトはなしですね。

    message ||= "execution expired".freeze

メッセージが指定されていない場合は、デフォルトのメッセージを設定しています。

    from = "from #{caller_locations(1, 1)[0]}" if $DEBUG

これは後でスレッドの名前に使っています。

    e = Error

e を Error クラスにしています。e は最終的に投げる例外を保持しているようです。

    bl = proc do |exception|

ブロックを呼び出す周辺のコードがここにまとめられています。

      begin
        x = Thread.current

これは自分自身のスレッドです。

        y = Thread.start {
          Thread.current.name = from
          begin
            sleep sec
          rescue => e
            x.raise e
          else
            x.raise exception, message
          end
        }

これは別スレッドを起動して、指定時間が過ぎたら 元のスレッドで例外を発生させています。

        return yield(sec)

指定ブロックを実行しています。 例外が発生しなかった場合はそのまま return しています。

      ensure
        if y
          y.kill
          y.join # make sure y is dead.
        end
      end
    end

別スレッドの終了処理です。

    if klass
      begin
        bl.call(klass)
      rescue klass => e
        bt = e.backtrace
      end
    else
      bt = Error.catch(message, &bl)
    end

例外クラスが指定されている場合は、それを引数として上記 proc を呼び出しています。 そのクラスの例外が発生した場合は、backtrace を bt に入れています。

例外クラスが指定されてない場合は else の方で、Error.catch を呼び出しています。 Error クラスは同じファイル中のすぐ上で定義されています。 catch/throw による大域脱出ができるようですが、 指定ブロックに tag が渡されるわけでもなく、意味はなさそうです。 そして何故返り値が bt として使えるのか全く解りませんでした。

ここから先は、タイムアウトした場合の処理です。

    level = -caller(CALLER_OFFSET).size-2
    while THIS_FILE =~ bt[level]
      bt.delete_at(level)
    end
    raise(e, message, bt)
  end

caller は backtrace を取得するメソッドだそうです。 これを使って bt から余分なものを削除し、 それを backtrace として例外を投げ直しています。

個人的には、例外の backtrace は発生した場所を正確に表していて欲しく、 余計な処理だと思います。

まとめ

Timeout.timeout が何をしているかはわかりました。ただし、

https://docs.ruby-lang.org/ja/latest/method/Timeout/m/timeout.html

timeout による割り込みは Thread によって実現されています。

ここはその通りでしたが、

C 言語レベルで実装され、 Ruby のスレッドが割り込めない処理に対して timeout は無力です。

と書いてあるにもかかわらず、割り込めてるんですよね。不思議です…

さて、弊社ではエンジニアを募集しています。詳細は以下からどうぞ。

https://ingage.co.jp/recruit

ESLint の no-multi-spaces で TypeScript のType 宣言の空白を許容させる

f:id:ishiyu1125:20210923145413p:plain

こんにちは。 @ishiyu です。

ESLint の no-multi-spaces ルールはすごく便利ですよね。

Function の定義で

function   (   aaaa ) {
  return   'test';
}

みたいな不要な空白だらけのコードでエラーを出してくれます。

また、--fix オプションを使えば自動でキレイに取り除いてくれます。

function (aaaa) {
  return 'test';
}

しかし、デフォルトの設定では視認性を上げるために空白を入れた配列もエラーになります。

const mens = [
  ["山田", "y.png",      25],
  ["佐藤", "sato.png",   19],
  ["鈴木", "suzuki.png", 22],
];
// => ESLint: Multiple spaces found before '25'.(no-multi-spaces)

こちらについては、以下のように eslintrc.json に定義しておけば、空白を許容し取り除かれなくなります。

"no-multi-spaces": {
  [
    "error", {
      exceptions: {
        "ArrayExpression": true
      }
    }
  ]
}

その他にも Object や変数宣言など JavaScript に関連する空白の許容は可能になっています。 (詳しくはドキュメントに載ってます)

しかし、TypeScript 特有の Type 宣言や Interface 宣言についてはドキュメントに記載がないため、空白を許容する設定ができず、このように書くしかありません。

type TestType = {
  name: string;
  genderCode: number;
  email: string;
}

ということで、 no-multi-spaces で TypeScript のType 宣言の空白を許容させる方法を調べてみました。

TypeScript のType 宣言の空白を許容させる

で、本題です。

試した環境

ちょっと古いけど。。。

typescript: 4.2.3
eslint: 7.23.0
typescript-eslint/perser: 4.20.0

設定するルール

exceptions"TSTypeAnnotation": true を追加すれば良いです。

これだけで、 Type 宣言時の空白チェックを無視するようになります。

"no-multi-spaces": {
  [
    "error", {
      exceptions: {
        "TSTypeAnnotation": true
      }
    }
  ]
}

なぜこれで空白が許容されるようになるのか

ESLint はコードをパースして AST (Abstract Syntax Tree (抽象構文木)) の集合に変換し、チェックを行なっています。

これは TypeScript でも同様です。 ただし ESLint も TypeScript も変換される AST の Type(NodeType) の種類がそれぞれ定義されており、微妙に異なっています。

その差異を吸収するのが typescript-eslint/perser です。

そのため、TypeScript+ESLint の構成の場合、typescript-eslint/perser のみ意識すれば良いことになります。

ちなみに、AST についてはこちらが分かりやすかったです。

no-multi-spaces の exceptions のキーは、この AST の Type 名になっています。

配列の空白を無視するために設定した ArrayExpression や今回紹介した TSTypeAnnotation も AST の Type の名前です。ですので、ESLint のドキュメントに記載がない設定も空白を許可させることが可能になっています。

この AST については AST Explorer で、確認ができます。

今回紹介したno-multi-spaces ルールに無視する AST Type を確認する場合には、以下のように設定し、typescript-eslint/perser でパースする必要があるので注意してください。

f:id:ishiyu1125:20210923144456p:plain
AST Explorer

おわりに

インゲージでは TypeScript を語りたいフロントエンジニアを絶賛募集中です。

ご応募お待ちしてます。

↓↓↓↓↓↓↓↓↓↓↓↓↓

ingage.co.jp

コマンドラインが変な状態になったら

こんにちは、masm11 です。 今回は小技を一つ紹介したいと思います。

症状

コマンドラインがこんな状態に陥って、端末を閉じるしかなかった経験、ありませんか?

  • ctrl+pctrl+h が効かない
  • Enter が効かない (押しても ^M と表示される)
  • プロンプトが左端に表示されない

等々…

解決策

そんな状態になったら、さくっと以下のコマンドを実行します。

stty sane

これだけです。たいていの症状は解決してくれます。

なお、Enter が効かない場合は、代わりに ctrl+j なら効くことが多いです。

最後に

webpack を使ってると、エラー終了時に

  • ctrl+p が効かない (^P と表示される)

になることが多いです…

さて。弊社ではこんな小ネタもご存知なエンジニアを募集しています。 詳細は以下のページへ。

https://ingage.co.jp/recruit/

N+1問題について勉強会で発表してみた

f:id:ryohei515:20210819203336j:plain

ryohei515です。

Ruby on Railsを使う上で、N+1問題は避けては通れない問題です。

インゲージでは週に1度、社内勉強会を行っているのですが、今週は私が発表担当だったため、このN+1問題を知識整理がてら、社内勉強会で共有しました。

簡単な内容ではありますが、その資料をブログで共有したいと思います。

TL;DR

N+1問題に対しては、これが結論です。

メソッド 結合方法 使用箇所
joins 内部 内部結合で結合先の条件をフィルタしたいとき
left_joins 外部 外部結合で結合先の条件をフィルタしたいとき
preload 外部 結合先のデータを使いたいとき
eager_load 外部 結合先のデータを使いたいかつ、
結合先の抽出条件を指定したいとき
includes 外部 n+1を発生させないようにしたいとき
(できれば preloadeager_load を使う。)

ただ、勉強会では

  • sizecountlength の動きはそれぞれ違うよね。
  • インゲージが提供しているサービス Re:lation だと、 eager_loadpreload 、どっちが早い?
  • パフォーマンスを意識してpluck を使っているけど、結果を hash に加工する gem (pluck_to_hash) を使えば、より使いやすくなりそう!

といった感じで色々な話に派生し、学びの多かった勉強会となりました!

終わりに

こんな技術について語り合う場が欲しいと思っている方はぜひご応募頂けると嬉しいです!

カジュアル面談も可能なので、ぜひ以下からお願いします!

ingage.co.jp

DockerのBuildKitがヒアドキュメントをサポートしました

f:id:ingnis:20201120144028j:plain

Dockerでヒアドキュメントが使えるようになるみたいです。

DockerfileでのRUNコマンドの記述が楽になりそうです。

RUN apt-get update && \
    apt-get upgrade -y && \
    apt-get install -y ...

参考サイト www.docker.com

ヒアドキュメントの機能を利用するには、環境変数 DOCKER_BUILDKIT=1にする必要があります。

buildコマンドで利用するイメージを変更するために、Dockerfileの冒頭に# syntax=docker/dockerfile:1.3-labsを追加します。

ヒアドキュメントが使用できるコマンドはRUNCOPYになります。

RUNコマンドでヒアドキュメントを使う

上のRUNコマンドをヒアドキュメント化するには以下のように記述します。

# syntax=docker/dockerfile:1.3-labs

FROM ubuntu:20.04

RUN <<EOF
apt-get update
apt-get upgrade -y
apt-get install -y ...
EOF

ヒアドキュメントを使うことで、RUNコマンドでの&& \の数珠つなぎから開放されDockerfileの可読性が上がりそうですね。

COPYコマンドでヒアドキュメントを使う

# syntax = docker/dockerfile:1.3-labs

FROM alpine

COPY <<-"EOF" /app/script.sh
    echo hello ${FOO}
EOF

RUN FOO=abc ash /app/script.sh >> /hello
  1. ヒアドキュメントの内容で/app/script.shを作成します。(EOFをダブルクオート囲っているので環境変数は展開されない)

  2. 次のRUNコマンドで/app/script.shを実行します。(/hellohello abcが記述される)

私には利用用途があまり思い浮かびませんが、マルチステージビルドで複雑なbuildをするときに使うときが来るかもしれません。

短いですが以上です。

インゲージではエンジニアを募集しいています。 ご応募お待ちしてます。

↓↓↓↓↓↓↓↓↓↓↓↓↓

ingage.co.jp

過去コミットを分割してたら、知らないファイル変更が現れた

f:id:shutooike:20210818144401p:plain

はじめに

topic-a ブランチで新たに作成した ComponentX を topic-b ブランチでも使いたくなったので、

topic-a からコンポーネントXを作成したコミットだけ cherry-pick して別PRにしたいが、

コンポーネントXを作成したコミットには他の変更(コンポーネントXの適用)も入っているため単純に cherry-pick するだけでは無理なので

コンポーネントXを作成したコミットを分割しようとしてハマった話をします。

ハマりどころ

「git 過去コミット 分割」でググるとこんな記事を見つけました。

cha-shu00.hatenablog.com

先人に感謝しつつ、なるほど git rebase -i で edit して git reset HEAD^ して、別々に commit したらいいのか・・

やってみます。

$ git log
* ccccccc (HEAD -> topic-a) fix typo
* bbbbbbb add ComponentX
* aaaaaaa fix style

git rebase -i して

$ git rebase -i aaaaaaa

pick bbbbbbb add ComponentX
pick ccccccc fix typo

# Rebase aaaaaaa..ccccccc onto aaaaaaa (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
....(omitted)

分割したいコミットを pick → edit に変更

- pick bbbbbbb add ComponentX
+ edit bbbbbbb add ComponentX

git status

$ git status
interactive rebase in progress; onto aaaaaaa
Last command done (1 command done):
   edit bbbbbbb add ComponentX
Next commands to do (105 remaining commands):
   pick ccccccc fix typo
  (use "git rebase --edit-todo" to view and edit)
You are currently editing a commit while rebasing branch 'topic-a' on 'aaaaaaa'.
  (use "git commit --amend" to amend the current commit)
  (use "git rebase --continue" once you are satisfied with your changes)

コミットを一つ戻して、git status すると

$ git reset --soft HEAD^
$ git status
interactive rebase in progress; onto aaaaaaa
(省略)

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   foo/common/elements/CoponentX.vue 
        modified:   foo/views/settings/pages/UseComponentXPage.vue 
        modified:   foo/models/A.ts
        modified:   foo/models/B.ts
        modified:   foo/models/C.ts
        .
        .
        .

ん?!??

コミット bbbbbb での変更以外のファイルまで modified になっている。

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   foo/common/elements/CoponentX.vue # <- コミット bbbbbb での変更
        modified:   foo/views/settings/pages/UseComponentXPage.vue # <- コミット bbbbbb での変更
        # 以下、知らない子たち
        modified:   foo/models/A.ts
        modified:   foo/models/B.ts
        modified:   foo/models/C.ts
        .
        .
        .        

なぜこんなことに・・・・

解決

ESLint 君の autofix の仕業でした。

webpack コンテナを落として再度同じことをやると

$ git status
interactive rebase in progress; onto aaaaaaa
(省略)

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   foo/common/elements/CoponentX.vue 
        modified:   foo/views/settings/pages/UseComponentXPage.vue 

今度はコミット bbbbbb での変更だけが表示されました!

あとは個別にファイルをコミットして rebase --continue をしたら過去コミットの分割ができました!

さいごに

そういえば前にもこんなことあったなーと思ったので備忘録として残しておきます。

というか、適切な粒度でコミットしましょう。

弊社では適切な粒度でコミットできるエンジニアを絶賛大募集中です!

カジュアル面談も有りますのでご興味あればぜひお気軽に下記リンクからお願いします!

ingage.co.jp

後日談

コンポーネントXを作成したコミットには他の変更(コンポーネントXの適用)も入っているため単純に cherry-pick するだけでは無理なので

勝手に無理だと思い込んでいたんですが、実際やってみると

$ git log
* ccccccc (HEAD -> topic-a) fix typo
* bbbbbbb add ComponentX
* aaaaaaa fix style

$ git switch main

$ git switch -c cherry-pick-component-x
Switched to a new branch 'cherry-pick-component-x'

$ git cherry-pick bbbbbbb
error: The following untracked working tree files would be overwritten by merge:
        foo/common/elements/CoponentX.vue
Please move or remove them before you merge.
Aborting
fatal: cherry-pick failed

$ git status
On branch cherry-pick-component-x
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        foo/common/elements/CoponentX.vue

nothing added to commit but untracked files present (use "git add" to track)

と cherry-pick 自体は失敗するんですが、untracked な ComponentX.vue ファイルは生成されました。

綺麗な歴史にこだわりがないなら、これで十分だなーと思いました。

ではまた!