行動すれば次の現実

テック中心の個人ブログ

【Axlsx】Cellのtypeに:dateを指定しているのに日付型のセル書式にならない

前回同様、またAxlsx関連のマニアック話ですが、なぞ仕様に嵌ってしまったので記録として残しておきます。 同じように悩んでしまっている方の一助になれば幸いです。

Cellのtypeに:dateを指定しているのに日付型のセル書式にならない

以下のようにadd_cellで日付型オブジェクト(Date.today)、typeに:dateを指定しているのにも関わらず、セルの書式が日付型として入力されない事象が発生しました。

p = Axlsx::Package.new
ws = p.workbook.add_worksheet

header_style = ws.styles.add_style(border: { style: :thin, color: '000000' }, bg_color: 'E4E4E4')
value_style = ws.styles.add_style(border: { style: :thin, color: '000000' })

row = ws.add_row
row.add_cell('日付', style: header_style, type: :string)
row.add_cell(Date.today, style: value_style, type: :date)

▼日付型ではなく数値データ入力されている

なお、add_cellメソッドというのはCellをnewしているだけのシンプルなプログラムです。

# Adds a single cell to the row based on the data provided and updates the worksheet's autofit data.
# @return [Cell]
def add_cell(value = '', options = {})
  c = Cell.new(self, value, options)
  self << c
  worksheet.send(:update_column_info, self, [])
  c
end

axlsx/row.rb at 8e7b4b3b7259103452c191f73ce0bf64f66033fe · randym/axlsx · GitHub

ちなみにこの事象は、以下のようにstyleは未指定、type :dateのみ指定する場合だと正しく日付型で入力されます。

p = Axlsx::Package.new
ws = p.workbook.add_worksheet

row = ws.add_row
row.add_cell('日付', type: :string)
row.add_cell(Date.today, type: :date)

▼日付型として入力されている

どうやら、Cellをnewする時にstyleとtype :dateを併用するとセルの書式設定が日付型として認識されないという動きのようです。

styleに明示的にnum_fmtを指定すると日付型の書式が適用される

小一時間ほど悩んでいましたが、以下のドキュメントに答えが記載されていました。

Method: Axlsx::Styles#add_style — Documentation for randym/axlsx (master)

styleにnum_fmtというオプションで日付フォーマットを指定することで、セルの書式に日付型が適用されるようです。

p = Axlsx::Package.new
ws = p.workbook.add_worksheet

header_style = ws.styles.add_style(border: { style: :thin, color: '000000' }, bg_color: 'E4E4E4')
date_style = ws.styles.add_style(num_fmt: Axlsx::NUM_FMT_YYYYMMDD, border: { style: :thin, color: '000000' })

row = ws.add_row
row.add_cell('日付', style: header_style, type: :string)
row.add_cell(Date.today, style: date_style, type: :date)

▼日付型として入力されている

Cellをnewする際に、styleとtype :dateを併用する場合は、明示的にnum_fmtを指定する必要があるようです。

なぜこのような動きになるかというと、Cellクラスのコードを見るとわかるように、デフォルトの日付用style(STYLE_DATE)がオプションで指定したstyleで上書きされてしまうからです。

def cast_value(v)
  return v if v.is_a?(RichText) || v.nil?
  case type
  when :date
    self.style = STYLE_DATE if self.style == 0
    v

axlsx/cell.rb at 8e7b4b3b7259103452c191f73ce0bf64f66033fe · randym/axlsx · GitHub

Cellに対してオプションでstyleを指定してしまうと、typeごとに定義されたデフォルトのstyleが上書きされてしまい、セルの書式設定が無効となるようです。

Axlsxで出力したExcelをRSpecでテストする方法

RubyでExcelのインポート、エクスポートを実装する場合は、Axlsxを使用することが多いかと思います。

Axlsxでエクセルをインポートする処理をRSpecでテストするというコードはよく見かけるのですが、 エクスポートに関してはあまり見たことがありません。

先日、弊社プロジェクトでExcelエクスポートのRSpecを書く機会がありましたので記事にまとめてみました。誰かの参考になれば幸いです。

github.com

Axlsxで出力したExcelの内容をRSpecでテストする方法

ExcelExportServiceというExcel出力用クラス(①)をテストするにあたり、出力されたExcelファイル(②)をRSpecでアサーション(③)する場合を想定して説明します。

▼ ① ExcelExportServiceの実装内容(一部抜粋)

class ExcelExportService
  def call
    Axlsx::Package.new do |p|
      p.workbook.add_worksheet(name: 'Materials') do |sheet|
        # 出力処理が実装されている
      end
    end
  end
end

▼ ② 出力されたExcel

▼ ③ RSpecのテストコード

package = ExcelExportService.new.call
expect(package.workbook.worksheets[0].rows[0][0].value).to eq 'No'
expect(package.workbook.worksheets[0].rows[0][1].value).to eq '名字'
expect(package.workbook.worksheets[0].rows[0][2].value).to eq '名前'
expect(package.workbook.worksheets[0].rows[0][3].value).to eq '年齢'

expect(package.workbook.worksheets[0].rows[1][0].value).to eq 1
expect(package.workbook.worksheets[0].rows[1][1].value).to eq '田中'
expect(package.workbook.worksheets[0].rows[1][2].value).to eq '太郎'
expect(package.workbook.worksheets[0].rows[1][3].value).to eq '30'

expect(package.workbook.worksheets[0].rows[2][0].value).to eq 2
expect(package.workbook.worksheets[0].rows[2][1].value).to eq '山田'
expect(package.workbook.worksheets[0].rows[2][2].value).to eq '花子'
expect(package.workbook.worksheets[0].rows[2][3].value).to eq '34'

ExcelExportService.new.call(①)はAxlsx::Packageオブジェクトを返却しています。 テストコード(③)には、出力されたエクセル(②)と対応する形で、1次元配列や2次元配列形式で対象データを特定してアサーションを実行しています。

Axlsx::Packageオブジェクトにはworkbookというオブジェクトが格納されており、さらにその下にworksheetsオブジェクト、rowsオブジェクトが格納されています。

# 以下の構成でオブジェクトが格納されている
Axlsx::Package >> Axlsx::Workbook >> Axlsx::Worksheet >> Axlsx::Row

Axlsx::Worksheetは一次元配列の形式であり、Excelのシートと対応しています。

Axlsx::Rowは二次元配列の形式であり、1つ目の配列が行番号、2つ目の配列が列番号に対応しています。 また、セルの表示形式によってAxlsx::Rowに格納されているデータタイプも変更されます。

単純な構成ですので、簡単にテストコードが実装できるかと思います。

react-virtualizedのMultiGridでアプリ開発したときのTIPS

f:id:furu07yu:20220415135534p:plain

react-virtualizedとは

SPAで構築されたReactアプリでも、大量のリストを画面いっぱいに表示しようとすると初期描画に時間がかかってしまいます。

これは、画面に見えてない部分も含めて全てのデータをレンダリングしてしまっていることが原因です。

react-virtualizedを使用することで画面に見えている部分のみレンダリングされることができます。

画面をスクロールしたときに、表示されている領域のみのデータが描画される仕様になっていますので、効率的なレンダリングが実現できます。

github.com

react-virtualizedで使用できる様々なコンポーネント

react-virtualizedにはいくつかのコンポーネントが用意されています。簡単に代表的なコンポーネントを紹介します。

  • Grid: エクセルのようにデータを表示することができるコンポーネントです。行と列の固定はできません。
  • MultiGrid: エクセルのようにデータを表示することができるコンポーネントです。行と列の固定が可能です。
  • List: SNSのタイムラインのようにリスト形式でデータを表示することができるコンポーネントです。
  • Table: Listにヘッダーが付いた形式で表示することができるコンポーネントです。
  • Collection: いろいろな大きさのカードを横と縦に配置できるコンポーネントです。

今回は私がよく使用するMultiGridコンポーネントについての知見を共有したいと思います。 MultiGridを使用すると下記のようなエクセルライクな画面が実装できます。

f:id:furu07yu:20220414185459g:plain

基本的な実装方法は公式リファレンスに記載されております。

しかし、あくまでもシンプルな構成を想定している内容になっています。実際にアプリ開発していくと結構なクセがありましたので、その辺りの知見を中心にいくつかピックアップしています。

※react-virtualized v9.22.3に準拠しています

カラムの横幅を動的に変更させたい

カラムの横幅を動的に変更させたい場合、columnWidthというプロパティで制御できます。 columnWidthには引数でindexが渡ってきますので、それを使って横幅を変えることが可能です。

<MultiGrid
  columnWidth={({ index }) => {
    if(index === 0) {
      return 50;
    }
    // index によって返却値の出し分けが可能
    return 100;
  }}
/>

カラムの高さを動的に変更させたい

カラムの高さを動的に変更させたい場合、rowHeightというプロパティで制御できます。 rowHeightには引数でindexが渡ってきますので、それを使って高さを変えることが可能です。

<MultiGrid
  rowHeight={({ index }) => {
    if(index === 0) {
      return 20;
    }
    // index によって返却値の出し分けが可能
    return 40;
  }}
/>

React Hooksでreact-virtualizedを使いたい

リファレンスはクラスベースで記載されておりますが、もちろんHooksでも使用できます。

注意点としてはrecomputeGridSize等のPublic Methodが使用できるようにuseRefでGrid要素への参照ができるように設定することです。

function Grids(props) {

  const gridsEl = useRef(null);

  useLayoutEffect(() => {
    // gridsEl経由でrecomputeGridSizeを実行する
    if (gridsEl) {
      gridsEl.current.recomputeGridSize();
    }
  }, [
    anyProps
  ])

  return (
    <MultiGrid
      // gridsElをrefに設定する
      ref={gridsEl}
    />
  )

セルを結合させたい

MultiGridでセル結合ができれば素晴らしいのですが、この記事を執筆時点(v9.22.3)では残念ながらサポートされていません。

have some cell merge properties? · Issue #1637 · bvaughn/react-virtualized · GitHub

しかし、セル結合されているかのように実装することは可能です。

例えば固定ヘッダーの中で特定のセルを列結合させたい場合は一つのcolumnIndex中に、あたかも2つ列が存在しているかのようにデザインすることでそれが実現できます。

f:id:furu07yu:20220414230507j:plain

上記のように各日付のヘッダーとして発注日を列結合したようなグリッドを作りたいとします。 その場合は、一つのcolumnIndexの中にそれぞれの行を実装することで実現できます。

1行目は単純に1つのcolumnIndexの中に発注日のセルを配置します。

2行目はセル結合されているようにしたいので、1つのcolumnIndexの中にtableタグで7列のテーブルを実装するようにします。

このようにすることで、かなり強引ではありますが、あたかもセル結合されているかのように実装することが可能です。

固定列でもスクロールさせたい

固定列でもスクロールさせたい場合、enableFixedColumnScrollにtrueを設定することで固定列でのスクロールを有効にできます。

そのままではスクロールバーが表示されてしまいますので、一緒にhideBottomLeftGridScrollbarもtrueにしておくことをおすすめします。

固定行でもスクロールさせたい

固定行でもスクロールさせたい場合、enableFixedRowScrollにtrueを設定することで固定行でのスクロールを有効にできます。

そのままではスクロールバーが表示されてしまいますので、一緒にhideTopRightGridScrollbarもtrueにしておくことをおすすめします。

Railsアプリが爆速になるパフォーマンスチューニング集

Railsアプリを速くするためのパフォーマンスチューニング集をまとめました。 これらの手法を用いることで、弊社アプリにおいて、今まで30秒ほど掛かっていた処理が1.5秒まで短縮することができました。

あくまでもアプリケーションレイヤーでできるパフォーマンスチューニングのみに絞っていますので、テーブルインデックスやサーバーのスケールアップなどは対象外としています。

パフォーマンスに悩んでいる方の助けになれば幸いです。

N+1問題の対策

言わずとしれたN+1問題の対策は、手っ取り早くパフォーマンス改善が見込めます。

「N+1問題とは何か?」についてはご存じの方が多いと思いますので、ここでは説明を割愛させていただきます。

N+1問題を解消する手段としては、テーブルからデータ取得する際にincludesなどにより関連するテーブルのデータも含めて取得するのが有効的です。

@blogs = Blog.includes(:user)

また、N+1問題が検出できるbulletというgemもありますので、導入を検討してみると良いでしょう。

includesは計画的に

includesはとても便利な機能ですが、過度にincludesしてしまうとメモリを消費してしまうというデメリットがあります。使用する際は後述のキャッシュ化などを適用した上で計画的にincludesするのがポイントです。

テーブルデータのキャッシュ化

テーブルデータのキャッシュ化もパフォーマンス改善にはとても効果的です。

ループ処理の中で、あるテーブルに対してアクセスしなければならない場合、ループ毎にテーブルをfindするのは非効率です。 事前に必要なテーブルデータを取得しておき、そのキャッシュを使ってアクセスするほうが何倍も速くなります。

例えば以下のような処理です。

blogs = Blog.order(:id)
blogs.each do |blog|
  # blogsの件数分findが実行されてしまう
  category = Category.find(blog.category_id)
  # 省略...
end

このようなコードだとblogsの件数分categoriesテーブルへのfindが実行されてしまいます。 blogsの件数が多い場合は遅くなる要因となります。

下記のようにincludesを使ってcategory一緒に取得する方法も有効的かと思いますが、 仮にblogsが1万件、categoryが10件しかない場合、1万件分のblogオブジェクトにcategoryが乗ることになりますので、無駄なメモリを消費してしまいます。

blogs = Blog.includes(:category).order(:id)
# blogが1万件、categoryが10件しかない場合
# 同じようなcategoryがblogオブジェクトに1万件セットされてしまう

blogs.each do |blog|
  category = blog.category
  # 省略...
end

そのため、categoryのテーブルデータを事前に取得しておくことで メモリ負荷を抑えられる且つ、パフォーマンスの向上も期待できます。

blogs = Blog.order(:id)
categories = Category.all.index_by(&:id)

blogs.each do |blog|
  category = categories[blog.category_id]
  # 省略...
end

index_by(&:id)を使用することで、idをkeyとしたcategoryのHashが構築されますので、Arrayを使うよりも高速にアクセスすることができます。

pluckで必要最低限のカラムのみ取得する

取得したテーブルデータの中で、特定のカラムのみしか使用しない場合はpluckを使用することをおすすめします。

例えばblogのタイトルを出力するプログラムの場合、下記のようにblogオブジェクトを全て取得する必要はないと思います。

blogs = Blog.order(:name)
blogs.each do |blog|
  p blog.name
end

必要なデータはnameだけですので、pluck(:name)をメソッドチェーンしてnameの配列を取得するようにします。

blog_names = Blog.order(:name).pluck(:name)
blog_names.each do |blog_name|
  p blog_name
end

pluckを使用することでメモリの効率化が図れますし、SQLクエリの負荷を抑えられることになりますので、パフォーマンス向上にも繋がります。

# pluckを使用しない場合
#   アスタリスク(*)で全項目のデータを取得してしまう
SELECT "blogs".* FROM "blogs" ORDER BY "blogs"."name" ASC

# pluckを使用した場合
#   nameのみに絞られる
SELECT "blogs"."name" FROM "blogs" ORDER BY "blogs"."name" ASC

バルクインサートで一括登録・更新する

SQLの中でもinsert(updateも同様)は特に遅いです。insertの回数を減らすことでパフォーマンス改善に繋がるケースは多くあります。

activerecord-importというgemを使うことで一括insert(バルクインサート)が可能となります。

Rails6以上であれば標準でバルクインサートがサポートされましたので、そちらも効果としては同様になります。

csv_records.each do |csv_record|
  Product.create(name: csv_record[:name], price: csv_record[:price])
end

このようなコードだとcsv_recordsの件数分productのinsert文が実行されてしまいます。

バルクインサートを用いることでinsert文が一回で済むようになります。

insert_products = []
csv_records.each do |csv_record|
  insert_products << Product.new(name: csv_record[:name], price: csv_record[:price])
end

Product.import insert_products

activerecord-importを使用する場合、バリデーションが効かないためエラーハンドリングが出来ないというのがデメリットかと思います。

その場合はvalid?メソッドを使用して事前にバリデーションを実行することでエラーハンドリングを実装することができます。

insert_products = []
csv_records.each do |csv_record|
  product << Product.new(name: csv_record[:name], price: csv_record[:price])
  unless product.valid?
    # エラーハンドリングをここに実装
  end
  insert_products << product
end

Product.import insert_products

関連オブジェクトを予め詰めておく

Railsのログを確認してみると大量のSQLが発行されていることがしばしばあります。 ほとんどのケースはN+1問題が原因となっています。

例えば以下のようなコードがあった場合、blog.userの実行によりN+1問題が発生してしまいます。

blogs = Blog.order(:id)
blogs.each do |blog|
  # 省略...
  blog.user.company_id
  # 省略...
end

下記のようなSQLが大量に発行されます。

SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2

includesを使用することでN+1問題は回避されますが、 メモリ効率を考えると過度なincludesは避けたいです。

そのような場合はN+1を防止するためにレシーバーとなるモデルオブジェクトに関連しているオブジェクトを予めセットしておくことが有効です。

blogs = Blog.order(:id)
users = User.all.index_by(&:id)

blogs.each do |blog|
  # 省略...
  user = users[blog.user_id]
  # あらかじめblogオブジェクトにuserをセットしておく
  blog.user = user
  blog.user.company_id
  # 省略...
end

このようにすることで、N+1が抑えられる且つ、過度なincludesによるメモリ負荷を抑えることができます。

ただし、この手法を用いると可読性が極端に悪化しますし、セットする関連オブジェクトの最新状態が保証されないというリスクも伴います。どうしてもパフォーマンスを向上させたいケースを除いては使用しないほうが良いでしょう。

モデルのバリデーションを無効にする

モデルのバリデーションといえど侮れません。負荷のかかる処理は存在します。

例えばモデルのバリデーションであるuniquenessは新規登録や更新処理が実行されると必ず以下のようなSQLが発行されてしまいます。

SELECT 1 AS one FROM "products" WHERE "products"."code" = $1 AND "products"."department_id" = $2 LIMIT

もしバリデーションエラーが発生しない前提のロジックなのであれば意図的にバリデーションを無効にすることでパフォーマンスが向上します。 バリデーションを無効にすることで予期せぬデータが混入するリスクが伴いますので、あまり推奨はしません。

あくまでもパフォーマンス向上を重視する場合は検討することも視野に入れてみてはいかがでしょうか。

以下のようにsaveを実行することでバリデーションを一時的に無効にすることが可能です。

product.save(validate: false)

過度なパフォーマンスチューンングはNG

以上のようなパフォーマンスチューンングを施すことでRailsアプリでも爆速な処理を実装することができます。

パフォーマンスチューニングと可読性はトレードオフの関係にあります。

大々的なパフォーマンスチューニングを実施する場合、個々のロジック単位ではなく、機能全体で最適化を図ります。 そのため、どうしてもプログラムとして不自然なコードが生まれてしまい可読性が悪くなります。

可読性と天秤にかけた上で、場合によっては過度なパフォーマンスチューンングは避けるようにすることも必要です。

また、適宜コメントアウト等で処理の意図を補足するなどして可読性の低下を抑えるなどの心掛けが大事になります。

Rails7でReactを使う場合はimportmapはオススメできない

f:id:furu07yu:20220401124238p:plain Rails7からJavascriptバンドラーとしてNode.jsが非依存のimportmapが標準機能に採用されました。

早速、importmapを使ってReactを使用した開発を試みてみましたが、結論としてはimportmapは使用しないことになりました。

Reactの場合はimportmapをオススメできない

Reactを使用する場合、importmapはオススメできません。理由はJSXが使用できないためです。

JSXを使用するにはJSXで書かれたコードを素のJavascriptにトランスパイルする必要があります。しかしimportmapを使用しているとトランスパイルができません。

JSXが使用できない代わりに、htmというトランスパイルの必要がないHTMLマークアップライブラリがありますが、以下の理由によりJSXの代わりとするには厳しいと判断しました。

  • Typescriptに対応していない
  • eslintに対応していない

htmはトランスパイルがどうしても使用できない環境制約がある場合や、eslintなどを使用しなくても問題のない小規模な開発の場合に適していると感じました。

また、今回の開発ではmuiというReact専用のスタイルコンポーネントを使用したいと考えています。muiもJSXで実装されていますので、使用するためにはトランスパイルが必要になりますので、importmapの使用は見送りました。

esbuildを使用するのがオススメ?

importmapを使用しない場合、他にはwebpack、rollup、esbuildという選択肢があります。

今回はビルド時間がwebpackの100倍速いと言われているesbuildを使用することにしました。 esbuildはホットリロード(HRM)の機能が備わっていないという点が少しネックですが、ひとまずHRMは使用せずに開発を進めてみようかと思います。

開発する上で不都合などあるかもしれませんが、使用感などのレポートはまた別の記事にまとめようと思います。

それでは。

M1 Macbook Proにrbenv経由でRubyをインストールしようとしたらエラーになってハマった

f:id:furu07yu:20220324164425p:plain M1 Max Macbook Proにrbenv経由でRubyをインストールしようとしたところ、エラーが発生してインストールできませんでした。

この原因調査と対応にほぼ丸一日時間を費やしてしまったので、戒めも込めて記事にすることにしました。 誰かのお役に立てれば幸いです。

readlineでエラーが発生している

rbenv経由でRuby 3.1.0をインストールしようとしたら以下のエラーで失敗になってしまいました。

Downloading ruby-3.1.0.tar.gz...
-> https://cache.ruby-lang.org/pub/ruby/3.1/ruby-3.1.0.tar.gz
Installing ruby-3.1.0...
ruby-build: using readline from homebrew

BUILD FAILED (macOS 12.2.1 using ruby-build 20220218)

Inspect or clean up the working tree at /var/folders/t3/xx1z7jpj0zbfj_rwd9jh8bm00000gn/T/ruby-build.20220323144141.55529.x88PvV
Results logged to /var/folders/t3/xx1z7jpj0zbfj_rwd9jh8bm00000gn/T/ruby-build.20220323144141.55529.log

Last 10 log lines:
compiling ossl_x509ext.c
compiling ossl_x509name.c
compiling ossl_x509req.c
compiling ossl_x509revoked.c
compiling ossl_x509store.c
linking shared-object psych.bundle
linking shared-object date_core.bundle
installing default openssl libraries
linking shared-object openssl.bundle
make: *** [build-ext] Error 2

Results logged to /var/folders/t3/xx1z7jpj0zbfj_rwd9jh8bm00000gn/T/ruby-build.20220323144141.55529.log と記載があるように、まずはログを確認してみます。

compiling readline.c
compiling ossl_cipher.c
linking shared-object objspace.bundle
compiling psych_emitter.c
readline.c:1903:37: error: use of undeclared identifier 'username_completion_function'; did you mean 'rl_username_completion_function'?
                                    rl_username_completion_function);
                                    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                                    rl_username_completion_function
readline.c:79:42: note: expanded from macro 'rl_username_completion_function'
# define rl_username_completion_function username_completion_function
                                         ^
/usr/local/opt/readline/include/readline/readline.h:485:14: note: 'rl_username_completion_function' declared here
extern char *rl_username_completion_function PARAMS((const char *, int));
             ^
compiling psych_parser.c
1 error generated.
make[2]: *** [readline.o] Error 1
make[1]: *** [ext/readline/all] Error 2
make[1]: *** Waiting for unfinished jobs....

ログにはmake[2]: *** [readline.o] Error 1と出力されていますので、readlineでエラーが発生していることが確認できます。

make[1]: *** [ext/readline/all] Error 2rl_username_completion_functionなどの文言も気になります。

使用されているreadlineに問題がありそう

  • m1 mac rbenv install
  • BUILD FAILED (macOS 12.2.1 using ruby-build 20220218)
  • rbenv readline m1 mac
  • "rl_username_completion_function"

等の文言で検索してヒットした記事を参考にしたところ、以下のようなコマンドでインストールするのが有効だと分かりました。

RUBY_CONFIGURE_OPTS=--with-readline-dir="$(brew --prefix readline)"
rbenv install 3.1.0

このコマンドではhomebrew経由でインストールされているreadlineを使用してrubyをインストールするというオプションを指定しています。

今回のようなエラーの場合、ほとんどのケースではこのコマンドを実行することで無事にインストールできるかと思います。

しかしながら私の環境では、このインストール方法を用いても依然として同様のエラーが発生してしましました。

homebrew自体に問題がありそう

エラーログをよく確認してみるとruby-build: using readline from homebrewと出力されていますので、すでにhomebrew経由でreadlineを使用していたということが確認できました。

ということはhomebrewでインストールされているreadline自体に問題がありそうだと考えました。

brew update readlineを実行してみても特に変化はありません。

改めて記事を検索していたところ、この記事にヒントが有りました。

Building Ruby 3 on Mac M1 ARM — brandur.org

readlineの向き先としてhomebrewにインストールされているモジュールを使用するオプションを指定するのですが、Rosetta版のとARM版とでhomebrewのインストール先が違うという点でした。

確認してみると、私のmac上にはRosetta版がインストールされていました。

homebrewをアンインストールして、改めてARM版のhomebrewをインストールしたところ、Rubyのインストールが正常に行われました。

結論

私の場合、ARM版のhomebrewがインストールされておらず、コンパイルに失敗するというのが原因でした。

思い返してみるとintel Macから移行アシスタントを使用してM1 Macの環境構築をしていました。その際、あまり意識せずにRossetaの使用確認を許可したのだと思われます。

移行後はDocker環境で動かすことがほとんどでしたので、homebrewを使用する機会が今の今まで訪れず、ようやく問題が露呈されたということになります。

ひとまず一件落着です。

Herokuで可用性の高いサービスを運用するために入れておきたいアドオン3選 | Rails

f:id:furu07yu:20220316185416p:plain

弊社では多くのWebサービスをHeroku上で稼働させています。

初めてローンチした当初は数々のトラブルが発生して可用性が不安定な時期がありましたが、現在は可用性の高いサービス運用が実現できております。

それが実現できているのはアドオンのおかげと言っても過言ではありません。

今まで様々なアドオンを試してきましたが、本当に役に立ったのは後述する3つのアドオンに限られます。

今回はこの3つのアドオンを紹介したいと思います。

Papertrail

Papertrail - Add-ons - Heroku Elements

Papertrailはログを管理するためのアドオンです。

Herokuアプリ単体ではログは保存されません (正確には最新の1500行が1週間保存されます。それを超えるとログを閲覧することができなくなります)

そのため、ログを保存させるためのアドオンを別途使用する必要があります。

Papertrailを使用することで大量のログを一定期間保存することができます。また、保存したログを高度な条件で検索したり、特定の文言のログが出力された際に外部サービスに通知することができます。

Papertrailの主な機能と特徴

  • 無料から使用できる(1日のログのサイズ、保存期間などによって料金プランが定められている)
  • ログをリアルタイムに閲覧できる
  • 詳細な条件を指定してログを検索できる
  • アラート機能により特定の文言が含まれるログを外部サービス(メールやSlack等)に連携できる

Scout APM

Scout APM - Add-ons - Heroku Elements

ScoutAPMはアプリケーションパフォーマンス管理ツール(APM)です。

HerokuにもMetricsという機能があります。サービス全体での大まかなパフォーマンス監視は可能ですが、それはあくまでもおまけ程度と考えたほうが良いです。

ScoutAPMを使用することで、N+1クエリやメモリブロートの原因、パフォーマンスの異常を詳細に調査することができます。

エンドポイントやリクエスト単位でトランザクションを追跡することができるので、ボトルネックとなっている箇所を詳細なレベルで把握することが可能です。

PapertrailでR14やH12等の文言でアラートを検知して、ScoutAPMで調査するような改善を繰り返すのがオススメです。そうすることでアプリの信頼性が向上して顧客にストレスなく価値を提供し続けることができます。

ローンチした最初のうちは有料プランで使用して、ある程度ボトルネックを洗い出し終えて、アプリが安定稼働してきたら無料プランに切り替えるのが良いのではないかと思います。

ScoutAPMの主な機能と特徴

  • 無料から使用できる(トレースできる期間などによって料金プランが定められている)
  • エンドポイントやリクエスト単位でトランザクションを追跡することができる(競合サービスのNewRelicでは出来ない)
  • レスポンスタイム、メモリブロート、SQLスロークエリなどの切り口からボトルネックを調査できる
  • Githubと連携することでコード単位で追跡することができる

Pingdom

Pingdom - Add-ons - Heroku Elements

Pingdomはアプリの死活監視を行うサービスです。

Herokuアプリ自体がダウンしまった場合、それを検知させる外部のサービスが必要です。

Pingdomを使用することで、世界中にある100以上の拠点からアプリを監視することができます。 ヘルスチェック(死活監視)やパフォーマンステストはもちろん、トランザクションテスト(E2E)を実行する機能もサポートされているので、これ一台で様々な監視を行うことが出来ます。

特にE2Eテストはオススメです。E2Eテストというとハードルが高いイメージがありますが、PingdomのTransactionsという機能を使用することで直感的なUIで簡単にシナリオテストを作成することが可能です。

Pingdomの主な機能と特徴

  • 有料プランのみ対応
  • Real User Monitoringでユーザーの属性などをリアルタイムに把握することが出来る(GoogleAnalyticsに似た機能)
  • Uptimeで複数のアプリに対して死活監視することが出来る。チェック頻度や通知方法など細かな設定が可能
  • Page Speedでアプリのパフォーマンステストをすることができる
  • Transactionsで簡単なE2Eテストを作成して定期実行することが出来る。「ログインして特定のページで任意の操作を行う」などのシナリオが簡単に作成可能