コマンドラインツールの例外実装, string-transformとOverloadedStringsの相性が悪い, Multipart Upload
コマンドラインツールの例外実装
テストできるようにモジュールをlibraryとexecutablesに分けました.
baseのexceptionを使うよりsafe-exceptionsを使うほうが良いそうなのでimport
先だけ変えておきます.
現在非同期例外を使うことはないのであまり恩恵は無いかなあ…
と思いましたが,
try
にたくさん型注釈をつけていたところをtryAny
にすることが出来ておおこれは便利.
昨日書いたstring-transform: simple and easy haskell string transformをこのプログラムにも使うことにしました. 単純なモジュールですが, もっと広まって世の中のhaskellコードの可読性が上がって欲しい.
プログラムをDuplicateRecordFields
とOverloadedLabels
を前提に書き換えています.
しかし,
パターンマッチで取り出すことを前提にやっていると,
同じスコープで重複したラベルを扱う時,
結局はプレフィクスを付けてパターンマッチさせて重複させないということが必要なことがわかってきました.
コマンドラインオプションを取り扱う時Maybe
が出まくるのでのエラー処理をしてMaybe
を取り外す時に意味は同じなんだから同じ変数名を付けたくなります.
しかしhaskellでは変数名をシャドウするのは良くない習慣として取り扱われているのでダメです.
Maybe
付きのラベルにmプレフィクスを付けてしまうのが常道なんでしょうかね…
逆にMaybe
を取り外したものにjustプレフィクスを取り付けてしまった.
ローカル変数名なんて別にどうでもいいんですよ.
Prelude.read: no parse
を例外を引いて,
tryAny
で囲ってるのに何故例外が出てくる?
と思ったらやっぱり遅延評価が原因みたいですね.
haskell - Can't catch "Prelude.read: no parse" exception with Control.Exception.try - Stack Overflow
はじめて遅延評価由来のバグを引いた気がします.
そんなことはないか?
ここに「tryAnyDeep
を使えば良いですよ」と書き込んだら-1された…
悲しい…
deepseqのforce
を使って評価しようかと思いましたが,
safe-exceptionsに
tryAnyDeep :: (MonadCatch m, MonadIO m, NFData a) => m a -> m (Either SomeException a)
というまさに求めている関数がありました.
結局NFData
のインスタンスを要求するので,
deepseqに依存する必要があるんですけどね.
evaluate
の場合IO
になってしまって面倒な代わりにbaseで依存が済むようですね…
ラベルの重複は問題なくなったけど, 型コンストラクタの重複は問題になるのでやはりプレフィクスを付けざるを得ない.
ついに(やっと?)haskellでerror
を使っていた所を独自の例外に書き換えようとしていますが,
エラー表示に悩んでいます.
error
をそのまま使っていれば,
例外を最終的に処理するときに渡した文字列を表示してくれるのですが,
Exception
をthrow
すると,
Exception
の中にString
を入れてようが無視して表示されてしまいます.
ちゃんとエラーをユーザに表示するには以下の2つの方法があるでしょう.
- 上位層で例外をキャッチして例外に対応した文字列を表示する
- エラー表示をしてから例外を出力する
今回は後者を選ぶことにしました.
httpエラーの時にはレスポンスを表示するようにしているのですが,
これがフィールドが結構多いのでshow
そのままだと大変見にくい.
仕様には見やすくする必要があるとか入ってないのでやる必要は無いのですが,
流石にこれはどうかと思ったので何かpretty printをしておく必要がありそうですね.
cdepillabout/pretty-simple: pretty-printer for Haskell data types that have a Show instanceがちょうど良さそうですね.
pPrint
がstderr
に出力出来ませんが,
まあpShow
すれば良いだけですね.
pShow
だとカラーにならないかもと思いましたが,
なりました.
string-transformとOverloadedStringsの相性が悪いがどうにもならなかった
string-transformを実際に使っていったところ,
型推論が決定せずにAmbiguous type variable
になるケースがあることがわかってきました.
渡す関数がString
かText
かどっちを返すかわからないというケースは多分仕方がないです.
しかし,
OverloadedStrings
を有効にしている時に文字列リテラルを渡すとString
, ByteString
, Text
が候補に上がって決定できないというのは厳しすぎます.
本来OverloadedStrings
を有効にしていたら文字列リテラルから文字列を変換する必要は無いはずなのですが,
高度に型クラスを使用していると必要になる時があります.
もちろん,
その時は本来文字列リテラルに型注釈を付けるのが正解なのですが,
変換を指定された時に動くようにしておきたいという欲求もあります.
解決策を考えます.
ぱっと思いつく解決策はIsString a
向けのインスタンスを追加してみることです.
いや,
ダメですね,
IsString
はあくまでfromString :: String -> a
でString
から何かを生成するためのクラスなので,
これでインスタンスを構成することは出来ません.
数値型なんかは複数選択肢があってもInt
に定まったりするので,
何かデフォルトを指定するプラグマがあるんでしょうか.
参考にしようと思って見てみましたが,
GHC.Num
もGHC.Float
もGHC.Real
も実装のソースコードがstackageから見れない…
ExtendedDefaultRules
とかいうのがあるらしいのでこれかなあと思って見てみましたが,
default
の指定方法がわからない.
githubからghcのフルソースコードを持ってきました. これを読む. しかし, defaultが設定されているようには見えません…
あれ, もしかしてghciだとdefaultが定まるだけで, 本物のソースコードでdefaultを設定する方法は存在しない…?
Defaulting – Haskell Primeにhaskell 98の頃の提案が書かれていました.
同じようなことをやっている人が居ました. haskell - Why am I forced to specify a type here? - Stack Overflow
ExtendedDefaultRules
はghciだとデフォルト指定なんですね.
結局これを指定してお警告は消えないので,
ダメそう.
結局実害は殆どないようなので放置することにしました. 一部のところで関数に型注釈しなければいけないのはfromを省略してtoだけ指定するようにしている都合上仕方がない.
文字列リテラルから変換できないのは文字列リテラルに型注釈を付けるべきということですね.
Multipart Uploadでのアップロードを実装しました
補助ライブラリの開発に夢中になって, 本題を忘れかけてしまっていましたね. Multipart Uploadの実装を進めていきましょう.
とりあえず小さいサイズのファイルをこれまでのようにPutObject
するためにContent-Size
を取得して比較するなんてことをやっているのですが,
これバイトサイズなので気をつけないと32bitを超過してしまいますね.
HaskellならInt
になるのを警告してくれますし,
Int64
もInteger
もあるから大丈夫ですが.
オーバーフローしてしまうブラウザの実装がたくさんあるのも納得です.
32bit環境で動くブラウザで大規模数扱うの,
gmpがないとつらそう.
サンプルコードを見ようと思ってaws/MultipartUpload.hs at master · aristidb/awsを見てみましたが, チャンクサイズとかの調整が知りたかったのに全部コマンドライン引数に任せているのでほとんど参考にならない… 思わずamazonkaに変えようかと思って見てみましたがこちらはLensしている上サンプルコードが全く無かったのでやめました.
awsの中にもたくさん関数があって何を使えばいいのかさっぱりわからない,
とりあえずサンプルで使われていたmultipartUploadSink
を使ってみます.
multipart uploadというぐらいだから数回接続をするんだろうなあと思ってsimpleAws
ではなくpureAws
を使おうと思いましたが,
multipartUploadSink
は他のawsパッケージの関数とは違って自前で実行まで行うようですね.
適当にS3.multipartUploadSink
で書いていたら型チェックが通りました.
ソースを一部公開すると以下のような感じです.
withManager $ do
mgr <- ask
fileSource fileInfo $$
S3.multipartUploadSink appAwsConfiguration Aws.defServiceConfig mgr appS3Bucket
(fileNameOfKey key) (10 * 1000 * 1000) -- 10MB
もしかして動くんじゃないか…? と期待しながら動かしてみます.
動かしてみたら100MBのファイルは普通に分割アップロードできました.
しかし,
1GBのファイルをアップロードしてみると,
ConnectionTimeout
が発生してしまいました.
もう一度やってみたら今度は動いているようです, 前回はネットワークインスペクタを開いてたから大量のリクエストボディを見てしまって破綻してしまったのでしょうか.
しかし, 1GBのファイルアップロードにかなり時間がかかりますね… 回線が悪いのかな? 並列実行しないとパフォーマンスが出ないのでしょうか…? それともメモリが足りないからswapにアクセスしてしまっているのでしょうか.
やっぱりユーザのブラウザから直接アップロード出来るようにした方が良さそうですね.
- ブラウザからS3に巨大なファイルを低メモリで送りつけるアレ – Wano Developers Blog
- TTLabs/EvaporateJS: Javascript library for browser to S3 multipart resumable uploads
こっちの方が進行状況をブラウザで表示するのも容易ですし, こちらにしてしまいましょうか. 同期問題もcompleteして初めてデータベースに登録すればまあ現実問題にならなさそう. 問題はcompleteしたことをサーバに伝えずに延々ファイルだけ送信してくるクラッカーが居た場合ですが, 対策は
- ジョブを動かしてデータベースに登録されてないS3ファイルを定期的に消す
forkHandler
で待ちスレッドを作り, 一定期間待ち, completeが来なければ削除してしまう- そもそもcomplete通知をブラウザに任せずにS3のイベント通知を利用する
などの対策が考えられますね. しかし, データベースへの登録はファイルが送信し終わってからでないと不都合が生じるんですよね. それでいてファイル名はデータベースのIDに依存するのでcommitは開始しないといけない. completeフラグを生やしてcompleteしていないレコードは無いとして扱うとかで済ましましょうか? うーんやはり難しい.
まあ, それは後で良いでしょう. まずはダウンロードだけでもwebサーバを介さない方法に切り替えたい. 今はアップロードができる, それだけで十分でしょう.
チャンクを100MBに増やしてみましたが別に高速にはなりませんね…