• 作成:

awsパッケージは出来る子でした, 大容量のファイルのS3へのアップロードが出来なくなりました, yesodにpull requestを出しました

バケット名にドットを付けていたのが全て悪かった, awsパッケージは出来る子でした

AWSのS3にhaskellでアクセスするのに苦戦してます, flycheck-stackはもう不要になってました, optparse-applicativeがいい感じ - ncaqに書きましたが, aws :: Stackage Serverで上手くpre signed urlを生成できなかったので, s3-signer :: Stackage Serverを使ってみました.

そしてAmazon S3のバケット名にドットを使ってしまいhttps通信が出来ない環境を作ってしまった, S3のダウンロードファイル名を指定できるライブラリがなくて困っている - ncaqに書きましたが, Amazon S3のバケット名にドットを付け加えるとhttpsなuriが生成できないことがわかったので, 開発用のバケット名の区切りをドットからハイフンに書き換えました.

それで, s3-signerはresponse-content-dispositionに対応していないので, s3-signerを改造するか, amazonka-s3 :: Stackage Serverに移行してみるか迷っていました.

しかし, もしかしたらawsパッケージでURLを作成できないのはバケット名にドットを使っていたからでは? と思って書き換えてみたところ, Aws.awsUri関数で無事pre signed urlが作られました.

configurationでHTTPを指定しないと動かなかったのもバケット名にドットを使っていたからでした. ドットが無ければAws.defServiceConfigで動きました.

ドット付きバケット名でも問題ないようにs3-signerをforkすることも考えていました. しかし, awsパッケージの関数がバケット名にドットさえなければ動くので, 本番向けのバケットも変更して, 一時的にサービスを止めてsyncした方が良さそうですね.

大量にファイルを生成するテストを削除しました

これまで開発やテストにはローカルのディスクを使って, 本番のみgoofysを使ってディレクトリをマウントしてS3を使用するようにしていました. これからはgoofysは使わずにS3のSDKを使っていくので, テストにもS3を使わざるを得なくなりました.

そうなるとこれまでの大量のファイルを投稿して挙動を調べるテストが, 物凄い時間をかけるようになってしまいました.

これまではローカルで完結していたのをネットワーク越しのS3を使うようになったので当然ですね.

仕方がないのでテストに使うファイルのサイズを大幅に減らして, ファイルを大量投稿するテストは削除することにしました.

ファイル投稿制限のテストが出来なくなってしまいました, どうしましょうか… 開発時のみローカルディスクを使用するようにしておくのも手ではありますが, 本番環境を使わないテストなんて無意味な気がします.

テストにはテスト用のバケットとキーを.travis.ymlに記述します. テスト時に開発側で弄ってるS3ファイルが書き換えられたりしたらアレなので.

travisの暗号化環境キーを使えば新規アカウントは必要ないかなと思いましたが, 全権限を持つアカウントキーが流出したら大変なので, テスト用のバケットにアクセス出来るだけのアカウントを結局作ることにしました. クローズドソースだからこそ出来る楽というものもある.

AWSのポリシー名って変更できないのかな, わかりにくいから意味のある値に変更しようとしても出来ませんでした. まあいいやと思ってそのまま…

よく考えたらyesodのテスト時設定変数書き換え機能を使えばいいじゃんとなったので.travis.ymlに書き込むのはやめました.

大容量のファイルのS3へのアップロードが出来なくなりました

実装が出来たので実際の動きを確かめてみようと思ったのですが, ちょっと大きいファイルをアップロードしようとするとうまく行かない. サイズは100MBぐらいを超えるとうまく行かなくなります.

エラーログは出ない.

何回かファイルをアップロードしてみようと思ったらメモリを大量に食い始めてswapを食いつぶし始めました.

何も考えずにAWS SDKのAPIを叩くより, goofysを使った方が最適化されていということなのですか…?

たしかにgoofysを使っているときはfileMoveを使って後はファイルシステムに任せて, こちらの実装はhaskell側で全部をByteStringにして取り扱うようにしたわけですからね, ロジックが異なる.

S3にクライアントから直接アップロードしても良いのですが, サーバ側との一貫性を取る方法がまるでわからないのでそれは一旦取り止め.

conduitがわからない

ちょっと前に

-- | Bytestring 'fileSource'
fileSourceBytes :: MonadResource m => FileInfo -> m ByteString
fileSourceBytes fileInfo = fileSource fileInfo $$ foldC

を書いてFileInfoByteStringにしてしまってしまえばconduitの意味不明な関数群を理解する必要はなくなったと思いましたが効率から逃れられなくなってきました.

このようにS3.putObjectするときに何も考えずにByteStringにしたものをRequestBodyBSで包んで渡しているのが悪いのかと思ってlow levelにストリームを取り扱ってみようと思いましたがよくわからなくなってきました.

conduitの何がわかりにくいかってSource, Conduit, Sinkとか色々な型があるけどそれは結局typeなのでghciで動かしてみるとConduitMが帰ってきて意味がわからなくなるところですね… 脳内で型エイリアスを変換できない. 厳しい.

要はRequestBodyを効率的に作れば良いのですが, Network.HTTP.Client.ConduitrequestBodySourceChunkedSource IO ByteStringを求めているため, YesodのfileSourceが生成するMonadResource m => Source m ByteStringだと渡せない.

http-conduitはconduitを利用しているだけでconduit用の関数を用意しているわけではないのでこれを見てもあまり意味なさそうですね.

最適化の問題では?

Aws.defaultLog Aws.DebugしてみたりputStrLnデバッグしてみると, AWSにリクエストを飛ばしているわけではなく, fileSourceBytesを実行した段階で応答がなくなっていることがわかるので, やはり効率の問題っぽい.

効率…ByteString…待てよ, そう言えば前に haskellプログラムがメモリを食いまくって落ちていたのはghcに-O0を指定していたからだった - ncaq ということがありましたね…

yesod develが実行するのはstack build --fastになっていることを以前見たので, 最適化は前の時と同じく切られているはずだ… stack cleanした後に, stack exec -- yesod devel -e --ghc-options="-O2"を実行してみました. しかし, 前と同じく失敗してしまいました.

しかしやはり怪しい, --fast--ghc-option="-O2"より優先されているのかな? と思ったのでyesodweb/yesod: A RESTful Haskell web framework built on WAI.をcloneして, yesod-binのソースコードを改変して--fastを取り除いてインストールし直してみました.

そうすると状況に変化が現れて, awsへのputリクエストが発生したことがログで確認できました. しかしやはりネットワークエラーになってしまうので, 最適化が足りないのかと思い, 改変したソースでもう一度yesod devel -e --ghc-options="-O2"を実行してみました. しかしやはりダメ. ですがファイル自体はS3にアップロードされていました. は?

もう一度やってみると, クライアント側ではネットワークエラーが表示されるのに, サーバサイドではamazon s3へのputが完了するという意味不明な出来事が起きました.

応答が全く無いんですけど…

まあ, とりあえず, 最適化を有効にすればファイルアップロード自体は出来ることがわかったので, yesod develstack build--fastを付加するのを消すpull requestを出してみようと思います.

本当はconduitの関数を読み解いて効率の良い実装にした方がもっと良いのでしょうけど, とりあえずは最適化ビルドが可能なように元を改変したい. 最適化がないと, ちょっとしたByteString操作でもサイズが大きくなるとクラッシュしてしまうことがわかったので.

サーバは200 OKを返しているのにクライアントがネットワークエラーを出しているのは何故かわからない. 明日調べたいと思います. fetch apiの仕様なのかな?

yesodにpull requestを出しました

pull requestを出しました. 英語つらい.

remove the --fast flag from stack build executed by yesod devel by ncaq · Pull Request #1442 · yesodweb/yesod

google翻訳に頼ったわけですが, せっかくなのでここに原文を置いておきます.

原文

yesod develが実行するstack buildから--fastフラグを取り除く提案です.

私はyesodで, ファイルをS3へとアップロードする機能を持つアプリケーションを書いています. サイズは2GB程度まで対応しています.

ファイルはByteStringへとひも解かれます.

ここで, yesodアプリケーションをビルドする時に最適化が無効になっていると, アプリケーションは大量のメモリを使い, クラッシュしてしまいます. 100MB程度のファイルで問題が起きてしまいます.

つまり, yesod develstack buildする時に最適化をしてくれるようにすれば良いわけですが, stack build --fastがかかっていると, stack exec -- yesod devel -e --ghc-options="-O2"しても, --fastが優先されて, 最適化が有効になりません.

昔のyesodでは, devel時の最適化の無効化は, 個々のアプリケーションのcabalファイルで書かれていたため, 変更が容易でしたが, 今は私が変更する方法がありません.

なので, --fastフラグを削除するpull requestを開きます.

最適化の有無は, 昔のようにユーザが選択できた方が良いと思います.

少なくとも, 私のアプリケーションは今のyesod develでは動かなくて, 困っています.

10ヶ月前の変更なのにも関わらず, 今問題を報告することをお許しください.


ファイルをByteStringに紐解くのに使っているコードを添付しておきます.

以下のコードは, 最適化フラグがデフォルトだと動きますが, 最適化が無効(-O0)だと動かなくなります.

-- | Bytestring 'fileSource'
fileSourceBytes :: MonadResource m => FileInfo -> m ByteString
fileSourceBytes fileInfo = fileSource fileInfo $$ foldC

最適化を無効にすると動かなくなるようなコードを書くべきではないという方針ならば, それをお伝え下さい.

Data.ByteString.Char8.packを誤ってマルチバイトのStringに使っていた

StringをByteStringに変換したい時Data.ByteString.Char8.packを使っていたのだけれど, これはマルチバイトのString文字列を壊してしまうのでよろしくない.

誤って使いまくっていて, 日本語ファイルを文字化けさせてしまっていた…

じゃあ何を使うかというとData.ByteString.UTF8.fromStringを使う.

unicode - Using Haskell to output a UTF-8-encoded ByteString - Stack Overflow

utf8-stringは前に使っていたはずなのだけれど, すっかり忘れてしまっていた.

haskell歴はそこそこあるはずだけど, ByteStringText関連のものはよく忘却してしまう, どうにかならないだろうか.