きり丸の技術日記

技術検証したり、資格等をここに残していきます。

Pythonのエラーチェイン(例外を元に例外を投げる)には3種類あるがあまり気にしなくていい

Pythonでフレームワークから発生した例外を元に、適切に自作した例外に変換する方法に複数あることを知ったので、それを素振りしました。

なお、結果だけ先にお伝えするとraiseするだけでも9割問題ありません。

環境

  • Python
    • 3.11.6

確認方法

Pythonでは単純にエラーをraiseするだけでなく、from efrom Noneという構文を付けてraiseすることもできます。

try:
    try:
        1 / 0
    except ZeroDivisionError as e:
        raise ValueError("ERROR") # 1つ目
        raise ValueError("ERROR") from e # 2つ目
        raise ValueError("ERROR") from None # 3つ目
except Exception as e:
    print(traceback.format_exc())

具体的にtraceback.formt_exc()で得られるstacktraceは次のとおりです。

# 1つ目のraise
tests/unit/test_raise_error.py Traceback (most recent call last):
  File "/tests/unit/test_raise_error.py", line 9, in test_01
    1 / 0
    ~~^~~
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/tests/unit/test_raise_error.py", line 11, in test_01
    raise ValueError("ERROR")
ValueError: ERROR
# 2つ目のraise
Traceback (most recent call last):
  File "/tests/unit/test_raise_error.py", line 9, in test_01
    1 / 0
    ~~^~~
ZeroDivisionError: division by zero

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/tests/unit/test_raise_error.py", line 11, in test_01
    raise ValueError("ERROR") from e
ValueError: ERROR
# 3つ目のraise
Traceback (most recent call last):
  File "/tests/unit/test_raise_error.py", line 11, in test_01
    raise ValueError("ERROR") from None
ValueError: ERROR

差分としては次のとおりです。

  1. 1つ目のやり方
  2. エラーメッセージにDuring handling of the above exception, another exception occurrが出力される
    1. 例外処理中に例外が発生しました
  3. 2つ目のやり方
  4. エラーメッセージにThe above exception was the direct cause of the following exceptionが出力される
    1. 上記の例外が直接の原因で次の例外が発生しました
  5. 3つ目のやり方
  6. 元の例外を握りつぶす

結論

正確に例外を表現するならraise Error from eを使用してください。

ライブラリやフレームワークを作成したり、テスト用の便利なモジュールを作っているときに、例外を伝播させたくないときにはraise Error from Noneを使用してください。

Pythonとしては、例外は意図的に握りつぶさないので単純にraiseするだけでも9割のケースで問題ないでしょう。

ソースコード

終わりに

個人的に、raise from eにはもう少し情報が増えることを期待していました。もちろん、正確に表現するならraise from eがあったほうが良いです。しかし、stacktraceを読もうとしているときは不具合の原因をいち早く確認したいので、まずは発生箇所を特定することを最優先してしまい、結果としてDuring handling of the above exception, another exception occurrというメッセージは今まで目に入ってこなかったです。

まぁ、知識として知ったうえで単純にraiseすることは今後無くなるでしょうが、かといって無理やり既存のコードにraise from eを付与するほどではないので、結論としてはあまり気にしなくていいです。

Gitにchmodした結果を含めない、そして復帰させた時の手順(core.fileMode)

WSLで開発中にroot権限で作成されたファイルがあり、IntelliJ IDEAで更新できなかったファイルがありました。そのためディレクトリごとchownchmodを行ったのですが、Gitに変更箇所があると検知されてしまい、気づかずにコミットしてしまいました。

今回の記事では、chmodを行っても検知されないようにする設定方法と誤ってコミットしてしまったときの復帰手順の操作を記載します。復帰手順に関しては、もっといいやり方がありそうですので、もし同じ事象にハマったら教えてください。

環境

  • Git
    • 2.34.1
  • IntelliJ IDEA
    • 2024.1 EAP

設定方法

次のコマンドを実行すれば、chmodしても結果がコミットされなくなります。

git config --global core.fileMode false

復帰方法

  1. 一時ブランチを切る
  2. 目的のブランチを戻したいコミットまで戻す

IntelliJ IDEAのGit(Alt + 9)で操作しました。目的のコミットを選択しながら右クリックでReset Current Branch to Hereを選択してください。次に出現するウィンドウはHARDを選択してください。

  1. 一時ブランチで目的のコミットから目的のファイルだけget From Revisionで戻す

終わりに

普段なら戻したいファイルをRevert Selected Changesで選択していましたが、chmodの変更は戻せませんでした。復帰方法をまとめてくれる記事は今までなかったと思うので、これを機に同じ失敗した人に役立てばいいと思います。

なお、Chat GPTからは目的の答えは聞き出せませんでした。

類似記事

RailsのRSpecテストを並列化して30分から12分に短縮した(parallel_testsと性能アップ)

CI上のRailsのRSpecテストが30分もかかっていてストレスフルだったので、テストを並列化する等で30分かかっていた時間を12分まで削減しました。

そのときのメモ。

環境

  • AWS CodeBuild
  • Ruby
    • 2.7.2
  • Rails
    • 6.0.3.6
  • Parallel_tests
    • 4.4.0
  • MySQL
    • 8

前提

docker composeでテストは閉じている。そのため、RDS等のアクセスは発生していない。

対応

parallel_testsライブラリを入れる

並列化ライブラリのparallel_testsを導入します。

group :development, :test do
  gem 'parallel_tests'
end

テストのDB名を修正する

並列化したプロセスごとに独立したスキーマを割り当てないとデッドロックが頻発するのでプロセスごとにスキーマを作成します。

parallel_testsライブラリが実行しているプロセスごとに環境変数TEST_ENV_NUMBERを用意してくれるので、suffixに環境変数を指定します。

test:
  database: sample<%= ENV.fetch('TEST_ENV_NUMBER', '') %>
  # sample
  # sample2
  # sample3
  # といった具合に、プロセス数に応じたスキーマ名が振り当てられます。
  # 1は割り振られないので、どうしても1と割り振りたい場合は自作rakeが必要です

テスト実行時のRakeを自作する

私の環境の場合docker composeを使用して閉じた環境でテストしていることもあり、商用環境テーブルは不要でした。そのため、parallel_testsのライブラリをそのまま使用するとマイグレーションチェックが走ってエラーが発生してしまったため、その過程を飛ばしています。

また、自作Rakeがdevelop testgroupに依存しているのもあり、商用でエラーになってしまうので、LoadErrorをスキップするようにしています。

# frozen_string_literal: true

# NOTE: テスト時にしか使用しないものなので、商用ビルドするとインポートエラーが発生してしまう
begin
  require 'parallel_tests/tasks'
rescue LoadError
  puts 'Unable to load parallel_tests/tasks, skipping'
end

namespace :my_parallel do
  # NOTE: 必要な個所のみ抜き出す
  # https://github.com/grosser/parallel_tests/blob/8129c4c6feb6db7f94de144b995ea2a42aae0af5/lib/parallel_tests/tasks.rb#L258C1-L266C8
  task :spec, [:count, :pattern, :options, :pass_through] do |_t, args|
    # 商用環境テーブルのマイグレーションはチェックしない
    # ParallelTests::Tasks.check_for_pending_migrations
    ParallelTests::Tasks.load_lib
    command = ParallelTests::Tasks.build_run_command("spec", args)

    abort unless system(*command) # allow to chain tasks e.g. rake parallel:spec parallel:features
  end
end

CIを修正する(buildspec-ci.yml)

AWS CodeBuildを使用しているので、次のようなymlを用意しました。

phases:
  pre_build:
    commands:
      # MySQL 8 だと_utf8mb4 というsuffixがついてしまうので、それを削除する
      - sed -i 's#_utf8mb4\\\\'\''_\\\\'\''#'\''_'\''#g' db/schema.rb
      # スキーマの削除・作成・db/schema.rbの反映
      - docker-compose run --rm api rails parallel:drop parallel:create parallel:load_schema
  build:
    commands:
      # 自作rakeでのテスト実行
      - docker-compose run --rm api rails my_parallel:spec

プロセス数は指定したイメージのCPU数に依存するので、もし指定したCPU数よりも少ないプロセスで動かしたい場合は環境変数で指定してください。ローカルPCで挙動確認すると、20並列で動いてしまって期待の速度が出なかったので制御していました。

env:
  variables:
    PARALLEL_TEST_PROCESSORS: 4

結果

もともと、30分の内訳としては次のとおりでした。

  • 起動準備
    • 2分
  • PRE_BUILD
    • 5分弱
  • BUILD
    • 23分

テストを並列化した結果、25分になり、内訳は次のとおりでした。

  • 起動準備
    • 2分
  • PRE_BUILD
    • 5分弱
  • BUILD
    • 18分

ここまではコンピューティングが3GBメモリ2vCPUでしたが、7GBメモリ4vCPUに変更した結果。16分になり、内訳は次のとおりでした。

  • 起動準備
    • 2分
  • PRE_BUILD
    • 5分強
  • BUILD
    • 9分

最後に15GBメモリ8vCPUまで性能を上げて、12分になり、内訳は次のとおりでした。

  • 起動準備
    • 1分40秒
  • PRE_BUILD
    • 5分強
  • BUILD
    • 5分弱

AWSが用意するコンピューティングとのバランスを考えたときに、並列化によるこれ以上のメリットはないと考えてここで終了しています。

ソースコード

なし

終わりに

イメージ作成したり、DBを用意したりするPRE_BUILDが測定前に想像していなかったほど重く、ここを改善しないとこれ以上の高速化は見込めませんでした。

ビルドをDocker Build Cloudに任せたり、DBを作ったイメージごとsaveして流用する等々の考えもありましたが…。複雑になる上、高速化に貢献するか微妙なラインだったので、検証はしていません。

EC2だけでなく、Lambdaをうまく組み合わせたら早いとかそういう話があればぜひ聞きたいです。

参考情報

https://www.publickey1.jp/blog/24/docker40docker_build_cloudappleaws_graviton.html