やる気がストロングZERO

やる気のストロングスタイル

SpringBoot @Retryableでリトライがかからない事象にハマった

SpringBoot @Retryableでリトライがかからない事象にハマって時間を消費したので共有

結論

@Retryableアノテーションを付けていても、内部呼び出しだとリトライされない。

以下のような構成で、functionAを外部から叩かれた時、functionBはリトライがかかる想定だったがリトライされなかった。

public void functionA() {
    this.functionB();
}

@Retryable
private void functionB() {
    throw new Exception("例外発生")
}

リトライしたければ外から叩かれるメソッドにアノテーションを付ける必要がある。

以下のコードだと想定通りにリトライされる。

@Retryable
public void functionA() {
    this.functionB();
}

private void functionB() {
    throw new Exception("例外発生")
}

理由

SpringではDIでインスタンス生成を管理できるが、DIで生成されるインスタンスはSpringの機能で一枚ラップされている。
このラッパーによって、アスペクト思考的な機能が提供されている。
@Retryableもその一つで、ラッパーを通してメソッドが呼び出された時、例外をラッパー側でチェックしていて、必要があれば再度実行してくれる仕組みになっていると思われる。

なので、インスタンス内部間のメソッド呼び出しではラッパーが例外を検知できないので、リトライが効かない、という状況になっていたのだと思われる。

アノテーションって付けたら動作すると思ってしまうけど、うまく行かない時のデバッグに時間かかりがち。

JavaSE、JavaEE、JakartaEEあたりが何を意味しているのかわからんかったが少しわかった気がするのでメモ

JavaSE、JavaEE、JakartaEEあたりが何を意味しているのか長いことわからんかったが、以下の書籍を読んでやっと少しわかったのでメモする。 ※間違った部分あったらすいません。

参考書籍:

最初に:JavaEEとJakartaEE

権利を保持する団体が変わった際に、Javaの標章が使えなくなったため名称がかわった。
JavaEEとJakartaEEは、指しているものは同じと言って良いと思う。
あるバージョン以降はJacartaEEとして提供されることになる。

以降、JavaEEと書いている部分はJakartaEEと読み替えてもOK

JavaSE

Java言語の標準的な機能を定義している仕様。 いつも書いてるような以下コードとかは「このように書ける」っていうのがJavaSEに定義されている。

var str = new Object();

JDKは色々種類があるけど(OracleOpenJDKとかAmazon Correttoとか)、これらはJavaSEの仕様に沿って実装されている。

JavaEE

JavaSEに追加する拡張仕様。 アプリケーションサーバーで稼働するアプリケーションでよく使われる機能(DIとかWebアクセスのルーティングとか)のIFが定義されている。 JavaEEが実装されているアプリケーションサーバーを利用すると、これらの機能を自分で実装する必要がなくなる。(JavaEEコンテナにはGlassFishとかApache TomEEとかがある)

JavaEEの構成内容として以下のようなものがある。(書籍から引用。バージョンによって異なるかもしれない)

Java Servlet
JavaServer Pages(JSP
Standard Tag Library for JavaServer Pages(JSTL
Expression Language(EL)
Java API for RESTful Web Services(JAX-RS
JavaServer FacesJSF
Java API for WebSocket
Contexts and Dependency Injection for JavaCDI
Dependency Injection for Java
Common Annotations for the Java Platform
Java Persistence(JPA
Java Transaction APIJTA
Enterprise JavaBeansEJB Lite)
Bean Validation
Java API for JSON Processing

これら全てを実装しているアプリケーションサーバーはJavaEEコンテナを名乗ることができる。(JavaEEコンテナについては下記します)

ちなみにDependency InjectionとかはSpringFrameworkが先行実装していたものを仕様としてJavaEEが取り込んだ形らしい。(全く同様の仕様なのかどうかは知らない)
だから、SpringFrameworkを使う場合はアプリケーションサーバーがDependency Injection for Javaを実装していなくても同様の機能を使うことができる。(通常の外部ライブラリを利用する感じ)

JavaEEコンテナ

Javaのサーバーアプリケーションを実行する実行環境。アプリケーションサーバーみたいなやつ。(RubyとかRailsでいうとPumaとかUnicornとかにあたる)
JavaEEコンテナをサーバーで起動して、それが僕らが書いたJavaコードを呼び出す感じ。

Servletコンテナ

JavaEEすべてではなく、JavaEEの一部であるサーブレットJSPあたりだけが実装されたものもある。
これはサーブレットコンテナ呼ばれる。
Tomcatとか、Jettyとかがこれにあたる。
SpringBootを使う場合、使われるアプリケーションサーバーはTomcatやJettyなのでServletコンテナである。
なのでDependency Injection for JavaJPAの機能は使えないが、SpringFramework自体がそれぞれ機能を提供(Spring Data JPAなど)しているため使える。

おわりに

Java周辺のもろもろってネーミングも独特でわかりやすい情報も得づらい気がしていて、概念の理解難しいんだがみんなどうやって理解しているんだろうか?
ある程度理解してから調べると確かに書いてあるんだけど、最初は全然理解できなかったんだよな。。

xargsの使い方メモ

xargsコマンド便利そうと思ってるけどいざ使いたい時にだいたい使い方忘れてて、調べて思い出すにもパッと思い出せなくて初動が重くなりがちなので自分用にまとめておく。

基本

*.txtのファイル郡の中から"検索文字列"が含まれる行の情報を取得したい

find . -name "*.txt" | xargs grep -n "検索文字列"

上記コマンドで実際に実行されるコマンドは

grep -n "検索文字列" file1 fine2 file3,,,

一度に渡す引数数を調整する

上記ではgrep一回にすべてのfileが引数として指定されていた。
これを特定の回数に制限したいとき(例えば1コマンドにつき10ファイルまでとか)は以下のようにする

find . -name "*.txt" | xargs -L 10 grep -n "検索文字列"

これで実際に実行されるのは

grep -n "検索文字列" file1 fine2 file3,,,file10
grep -n "検索文字列" file11 fine12 file13,,,file20
...

みたいな感じ

引数場所指定

引数を特定の位置に指定したいときは以下のように書く この場合渡される引数は1つずつになる。
grepではあまり意味ないが、cpコマンドとかmvで使うとよさそう

find . -name "*.txt" | xargs -i grep -n "検索文字列" {}

{}ではなく自分で指定した文字列を使いたい場合は以下

find . -name "*.txt" | xargs -IXXX grep -n "検索文字列" XXX

ドライラン

-pオプションで実行前に実行されるコマンドを予め確認できる

find . -name "*.txt" | xargs -p grep -n "検索文字列"

同時実行最大数

-Pオプションで同時実行最大数を指定できる

find . -name "*.txt" | xargs -P 5 -L 10 grep -n "検索文字列"

上記で、最大5つのgrepプロセスが同時起動して実行される認識(実際にプロセス状況を見て確認したわけじゃないのでたぶん。)

Unixという考え方と読んで思い出したことなど

Unixという考え方を読んだ。

電子書籍なかったので久々の紙の書籍で読んだ)

自分もプログラミングに接してからUnix(というかLinuxUnixLinuxの厳密な違いとか認識できてないので以降自分の体験について書くときはLinuxと書きます。)にも触れて、その良さについて漠然と認識してたけど読んで「そうだよな、Unixはいいよなぁ」と改めて思った。

Unixの考え方のいい部分はプログラミングする際にも適応できるところ多いと思っていて、読みながら以下のことを考えたりしていた。

定理2:一つのプログラムには一つのことをうまくやらせる

一つのことをうまくやるっていうのは依存が小さいってことだと思う。
依存がないと応用が効きやすい、身動きがしやすい。
Unixのコマンド類が様々な組み合わせで様々な用途に使えるのはこのおかげである。
僕らが普段組むプログラムもこのように小さなプログラムで用意しておき、それの組み合わせで要求仕様動作をするように作っておけば、ちょっとした仕様変更や追加にはすぐ対応(グルーコードの追加変更だけで済むような)できる柔軟な構造にできる。

今までなんども複数のことをやるプログラムに触れてきた。
それは例えば、DBからユーザーデータを取得し、加工し、テーブルに保存するという3つの事を行う一つの大きなプログラム。

その要求仕様をやるには全く問題無いんだが、すこしだけ違うことをしようと思うと途端にできない。

  • 検証用に用意したユーザーダミーデータを加工してみて結果を見たいが、DBからしか受け取ってくれないので一度DBにデータを入れなければならない。
  • 加工結果は標準出力に出してくれれば十分なのに、DBに入れられてしまう。

こういうのはテストコードを用意しようとしたときに「いろいろやりすぎていてテストが書けない。。」ってなる。

定理9:すべてのプログラムをフィルタにする

自分がプログラミングに触れ始めてどこかの時点でデータフローダイアグラム(DFD)を知った時、この考え方が身についたと思う。
プログラムとは、データストア(データが保持されている場所)とデータを加工する処理の存在で構成されるデータの流れである。

※以下のイメージ
データストア -> 加工処理 -> データストア -> 加工処理 -> データストア

このように見ると、加工処理とはフィルタに見える。

この考え方は普段の生活や仕事でも活きる考え方になった。

たとえば仕事で判断に悩んだとする。
上記プログラムのデータの流れに照らし合わせてみると、自分は判断する処理(フィルタ)であるが、入力されているデータが不足しているので判断できない状態に陥っていると考えられる。
DFDで考えるとその情報はどこから提供されるべきものかイメージできる。

不足データを持っている人や場所(データストア)はどこかを考える。
そのデータを持っている人や場所にそれを求めれば得られる場合もあるし、その他もろもろの事情で得られない場合もある。
得られない場合はそれ以上考えても答えがでないので、時間を使うだけ無駄でエイヤで決めるしか無い状況だと素早く認識することができる。

効率より移植性

以下内容になるほどど思った。

  • 効率はマシンの性能アップが勝手に実現してくれる。
  • マシンの性能アップの恩恵が受けられるように、移植性を優先しておくとよい。

だから、移植性を落とすようなチューニングには注意すべし。

WindowsよりもUnix(Linux)を好む理由

これはこの本に書かれていた内容ではないし、Unixどうこうじゃなく、CUIGUIの差な気もするが本を読みながら思い出したので書いてみる

Windowsは基本GUI操作で、CUIから操作されるための口が用意されていない場合が多い(ように思った。詳しくないのでなんか方法あるのかもしれないが)
たとえば、なにかしらの設定を変えたい場合、Linuxだとたいてい特定のファイルの内容を編集するだけですむ。
それを人に共有したい場合はこの設定ファイルを共有するだけで済む。

Windowsの場合は、設定できる画面を探して、マウスでポチポチ設定を変更する。
画面は不安定で、Windowsのバージョンによってはその設定が配置されている画面が変わったり、設定値の名称が変わっていたりする。
以前設定したのでわかっていると思っても、数年経ってもう一度設定しようとするとその画面がなかったりする。 ググっても古い画面の情報しかなく、新しいバージョンでの設定方法がなかなかわからなかったりする。
なぜかこういうのは公式情報でも探し出せない。。(あと、Windowsの設定まわりの情報はWeb上でかなり汚染されていてまともな情報になかなかたどり着けない)

これを人に共有したい場合は、操作手順を文章とスクショで用意して、相手の「〇〇という項目がないです」という質問に対応しないといけない。 手間がすごい。

文字コードまわりを知ってる範囲でまとめた

文字コードまわりって普段あんまり気にしなくても問題ないけど、たまにこれ絡みでの問題があったときに構造を理解してないとどう手をつけていいかわからなかったりするので、自分が理解している範囲で書いてみる。

参考)

文字が表示されるまでの流れ

コンピュータが記録したり扱ったりできるのはバイナリ(0101みたいな)である。
例えば「あ」という文字をコンピュータで扱うにはバイナリとして扱う必要がある。
なので、「あ」 => 「00000001」, 「い」 => 「00000010」のように、文字とバイナリの対応表を前もって用意しておく。
そうすると、00000001があったときには「あ」と表示すればいいし、00000010があったときには「い」として表示すればいい。

上記したルールは今適当に割り振っただけなので一般的には使えないが、一般的に浸透しているルールがあって、それがShift-jisとかUTF8とかである。

Shift-jisだと 「あ」は「10000010 10100000」、「い」は「10000010 10100010」だし、
UTF8では「あ」は「11100011 10000001 10000010」、「い」は「11100011 10000001 10000100」となる。
Ubuntuだと基本文字コードがUTF8なのでターミナルで確認できる。

$ echo あ | od -tx1
0000000 e3 81 82 0a
0000004

16進数で表示されているが、それぞれ

e3 => 11100011
81 => 10000001
82 => 10000010

で「あ」の文字コードになっている。(最後の0aは改行コードである)

コンピュータがテキストファイルを読み込む時、どの文字エンコードを使うかを予め指定しておく。(テキストエディタでUTF8とか指定されているのはこの部分)
コンピュータは読み込むデータの中に 「11100011 10000001 10000010」というデータをみつける。
コンピュータはこれをUTF8の対応表にあてはめて、これは「あ」だと認識する。
※ 実際はコンピュータが認識するというより、UTF8の対応表に当てはめて、フォントデータの中から対応する文字の画像を表示しているだけだと思う。

ASCII

ちなみに英語でよく使われるアルファベッドだけで構成されているテキストはASCIIという文字エンコードが使われている。 Shift-jisもUTF8もこのアルファベッド部分はASCIIと全く同じ文字コードになっていて互換性がある。
だから、ASCIIに含まれる文字だけで構成されているtextはASCII、UTF8、Shift-jisのどれを使って文字エンコーディングしても問題ない。

文字化けの仕組み

文字が意味不明な漢字の羅列になってしまって「文字化けした」ってなる状態がある。なにが起こっているのか?
これはバイナリの文字データ(例えばUTF8の「あ(11100011 10000001 10000010)」)を誤った文字エンコード(例えばShift-jis)の対応表を使って文字に対応づけた時に、「あ」ではなく別の文字が対応付けられてしまうことから起こる。

エディタの文字エンコード判定(詳しくない。聞いた程度の知識。間違ってるかも)

上記の文字化けは、入力時に指定されている文字エンコードと、出力時に指定されている文字エンコードが異なる場合に起こるが、最近はエディタが自動的に文字エンコードを判定してくれて表示されるのであまり文字化けに遭遇することもなくなった。

ではエディタはどうやって文字エンコードを判定しているのか?

エディタで開くようなシンプルなtextファイルの場合は基本的に文字エンコードの情報を持っていないので「わからない」のだが、バイト列の特徴からあたりをつけて判断したりしているらしい。 なので、あまりに入力している文字数が少なく、判断材料が少ないと誤った文字エンコード判定が行われる場合もあるっぽい。

(一部のエンコードには先頭に特徴的なバイト列があるので、それが判断条件になっていたりもするっぽい。)

ユニコードについて

昔は各言語ごとに文字エンコードが存在した。
日本語はShift-jisはEUC-JPなど。
韓国には韓国の文字エンコードが、アラビアにはアラビアの文字エンコードがあった。
Shift-jisにはアラビアの文字コードは入ってない。
アラビア語で書かれた文字を表示するにはアラビア語の文字エンコードを指定する必要がある。

じゃあ、日本語とアラビア語の両方が含まれた文字を表示させるにはどうすればいいのか?

こんな感じ「こんにちは!مرحبا!」 (ここでは実現できている)

詳しくはうろ覚えだが、言語が切り替わる位置で特定のバイト列(言語切替用)を挿入することで、「ここからはアラビア語の文字エンコード」という感じで実現していたっぽい。

こういうのもなかなか大変だということ(なのかどうかよくわからんが)でユニコードが誕生した。
ユニコードは全世界の文字を一つの文字エンコードに含んでしまおう、というようなやつ。
日本語も韓国語もアラビア語もここに入っている。
だからユニコードの文字エンコードの1つであるUTF8で書かれているこの文章では「こんにちは!مرحبا!」は特に切り替えバイトなどなくとも単純に文字エンコードの対応表で変換する事が可能になっている。
ユニコードには絵文字も入ってるので、昔あった「docomoとjphoneでは絵文字のやり取りができない(昔は絵文字は各自独自でエンコード領域を拡張して使っていたため互換性がなかった)」というような状況は今は起きていない。今はPCでもスマホでも共通の絵文字が使えるようになっている。

ユニコードとUTF8, UTF16とか

ユニコードは上記した通り「全世界の文字を一つのコード表にのせてしまおう」という表になっている。
ここに掲載された各文字を「どのようなバイト列に当てはめるか」はまた別の話になる。(ややこしいが)
「どのようなバイト列に当てはめるか」の部分が文字エンコードで、UTF8やUTF16やUTF32(他にもあるのかも?)になる。

たとえば、ユニコードに掲載された「あ」をUTF8を使ってバイト列にすると「11100011 10000001 10000010」になるし、
UTF16を使ってバイト列にすると「00110000 01000010」になる。

ちなみにUTF16の「あ」のバイト列はユニコードのコードポイントと同じである。
コードポイントとはユニコードに掲載された文字に割り振られるIDみたいはやつで、バイト列とは無関係だがこういう感じで同じになっているものもある。(UTF32だと全く同じになるらしい。)
バイト列変換ルールはサロゲートペアとかあってややこしくて詳細には理解できてない。
UTF8とかは、文字によってバイト列が2バイトだったり3バイトだったりするので、このあたりの事情で「文字数を数える」という処理がなかなか難しい場合がある事を気に留めておくといい。(単純に1文字 = 2バイトってやるとバグる場合がある。)

フォントについて

文字を表すバイト列を文字エンコードを使って何の文字を表すのかわかった。
次はディスプレイにこの文字を表示するだが、ここでフォントが必要になる。
フォントは文字の形を表す画像データで、「あ」のバイト列に対応するフォントデータ「あ」がまさに画面に表示される。

ユニコードが使われるようになって全世界の文字を扱う事ができるようになったが、これを表示できるかどうかは、その文字を表すフォントが利用できるかどうかによる。
先程アラビア語(مرحبا!)が表示できていたが、これはあなたのデバイス(PCかスマホか知らないが)にアラビア語のフォントが入っているから表示できている。(もしかしたらブラウザ表示時にweb上から必要なフォントをダウンロードする仕組みがあったりするかも)
今どきはデフォルトで全世界の言語のフォントが入っているっぽい。詳しくは知らない。

自分が知ってるのはそんな感じだが、参考にした以下の本をみるともっと泥臭い話とかあってとてもおもしろいのでものすごくおすすめです!

書いたコードがCPUで実行されるまでの流れ

書いたコードがどのようにCPUで実行されるかここ1年くらいでやっとイメージ出来るようになったので書いてみる。
ちなみにJVMとかインタプリタ言語がどうやって実行されているのかはまだ良くわかってない。

コードがコンパイルされてマシンコードになり、実行ファイルになる

参考) 低レイヤを知りたい人のためのCコンパイラ作成入門

コードはまずアセンブリに変換される。
アセンブリはほぼほぼCPU命令と1:1の人が読みやすい言語。
ほぼほぼCPU命令なので、CPUアーキテクチャ毎(x86_64, arm64など)に使えるアセンブリは異なる。
コンパイラは指定されたCPUアーキテクチャでプログラムコードをアセンブリに変換する。
その後、アセンブリからマシンコード(CPU命令そのもの)に変換され、(外部ライブラリコードを結合されたりもして)実行ファイルとして出力される。

実行ファイルにはマシンコードだけではなく、実行時に必要な情報(エントリーポイントや確保メモリサイズなど?)なども含まれたものになっている。

実行ファイルも色々形式があって、Windows用とLinux用で形式が異なったりする。(だからCPUアーキテクチャが同じでもWindowsの実行ファイルをLinuxで実行できない)
コンパイラは適切な実行ファイル形式で実行ファイルを作成する。(どの形式で実行ファイルを作成するか指定できたりするのかも)

OSから実行ファイルを実行し、プロセスになる

参考)

www.youtube.com

OSから実行ファイルを実行すると、マシンコードを含むファイル内容がメモリ上に読み込まれる。
OSはこの実行状態になったプログラムを「プロセス」として管理し、CPUを効率よく使うようにスケジューリングしながらプロセスをCPUに割り当てていく。

具体的には、プロセスはCPUに割り当てられて一定時間立つとCPUから外され次の割り当て機会を待つ。
その間、CPUには別のプロセスが割り当てられている。
シングルコアCPUの場合、このように正確にはある時間では1つのプロセスしか実行していないが、短い時間で切り替えながら複数のプロセスを処理するため、人間の体感的には複数のプロセスが同時に動いているように感じる。
マルチコアCPUの場合には複数のCPUがこれを同時に行うため、実際に複数のプロセスが同時に処理されている。

CPUがマシンコードを実行する

参考)

CPUはメモリ上にあるマシンコードを順次読み込み実行していくのみである。

CPU上にプログラムカウンタというレジスタレジスタはCPU上にある保存領域で容量は小さいがCPUからのアクセスが超早い。CPUのワーキング領域みたいな感じ)にメモリ上のマシンコードの実行したい部分のメモリアドレスが保持されている。

CPUはプログラムカウンタの指すメモリアドレスからマシンコードを読み込み、デコーダーでその命令を実行できるデジタル回路を有効化(という表現がいいのかよくわからないが、とにかく命令が正しく実行できる回路を利用できるように調整する)して、その回路を使って命令を実行する。

CPUにとっての「命令の実行」とは、対象のデジタル回路へ入力し、出力を得る事である。 例えば加算命令の場合、加算用のデジタル回路の入力へ1と2を入れると、出力から3が得られる。

そういう感じで、CPUは様々な命令に対応するデジタル回路を持っていて、マシンコードをデコーダーに通すことでどのデジタル回路を利用するかを調整し、適切なデジタル回路を使って結果を得て、また次のマシンコードをデコーダーに通し、、を繰り返している。

(ついでに)CPUの実行速度効率化

参考)

CPUの世界から見るとメモリの場所はかなり遠く、読み書きに時間がかかるので、CPUは利用するデータをできるだけ近くに配置する。
一番近く一番速いのがレジスタで、クロックサイクルでいう1サイクルで読み書き可能。

CPUとメモリの間にはキャシュが存在している(L1キャッシュ、L2キャッシュ, L3キャッシュなど)
これらはレジスタよりも遠いが、メモリよりもかなり近い。
なので、このキャッシュに収まるデータ量で収まるようなプログラムであれば実行速度が期待できるが、逆に収まらないとキャッシュがうまく使えず期待した実行速度がでないかもしれない。

CPUはできるだけ無駄な待ち時間が無いように動くように設計されている。
たとえば、ある命令が実行されている間、デコーダーが何もしないのはもったいないので、次の命令のデコードを同時に行う。
また、ある命令でメモリからのデータ取得を待っている間(メモリからの読み込みは時間がかかる)、何もしないのは勿体ないので次の命令を先に実行してしまっておく。
他にも色々ある。

分岐の無い真っすぐなプログラムの場合、上記のような工夫を使って最大限効率化されて実行される(GPUがまさにこのケースで最適化されているらしい)が、実際のプログラムには分岐処理などがあってなかなか効率化できないシーンがある。

分岐結果を予め予測し、空き時間の間に先行して分岐先の命令を実行しておく投機的実行も行われたりするが、分岐結果予測を外した場合にはそれらが無駄になってしまうなど、いろいろ面白い。

また、マルチコアで並列で実行されているゆえに効率化できないシーン(CPUキャッシュの同期が必要になる)などもある。

これらを理解しても、なかなかプログラムコードのレイヤーからそれらを制御することは難しいのでコーディングに活きるかはわからないが、面白かった。