esm アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

Named pipe でコンテナ越しにお手軽プロセス間通信?

本日の TIL は、コンテナ間でデータをやりとりするために名前付きパイプ (named pipe, FIFO) を使ってみたことでわかった利点と注意点です。

先日、ある PoC のための簡単な web アプリケーションを作っている際に、2つのコンテナ間でちょっとしたデータのやりとりをしたい場面がありました。具体的には下の図のように、 ブラウザから入力されたテキストを web のコンテナで受け取り、それを別のコンテナ (仮に processor とします) に投げて処理させ、その結果を受け取り、最後にブラウザに表示する必要があります。

f:id:hibariya:20220401094832p:plain

web と processor でのやりとりをどう実装するか少し検討してみた結果、名前付きパイプを使ってみることにしました。なぜなら、今回の用途には十分に感じられ、かつ簡単に実装できそうだったからです。ただし、もしこれが production 環境で向こう何年か動き続けるものだとしたら話は変わってきそうです。お手軽さを選択してみることができた背景には、このアプリケーションが主にプロジェクトメンバー達のラップトップ上で動く試作品で、通信方法もこのときの主眼ではなかったという点がありました。

そういうわけで実際に名前付きパイプを使ってみたところ、結論としては、確かにこの方法は簡単でした。また、シェルだけで実装できるため、実行環境に追加のソフトウェアをインストールする手間も不要という手軽さでした。しかし一方で、簡素な仕組みであるゆえに、少しでもデータのやりとりが複雑なものになると、より高級な既存のプロトコル (HTTP など) を採用する場合とくらべて、必ずしも手軽とは言いきれないということがわかりました。また、パイプを使う上での特有の注意点も見えてきました。

ここからは、より具体的な説明をするために、実際に動く例を使ってみたいと思います。その中で、この方法の注意点にも触れていきたいと思います。

どう実装したか

話を簡単にするために、冒頭の processor でやっていた実際のテキスト処理を、記事向けにスペルチェックツール aspell(1) で置き換えたサンプルアプリケーションを作ってみました。ブラウザから見ると「入力したテキストを submit したら、スペルチェックされた結果が画面に表示される」という動作になります。処理の流れは冒頭の図の通りです。

名前付きパイプを使っての web と processor との間のデータの流れは次のようなイメージです。図のように、input/output それぞれにひとつずつパイプを使います。

f:id:hibariya:20220401094857p:plain

実装に必要なのは次の4つのファイルです。

$ tree
.
├── docker-compose.yml 
├── processor
│   └── worker.sh
└── web
    ├── index.html
    └── server.rb

2 directories, 4 files

2つのコンテナ (web と processor) は Docker Compose で管理します。以下の docker-compose.yml に、各コンテナや volume の情報などを設定します。

x-env: &env
  - PROC_INPUT=/var/run/sample/proc-in
  - PROC_OUTPUT=/var/run/sample/proc-out
  - PROC_LOCK=/var/run/sample/proc.lock

volumes:
  ipc:

services:
  web:
    image: ruby:3.1-slim
    ports: ['4567:4567']
    init: true
    working_dir: /work
    environment: *env
    command: ['ruby', 'server.rb']
    volumes:
      - type: volume
        source: ipc
        target: /var/run/sample
      - type: bind
        source: ./web
        target: /work

  processor:
    image: r.j3ss.co/aspell
    init: true
    working_dir: /work
    environment: *env
    entrypoint: ['/bin/ash']
    command: ['/work/worker.sh']
    volumes:
      - type: volume
        source: ipc
        target: /var/run/sample
      - type: bind
        source: ./processor
        target: /work

名前付きパイプではファイルを読み書きする要領で通信ができるので、シェルさえあれば実装できます。コンテナ間でやりとりする場合は、名前付きパイプの共有をどうするかを考える必要が出てきますが、実は普通のファイルと同様に volume で共有できます。上記では、そのために定義した volume ipc を各コンテナの /var/run/sample に配置しています。また、名前付きパイプのファイルパスなどを環境変数として定義しています。それぞれ次の用途です。

  • PROC_INPUT: 入力用の名前付きパイプ。
  • PROC_OUTPUT: 出力用の名前付きパイプ。
  • PROC_LOCK: 排他制御 (後述) に使うための普通の空ファイル。

それでは各実装について、入力から出力まで処理の流れに沿って見ていきましょう。web 側は Sinatra で実装しました。 index.htmlPOST /process にテキストを投げるためのフォームです (ここでは省略します; gist)。

require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'
  gem 'sinatra'
  gem 'webrick'
end

set :static, true
set :public_folder, __dir__
set :bind, '0.0.0.0'

get '/' do
  content_type 'text/html'
  send_file File.join(settings.public_folder, 'index.html')
end

post '/process' do
  content_type 'text/plain'

  File.open ENV['PROC_LOCK'] do |lock|
    lock.flock File::LOCK_EX

    output = Thread.new { File.read(ENV['PROC_OUTPUT']) }
    File.write ENV['PROC_INPUT'], request.body.read

    output.value
  end
end

Sinatra::Application.run!

post '/process' のブロックで、次のような流れでパイプへの読み書きをしてブラウザにレスポンスを返しています。

  1. 出力用パイプから読み込みを別スレッドで開始。
  2. 入力用ファイルにテキストをすべて書き込む。

入力を書き込む前に出力を別スレッドで読み始めているのは、大きなテキストを渡す際に書き込みが止まってしまうことを防ぐための対応です。というのも、パイプには容量があり、その容量を超えるサイズのテキストを書き込むことはできません (ブロックするかエラーになる)。そのため、書き込みと同時に出力側からの読み出しも行なうことで、データの流れが止まらないようにしています。

また念のため、入力の書き込みから出力の読み込みまでの間に別のリクエストからの邪魔が入ることがないよう、IO#flock で排他制御をしています。ロック取得のために (入力用/出力用のパイプではなく) わざわざ別ファイルを使っているのは次のような理由で、名前付きパイプの特性上ロックの用途には使いづらいと判断したためです。

  • 入力用のパイプに流したデータの末尾 (EOF) を読み出し側に知らせるには、書き込み側でファイルを閉じるという操作が必要。しかし、ファイルを閉じるということはロックも手放してしまうことになる。ロックは出力用のパイプを読み終わるまで手放したくない。
  • 出力用のパイプを読み込むために開くには、書き込み側からもファイルを開く必要がある。しかし、ロックを取得したいタイミングは書き込み側がファイルを開くより前。

さて、上記でパイプへ書き込まれたテキストは、processor が worker.sh で読み込みます。そのテキストを入力として aspell を実行し、結果を出力用のパイプに書き込みます。

#!/bin/bash

set -e
test -p "$PROC_INPUT" || mkfifo "$PROC_INPUT"
test -p "$PROC_OUTPUT" || mkfifo "$PROC_OUTPUT"
test -f "$PROC_LOCK" || touch "$PROC_LOCK"

set +e
while :
do
  echo "Waiting input..."
  aspell -a < "$PROC_INPUT" > "$PROC_OUTPUT"
done

最初のあたりでは、各ファイルがまだ存在しなければ作成しています。あとはパイプに入力がある度に aspell をひたすら繰り返すだけです。

これで一通り準備ができました。起動してブラウザからテキストを送ると、期待した通り、スペルチェック結果が確認できます。ブラウザのスペルチェッカも反応しており、正しそうです。

f:id:hibariya:20220401094924p:plain

どんな問題があるか

名前付きパイプを使うことで、サーバ側の実装をほぼシェルだけでお手軽に済ませることができました。簡単な仕組みで、手間もかからず、うまく動いています。しかし、この方法には少なくともひとつ潜在的な問題がありました。それは、現状では標準入出力のやりとりしかできないという点です。たとえば aspell の実行途中にエラーが発生し異常終了したとしても、この仕組みでは終了ステータスを受け取る方法がないので、気づくことができません。また、標準エラー出力に何かエラーが出ている場合も、出力用のパイプに流しているのは標準出力だけなので、web からはわかりません。

ここからもし標準入出力以外のデータをやりとりしたくなった場合には、そのための仕組みを用意する必要があります。例えば、出力用のパイプを増やせば終了ステータスや標準エラー出力も受け取れるようになります。または、パイプでやりとりするデータを単なるバイトストリームではなく、JSON のような構造化された形にすることでも対応はできそうです。とはいえ、もともとお手軽さを求めて選んだ方法が、こうなってくるとあまりお手軽に感じられなくなってきました。むしろ、 Sinatra か Flask でも入れて HTTP でやりとりした方がまだ楽かもしれません。

また、この仕組みのまま時間あたりの処理能力を上げることが難しいという点も、用途によっては問題となりそうです。現状では、外部コマンドの実行という比較的コストの高い処理をひとつずつ順番に実行しているからです。もしこれを parallel に複数実行できれば処理能力を上げることはできそうですが、その場合もやはり、現状よりも複雑になることは避けられません。

おわりに

今回わかったことは大きく分けて2つです。

  • 名前付きパイプはちょっとした通信をお手軽に実装する手段として便利。ただし、やりとりが少しでも複雑になると相応の手数が必要になる。
  • 名前付きパイプは普通のファイルと似ているが、使い方にコツがある。
    • パイプには容量があり、流れないと詰まる。
    • データの末尾 (EOF) を読み込み側に知らせるには、書き込み側がファイルを閉じる必要がある。
    • パイプを開くには、反対側からも開く必要がある。

使い方が分かってしまえばお手軽な半面、複雑なことは自前でやる必要があるので大変。だから先が読めない状況では、はじめからより高級な既存のプロトコルを使った方がいいというのが今回の感想です。

参考

技術書(英語)読書会を継続している話

はじめに

こんにちは、エンジニアの 9sako6 です。

昨年の夏に社内で Patterns of Enterprise Application Architecture(以下、PofEAA と呼ぶ)を読む読書会が発足し、かれこれ半年以上継続しています。

本記事では、PofEAA 読書会の沿革や、主催としての学びをつづります。

動機

私はおそらく PofEAA という本を Martin Fowler's Bliki (ja) で知りました。PofEAA の著者 Martin Fowler 氏のブログを日本語化したサイトです。

"Patterns of Enterprise Application Architecture" というタイトルにある、"Enterprise" や "Architecture" という単語に惹かれました。 当時、エンタープライズアプリケーション特有の問題や解決策に興味があったのです。私は学生時代に競技プログラミングをかじっていましたが、エンタープライズアプリケーションでは競プロのプラクティスが通じないことがあったからです(永続化の必要性や、ハードウェア・ミドルウェアの性能などによるボトルネックの違い)。

とはいえ1人で読んで理解できるかわからないのと、途中で挫けそうな懸念があったので、複数人で読み進めたいと思いました。弊社では業務時間内の勉強会開催が認められているので、読書会を立ち上げることにしました。

PofEAA についてもう少し知りたい方は、高橋征義さんのレビューが参考になると思います。

NTTコムウェア C+ | ITジャーナリストや現役書店員、編集者が選ぶ デジタル人材のためのブックレビュー 第5回:『エリック・エヴァンスのドメイン駆動設計』、『エンタープライズアプリケーションアーキテクチャパターン』

読書会の方針

社内でお声がけして、PofEAA 読書会に興味を持ってくださった数人で集まって進め方を決めました。

読書会を開催するにあたり、私は2つの理念を持っていました。

  1. 負担が少ない
    • 私たちの知識欲と好奇心はとどまることを知らず、学ぶべき事象はいくらでもあります。そんな中で読書会の負担が大きいとなると、他に学びたいことややりたいことができなくなってしまいます。
  2. 人が集まる利点が活きる
    • 1人で読むより有益な体験をしたいと思っていました。わからない点について教えあったり、議論できる場を作りたかったです。

2つのお気持ちを共有した上で全員で議論し、読書会は以下のタイムスケジュールで進めることになりました。開催頻度は週に1回、1時間です。

時間 やること 説明
5分 チェックイン 雑談による、しゃべりのウォーミングアップ
15分 読書 読む。発表に備える
20分 発表と議論と質疑応答 ランダムで選ばれたどなたかに読んだ範囲について説明してもらう。要はどういう内容だったのかを説明してほしい。議論や質疑応答も適宜やっていく
5分 次回の計画立て
(15分) バッファ 早く終わるでもよし、どこかに割り振るでもよし、次に進むでもよし
  • その他ルール
    • オンラインで集まる(音声通話)
    • 英語の勉強をしたいわけではないので、翻訳ツールをガンガン使っていい
    • 途中参加、途中退出大歓迎。聞くだけの参加も大歓迎
    • 毎回議事録(学びや議論内容を簡単にまとめたもの)を共同編集で作る
    • 読書会後はできるだけ議事録にひとことコメントする
  • ツール
    • 集まる場所:Google Meet
    • 議事録:esa

この進め方は、DMM さんの技術書の輪読会を定着させるまでの道のりで学んだこと記事にて紹介されている「その場で読んで、まとめて、発表するスタイル」にインスパイアされたものです。事前に読んだり準備する必要がないため、負担が少なくなっています。

始動から半年経って、ふりかえり

進め方を決めて、半年ほど特に問題なく読み進められました。 複数のメンバーが毎回参加してくれたこと、議論ができたこと、議事録に学びが残されたこと、解説役として koic さんが参加してくださったことが大きかったです。

PofEAA の章立ては以下のようになっており、この時点で "PART 1: The Narratives" の "Chapter 2" まで読み終えていました。

  • Preface
  • Introduction
  • PART 1: The Narratives
    • Chapter 1-Chapter 8
  • PART 2: The Patterns
    • Chapter 9-Chapter 18

いい機会だと思い、Google の Jamboard でふりかえりを行いました。画像はそのときのボードです。

ふりかえりボード
ふりかえりボード

Keep

議論することで理解が促進されたといった意見が出ました。最初の私のお気持ち通り、複数人で集まる利点が活かされたようでした。

  • 参加負荷がそこまで高くないので継続しやすい
  • 当初の思惑どおり負荷なく参加できている
  • koic さんの背景解説助かる
  • よくわからない点について、他メンバからの補足やヒント、議論をもらえてソロで読むよりも解像度高く読めていそう
  • 一人で読むと難しい内容だけど、みんなでこうじゃないかとか考えたり、解説してもらえることである程度理解できている
  • 参加メンバーによる解説・議論が理解のたすけになっている
  • マイペースで読んで読み終わったら、気になる部分を読み返したり、気になった周辺の調べ物をできたりするので、良い感じで同期/非同期で進めている感がある。
  • 内容が難しいので、各自読んだ後に誰かに要約してもらえる形式が議論が進んでよい感じ
  • ポストRailsの世界を生きる私にとって新鮮で面白い
  • ディスカッションによる気づき
  • みんな英文書読んでいてすごい。
  • 現代の翻訳テクノロジーがすばらしい
  • 難しい書籍コンテンツである中、きちんと継続されている
  • 議事録っぽく書かれていて、感想も書かれており、足跡が残っていてよい

Problem

英語と内容の難しさが挙げられました。

  • 英語ワカラナイ
  • 実質英語版で読めていない!*1
  • 用語だけ先に出てくることが多いので、理解してない部分がわりとある
  • パターンの説明?使い所?について読んでいるが、パターン自体の説明(カタログ)は後ろの方に記載されているので、理解しづらい
  • そろそろ、Google翻訳力が尽きてしまう。。。
  • 難しい箇所を読むと要約に時間が必要になる
  • 地の (英) 文によるファウラー節をスキップしてしまって、著者の言葉の選び方のニュアンスを味わえていない
  • 最近の現場での設計思想について Active Record の影響が大きいため、理解に戸惑うことがある。
  • RailsのActiveRecordと書籍内のActiveRecordを混同して混乱しがち
  • 参加メンバーが増えなかった
  • (良くも悪くも?) 固定メンバー化している
  • 読んだ場所の難易度によっては議論時に沈黙で無の時間がすぎる
  • 背景とか脱線した話で、説明が長くなってしまう時がある (自戒を込めて)

Try

3つの Try があがりました。

和じゃなど、人目のある場所でやってみて、入り口を開放する(旧リアル和じゃっぽく)

参加メンバーが増えなかったことに対する Try です。 「和じゃ」はアジャイル事業部メンバーが集まっている Slack の雑談チャンネルです。 「やってる感」を出して興味を惹くため、人目に付くハドルで開催することにしました。

無の時間が思考中なのか本当になにもわからんなのか発言して表明する(例:「これよくわかんないっすね〜」)

オンライン特有の問題に対する Try です。 議論中、たまにみんなが無言の時間がうまれます。音声通話をしているので、無言になるとやりとりされる情報が0になります。

「よくわかんない」などでいいので、適宜お気持ちを flush しようということになりました。

わからない部分がでてきたら、解説部分に派生して時間をとってみんなで読んでみる

内容の難しさに対する Try です。 PofEAA は PART I と PART II にわかれており、PART I にはパターンの概要、PART II にはパターンの詳細が書かれていることがあります。 PART I を読んでいる現在、概要だけでは理解しにくい部分があります。そういう場面で、理解を優先して PART II の詳細な説明をみんなで読みにいくことにしました。

読書会の現在の形

ふりかえりを経て、今はこの進め方で読書会を行なっています。

時間 やること 説明
15分 読書 読む。発表に備える
35分 発表と議論と質疑応答 ランダムで選ばれたどなたかに読んだ範囲について説明してもらう。要はどういう内容だったのかを説明してほしい。議論や質疑応答も適宜やっていく
(10分) バッファ 早く終わるでもよし、どこかに割り振るでもよし、次に進むでもよし
  • その他ルール
    • オンラインで集まる(音声通話)
    • 英語の勉強をしたいわけではないので、翻訳ツールをガンガン使っていい
    • 途中参加、途中退出大歓迎。聞くだけの参加も大歓迎
    • 毎回議事録(学びや議論内容を簡単にまとめたもの)を共同編集で作る
    • 読書会後はできるだけ議事録にひとことコメントする
    • 無の時間が思考中なのか本当になにもわからんなのか発言して表明する(例:「これよくわかんないっすね〜」) ← New!
    • わからない部分がでてきたら、解説部分に派生して時間をとってみんなで読んでみる ← New!
  • ツール
    • 集まる場所:Slack 「和じゃ」(人が多い)チャンネルのハドル ← New!
    • 議事録:esa

主催して得た Tips

PofEAA の知識

いわずもがな、PofEAA を読むことで エンターブライズアプリケーション特有の問題や特徴、Layring、Domain Logic、DB とオブジェクトのマッピング等の知識が増えました。 PofEAA は例に出される内容が古い場合があるのですが、歴史を学んで温故知新という気持ちで読んでいます。

読書会をプロダクト(生産物)として育てる意識

私はツールや Web サービスを作るのが好きなのですが、読書会も同じように自分のプロダクトと見なせます。 読書会自体が価値となるように、また、読書会を通して価値を与えたいという思いで運営するようになりました。

おわりに

参加者がいる限り、これからも読書会を継続していきます。


永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

*1:メンバーによっては、日本語版を購入して英語版とあわせて読んでいます

Rails / OSS パッチ会オンライン 2022年3月のお知らせ

2022年3月の Rails / OSS パッチ会を 3月14日(月)に Discord でオンライン開催します。

この会をひとことでいうと、日頃のお仕事で使っている Rails をはじめとする OSS について、upstream にパッチを送る会です。

会には Ruby と Rails のコミッターである顧問の a_matsuda もいますので、例えば Rails に送るパッチのネタがあるけれど、パッチを送るに適しているかの判断やパッチを送る流れが悩ましいときなど a_matsuda に相談して足がかりにするなどできます。

開催時間は 17:00-19:00 となりますがご都合のあう方はぜひご参加下さい。

Discord の Rails/OSS パッチ会サーバーへの招待 URL は以下です👇

discord.gg

以下、前回の活動が関わる成果です。

koic: Rails

github.com

これからパッチ会に参加してみようという方も、ぜひどうぞ。Discord でお会いしましょう。


永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

【入社エントリ】はじめまして!ふーがと申します。

はじめに

はじめまして!ふーが(@fugakkbn)と申します。2022年3月1日に中途入社しました。
蒙古タンメン中本、餃子、コカコーラをこよなく愛する光の戦士です。どうぞよろしくお願いします。

入社エントリということで、僕がどのような想いで永和に入社し、実際に入社してみてどう感じているのかを書きたいと思います。永和の雰囲気が少しでも伝われば幸いです。

外から見た永和

僕が永和に抱いていた印象はおおむね次の3点です。このような点に魅力を感じて、永和に入りたい!と思っていました。
面接のときも似たようなことを言ったと思います(緊張しすぎて記憶がない…)。

技術力が高い / 技術が好き な人が多い

ブログの内容や登壇資料、周囲からの評判もあり、印象として1番強く持っていました。
僕自身も Ruby をはじめとした技術が大好きですし、技術者としてやっていくからには追求していきたいという想いがあります。
永和のような熱量のある環境に身を置けたらたくさんの刺激を受けられそう、と感じました。

コミュニティとの親和性が高い

これは何のデータにも基づかない勝手な持論で恐縮ですが、コミュニティの活性度と言語自体の盛衰には一定の相関があると考えています。
コミュニティを盛り上げることで Ruby の発展に貢献できたら嬉しいですし、Ruby が好きなので個人としても会社としても貢献できたらとても幸せです。

その点、イベントへの登壇や協賛をはじめ、メンバーのイベント参加を会社の制度として促している永和でなら、前述のような貢献ができそうで素敵だなと感じていました。

価値の提供

僕は"課題解決のためにどうすべきか"を考えられるエンジニアでありたいです。
なにかを作りきったという達成感も嬉しいものですが、それ以上に、お客様の「ありがとう」という一言でこの上なくやりがいを感じられると思います。

永和のホームページには「私たちのやり方」というページがあります。
ここを読むと、永和でならとてもやりがいを感じられる仕事ができるのではないかと思えて、魅力的でした。

中から見て感じる永和

そんな想いで入社して1週間余り。
「永和に入ってよかったなあ」と感じることばかりです。具体的に書いていきます。

議論が活発

この1週間でお茶会*1や開発者ブログのふりかえり会などに参加しました。また、Slackもやり取りが活発です。技術的な面でも事業部としての運営面でも、活発な議論がなされています。
議論の仕方も感情面や主観でなく「こういう理由でこうしたい」といった理論的な議論ですし、みんなで問題解決していこうという雰囲気が素敵です。

日々KAIZENが進んでいる

ドキュメントやマニュアルも、情報が古くなったり現状に即していないと役に立たなくなってしまいます。また組織としての運営も仕組みで解決できる場合がありますね。

そういった部分を"気付いたらやる"という感じでどんどんKAIZENされていっている印象を受けています。
スピード感にぜんぜん付いていけてないのですが、僕も気づいたこと・できることを積極的にやっていくことで貢献できればと思っています。

仲間を大切にしている

これは外からはあまり見て取れなかった部分ですが、チームの垣根を越えてメンバー同士のコミュニケーションを大切にしていると感じます。
例えば times*2 で何気なく疑問をつぶやくとアドバイスが集まったり、誰かのアクションをいいね!と思ったらお礼を言ったり褒めたりする文化があります。僕のちょっとしたアクションにもお礼を言ってもらえたりして、すごく嬉しかったです。

こういう文化があるとアクションを起こしやすかったりアイディアを出しやすいです。
永和はいわゆる"心理的安全性"の高い環境だと思うので、僕も積極的に前向きなフィードバックをしていこうと思います。

これからのこと

入社して日が浅いにもかかわらず、「すごいなあ」「素敵だなあ」と思うことがたくさんあります。外から見た永和の魅力だけでなく、入社してからもたくさんの魅力を感じています。
入社したばかりの僕だからこそ気づける"外から見えていない永和の魅力"みたいなものをうまく発信していけたらいいなと思います。

そして何より、これからどんなお客様、サービスや技術と関わっていけるのだろうととてもワクワクしています!
1つ1つの関わりを大切にしながら腕をみがき、楽しい"永和ライフ"を過ごしていけたら最高です。

最後までお読みいただきありがとうございます。
改めて、これからよろしくお願いします!

*1:各人が持ち寄った技術的トピックについてゆるく話す会

*2:分報。Slackに個人のチャンネルがある。

Emacs でだって Docker で開発したい!

こんにちは。wat-aro です。

Docker 環境で開発する際に VSCode の Remote Container はとても便利ですね。
でも今まで Emacs で開発してきた人は VSCode ではなく Emacs を使いたいはずです。
ここでは僕が Emacs + Docker 環境でどのように開発しているかを紹介します。

docker コマンド

まずは docker コマンドを使えなくてはなりません。
Emacs 使いのみなさんはターミナルでなく Emacs から docker コマンドを叩きたいですよね。
そんなときは docker.el です。
https://github.com/Silex/docker.el
docker image コマンドや docker compose コマンドが Emacs から実行できます。
docker compose up で立ち上げたコンテナをリスタートすることなどもできます。
まずはこれでコンテナを立ち上げましょう。

docker コンテナに繋ぐ

コンテナを立ち上げたら Emacs からコンテナに入り、コンテナ内でファイルを編集します。
開発用のコンテナ内に Emacs を入れてしまう人もいるかもしれませんが、余計な依存を開発環境にも入れたくないためその方法は取りません。
また、Emacs からコンテナにアクセスせず、ホストの Emacs から docker や docker compose コマンドを叩けばいいのかと思われるかもしれません。
僕も始めは docker compose コマンドを叩く方法を検討しました。
しかし Emacs のパッケージの多くが外部コマンドにファイルの情報を渡す際にローカルの絶対パスを渡します。
コンテナにローカルの絶対パスを渡してもそのファイルがないため動きません。
プロジェクトルートからの相対パスを渡すことも可能かもしれませんが、flycheck などを見るとファイルパスを渡している部分がマクロになっていて変更が厳しそうだったため、Emacs からコンテナ内のファイルを編集する方法を取るようになりました。
仮にプロジェクトルートからの相対パスを渡せたとしても、様々なパッケージを入れるたびに相対パスを渡すために試行錯語するくらいならコンテナ内に入ってファイルを編集するほうが楽です。

Emacs からコンテナに入る方法です。
普段 Emacs を使っていて、ssh 先に繋ぐ際には tramp.el を使いますよね。
tramp.el ではホスト側の Emacs をそのまま使えるため、使い慣れた設定やパッケージを使えます。
ただし、外部コマンドを実行する場合はリモート先にその外部コマンドがインストールされている必要があります。

コンテナに繋ぐ際には docker-tramp.el が便利です。
https://github.com/emacs-pe/docker-tramp.el

C-x C-f /docker:user@container:/path/to/file  

でコンテナに繋ぐことができます。
毎回接続先を入力するのは大変なため、emacs-helm-trampcounsel-tramp を使うと便利です。
僕は counsel-tramp を使っていますが、コンテナを起動した状態で counsel-tramp を実行すると接続できるコンテナが補完候補として表示されます。
counsel-tramp でコンテナに入った時点では / ディレクトリにいるため Dockerfile に指定した WORKDIR に移動する必要があります。。

lsp-mode

昨今はLSPを使って開発する人が多いと思います。
僕は lsp-mode を使っていますが、 docker-tramp.el でコンテナに入っている際にローカルのままの設定の lsp-mode では Language Server が立ち上がりません。

https://emacs-lsp.github.io/lsp-mode/page/remote/#sample-configuration
emacs-lsp のドキュメントを見ると :new-connectionlsp-tramp-connection を使い :remote?t 設定するとよいようです。
これを参考に solargraph の設定を書くとこのようになります。

(with-eval-after-load "lsp-solargraph"  
  (lsp-register-client  
    (make-lsp-client :new-connection (lsp-tramp-connection '("solargraph" "stdio"))  
      :major-modes '(ruby-mode enh-ruby-mode)  
      :priority 1  
      :remote? t  
      :multi-root t  
      :server-id 'ruby-ls-remote  
      :initialized-fn (lambda (workspace)  
                        (with-lsp-workspace workspace  
                          (lsp--set-configuration  
                            (lsp-configuration-section "solargraph")))))))  

lsp-mode の設定にこれを追加することでリモートの solargraph を使うことができるようになります。

まとめ

僕の Emacs + Docker での設定を紹介しました。
よいプログラミングライフを!

Rails / OSS パッチ会オンライン 2022年2月のお知らせ

2022年2月の Rails / OSS パッチ会を 2月25日(金)に Discord でオンライン開催します。

この会をひとことでいうと、日頃のお仕事で使っている Rails をはじめとする OSS について、upstream にパッチを送る会です。

会には Ruby と Rails のコミッターである顧問の a_matsuda もいますので、例えば Rails に送るパッチのネタがあるけれど、パッチを送るに適しているかの判断やパッチを送る流れが悩ましいときなど a_matsuda に相談して足がかりにするなどできます。

開催時間は 17:00-19:00 となりますがご都合のあう方はぜひご参加下さい。

Discord の Rails/OSS パッチ会サーバーへの招待 URL は以下です👇

discord.gg

これからパッチ会に参加してみようという方も、ぜひどうぞ。Discord でお会いしましょう。


永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

U+301C from UTF-8 to Windows-31J (Encoding::UndefinedConversionError) に対応する

こんにちは。ima1zumi です。

私の開発している Rails アプリでは、Excel で読み込めるように 文字コードを Windows-31J に変換して CSV を出力する機能があります。 先日、CSV 出力にて Unicode の波ダッシュ を Windows-31J に変換しようとして Encoding::UndefinedConversionError が発生して CSV 出力に失敗したことがありました。なぜエラーになるのか、どうやって対応するのかをまとめました。

まとめ

encode メソッドの fallback オプションを使って未定義文字の変換先を定義することで変換できます。

str = "\u{2014 301C 2016 2212 00A2 00A3 00AC}"

undefined_signs = {
  "\u2014" => "\x81\x5C".force_encoding(Encoding::Windows_31J), # — EM DASH
  "\u301C" => "\x81\x60".force_encoding(Encoding::Windows_31J), # 〜 WAVE DASH
  "\u2016" => "\x81\x61".force_encoding(Encoding::Windows_31J), # ‖ DOUBLE VERTICAL LINE
  "\u2212" => "\x81\x7C".force_encoding(Encoding::Windows_31J), # − MINUS SIGN
  "\u00A2" => "\x81\x91".force_encoding(Encoding::Windows_31J), # ¢ CENT SIGN
  "\u00A3" => "\x81\x92".force_encoding(Encoding::Windows_31J), # £ POUND SIGN
  "\u00AC" => "\x81\xCA".force_encoding(Encoding::Windows_31J), # ¬ NOT SIGN
}

p str.encode(Encoding::Windows_31J, fallback: undefined_signs)

なぜこれらの文字がエラーになるのか

Unicode の WAVE DASH などの文字は Windows-31J に定義されていないからです。

文字コードを変換するとは、ある文字コードの1文字を別の文字コードの1文字に変換するということです。例えば、Unicode の「あ」は Windows-31J では「あ」に対応する、というように、変換元のある文字は変換先のどの文字に対応する、という関係が1対1で紐付けられています。Unicode の 「🥺」は Windows-31J に存在しないように、ある文字コードには存在していても変換先の文字コードには存在しないため変換できない文字もあります。

Unicode の WAVE DASH(U+301C)は Windows-31J には対応する文字がありません。このため、変換しようとすると定義がないため Encoding::UndefinedConversionError になります。ですが、Unicode の FULLWIDTH TILDE (U+FF5E) は Windows-31J の に対応しているため、 「~」は Windows-31J に変換できます。

"\u301C".encode(Encoding::Windows_31J) # WAVE DASH
# `encode': U+301C from UTF-8 to Windows-31J (Encoding::UndefinedConversionError)

"\uFF5E".encode(Encoding::Windows_31J) # FULLWIDTH TILDE
# => "\x{8160}"

Windows-31J の波ダッシュは FULLWIDTH TILDE に対応するため、 WAVE DASH は変換できないという対応付けになってしまっています*1が、実務上は形が同じ文字に変換してしまいたいことはあります。そういったときに、Ruby では変換未定義文字に変換テーブルを定義することで対応できます。

fallback

ということで、未定義文字のいくつかを自前で定義します。これは String#encode のオプションの fallback を使えます。 fallback には Hash, Proc, Method を渡すことができます。ここでは Hash を使います。キーには変換元である Unicode の文字を、変換先には Windows-31J で変換先に指定したい文字のバイト列を定義します。また、作成した Stringencodingforce_encoding で変更して、文字コードを揃えておきます。

str = "\u{2014 301C 2016 2212 00A2 00A3 00AC}"

undefined_signs = {
  "\u2014" => "\x81\x5C".force_encoding(Encoding::Windows_31J), # — EM DASH
  "\u301C" => "\x81\x60".force_encoding(Encoding::Windows_31J), # 〜 WAVE DASH
  "\u2016" => "\x81\x61".force_encoding(Encoding::Windows_31J), # ‖ DOUBLE VERTICAL LINE
  "\u2212" => "\x81\x7C".force_encoding(Encoding::Windows_31J), # − MINUS SIGN
  "\u00A2" => "\x81\x91".force_encoding(Encoding::Windows_31J), # ¢ CENT SIGN
  "\u00A3" => "\x81\x92".force_encoding(Encoding::Windows_31J), # £ POUND SIGN
  "\u00AC" => "\x81\xCA".force_encoding(Encoding::Windows_31J), # ¬ NOT SIGN
}

p str.encode(Encoding::Windows_31J, fallback: undefined_signs)

このように未定義文字に対して変換規則を作ることで対応ができます。

ref: String#encode (Ruby 3.1 リファレンスマニュアル)

UnicodeからWindows-31Jに変換できない文字はどのくらいあるか

Unicode は世界中の文字を収録した文字コードで Unicode 14.0 時点で 144,697 文字が使えます。Windows-31J は主に日本語が使える文字コードで約 7000 文字を収録しています。変換できない文字は非常に多くあります。 その中でもよく出てくる記号は先ほどのような波ダッシュ「〜」で、漢字では「𠀋」「㐂」「𠮷」(つちよし)など*2があります。これらの変換できない文字をすべて対応しようとするのはあまり現実的ではありません。また、記号であれば形が同じものを同じ文字とみなすことはありますが、特に人名において、字形の異なる漢字を同じ漢字としてみなすことには注意が必要です。このように、未定義文字は単純に別の文字で置き換えればいいという問題ではありません。

未定義文字にどう対応するか

対応方針はいくつか考えられますが、どれもメリット・デメリットがありどれがベストということはありません。扱いたい文章の性質によって対応を切り分けるのが良いと思います。

(1) 入力時に変換できない文字がないかチェックし、変換できない文字は入力させない

メリットは変換できない文字がデータとして入ってこないため、安全に扱えるということです。

デメリットは Windows-31J として入力できる文字かどうかのチェックが大変なことです。また、基本は Unicode として扱って一部 Windows-31J に変換したいような文字の場合、入力できない文字が多いことが不便です。

(2) 変換できない文字は ? のような別の文字に置き換える

メリットは未定義文字があっても変換に成功することです。

デメリットは変換後の文字列から「変換できなかった文字が何であるか」が分からないことです。また、人名や住所など置換すると意味をなさない文字列がある場合、この方法をとるべきではありません。(例:𠮷田が?田になると意味をなさない)

別の文字に置き換える場合、 String#scrub で置換できます。 String#scrub (Ruby 3.1 リファレンスマニュアル)

(3) 変換できない文字があった場合はエラーとし、処理を中断する

メリットは変換できない文字に対し個別に対応できることです。デメリットは、ユーザーは処理を中断されるため不便になることです。

おまけ: 補足と用語の整理

Unicode

世界中のあらゆる文字を収録することを目標とした符号化文字集合です。

UTF-8

Unicode の符号化方式のうちの1つで、8ビット単位の可変長です。

U+xxxx

Unicode codepoint の表記方法です。

\uxxxx, \u{xxxx xxxx}

Ruby で Unicode codepoint を指定して文字を表現する記法です。 {} で括ると複数の文字を指定できます。

ref: リテラル (Ruby 3.1 リファレンスマニュアル)

Shift_JIS

JIS X 0201を1バイトで、JIS X 0208を2バイトで符号化する可変幅文字符号化方式です。

Windows-31J, CP932

Shift_JIS のマイクロソフト拡張版文字コードで、Shift_JIS とは収録されている文字が一部異なります。 Ruby では Windows-31J, CP932 は同じ文字コードです。

WAVE DASH

ダッシュ「―」が波打っている記号で、日本語では波ダッシュと呼びます。

TILDE

ダイアクリティカルマーク(発音を区別する記号)の一種です。チルダ単独で使う場合は数学記号やコンピュータ・プログラミング言語において特別な意味を持つ記号として扱われます。

チルダ - Wikipedia

FULLWIDTH TILDE

いわゆる全角チルダで、互換用の文字です。

この符号位置は、1バイト文字と2バイト文字が混在する符号化方式における重複符号化を救済するための互換用に導入された符号位置です。具体的な用途としては、EUC-JPにおいてASCIIのチルダとJIS X 0212のチルダが重複してしまうのを解決するために、JIS X 0212のチルダに対応付けるための互換用として用いるのが妥当です。

引用元: 矢野 啓介 “WEB+DB PRESS plusシリーズ [改訂新版]プログラマのための文字コード技術入門" ページ位置85%

参考資料

*1:歴史的経緯により Windows-31J の波ダッシュは FULLWIDTH TILDE に対応していますが、適切な変換先ではないと考えられます。 ref: 波ダッシュはチルダではない

*2:参考: 髙﨑さん、草彅さん、𠮷田さん、あなたの名前はこうして化ける - Qiita