Rust学習 - CodeCraftersに入門・Redisサーバを作ろう
今日はCodeCraftersに挑戦する日だった。写真は夕方のイメージ、夕方からやりはじめた。
CodeCraftersはプログラミング学習サービス。より実践的な課題をサーブしてくれると評判だった。トライアル版があるようなので試してみようと思う。
概要
「CodeCrafters」というサービスの公式ページのURLをそっと貼っておく。
↑有料のサービスであり、そして、いくつかのコースが用意されている。
今回はトライアル版で
「Build your own Redis」という
「RedisっぽいものをRustで実装する」コースを選んでやってみた。
課題に対する回答を、CodeCraftersが指定するgitリポジトリにコミット(git commit/git push)することで、用意されたテストコードにより正否が判定される仕組みになっている。
進捗状況
途中ながら、進捗状況をおいておこう。「コード」と、そして「実行結果」。
トライアル版では、Stage1 - Stage7まであって、その中で状況としては「Stage-4」でエラーが出ているというステータス。
Stage-4is何というのは、CodeCraftersのコースの説明を読むと分かる通り。
「Handle concurrent clients」と称して、
複数のクライアントが存在する状態で並列でのリクエストが来ても動作するRedisサーバを実装できるか試してくる。
コード
#[allow(unused_imports)]
use std::env;
#[allow(unused_imports)]
use std::fs;
#[allow(unused_imports)]
use std::net::{TcpListener, TcpStream, Shutdown};
use std::io::Read;
// https://users.rust-lang.org/t/no-method-write-for-tcpstream/63486
use std::io::Write;
// https://doc.rust-lang.org/std/thread/
use std::thread;
use std::time::Duration;
use crypto_hash::{Algorithm, hex_digest};
fn main() -> std::io::Result<()> {
// You can use print statements as follows for debugging, they'll be visible when running tests.
println!("Logs from your program will appear here!");
// Uncomment this block to pass the first stage
let listener = TcpListener::bind("127.0.0.1:6379").unwrap();
loop {
let (mut stream, _) = listener.accept()?;
println!("new stream: {:?}", stream);
thread::spawn(move || {
let five_seconds: Option<Duration> = Some(Duration::new(5, 0));
println!("{:?}", stream);
stream.set_read_timeout(five_seconds).unwrap();
let mut count = 1;
loop {
let thread_id = thread::current().id();
println!("Loop in thread: {:?}", thread_id);
count += 1;
let mut data = [0;128];
match stream.read(&mut data) {
Ok(n) => {
println!("{:?}", n);
if n <= 0 {
break;
}
let u: &[u8] = &data;
let data_digest = hex_digest(Algorithm::SHA256, u);
println!("thread_id={:?}, loop_count = {}, {:?}", thread_id, count, data_digest);
// https://redis.io/docs/reference/protocol-spec/
let response = "+PONG";
// https://doc.rust-lang.org/book/ch20-01-single-threaded.html
let _hogehoge = stream.write(response.as_bytes());
stream.flush().unwrap();
}
Err(_e) => {
// println!("{:?}", _e);
continue;
}
};
}
});
std::thread::sleep(std::time::Duration::from_secs(1));
}
} // end main
実行結果
こちらが実行結果。git似pushした時にテストコードが実行され、デバッグ出力メッセージを含む結果が確認できる。
「Stage-4」(今回挑戦しているBuild your own Redisコースの4番目のステージ)でエラーになっている。↓の実行結果もよく見る(最後尾の方)そのようなエラーが確認できる。
remote: [stage-1] Running tests for Stage #1: Bind to a port
remote: [stage-1] Running program
remote: [your_program] Logs from your program will appear here!
remote: [stage-1] Connection successful
remote: [stage-1] Test passed.
remote: [stage-1] Terminating program
remote: [your_program] new stream: TcpStream { addr: 127.0.0.1:6379, peer: 127.0.0.1:39712, fd: 4 }
remote: [stage-1] Program terminated successfully
remote:
remote: [stage-2] Running tests for Stage #2: Respond to PING
remote: [stage-2] Running program
remote: [stage-2] Sending ping command...
remote: [your_program] Logs from your program will appear here!
remote: [your_program] new stream: TcpStream { addr: 127.0.0.1:6379, peer: 127.0.0.1:39718, fd: 4 }
remote: [your_program] TcpStream { addr: 127.0.0.1:6379, peer: 127.0.0.1:39718, fd: 4 }
remote: [your_program] Loop in thread: ThreadId(2)
remote: [your_program] 14
remote: [your_program] thread_id=ThreadId(2), loop_count = 2, "8de98ec2877b5b586baf05e56eecfa430ddff6ec70482247a58e724282a697c0"
remote: [your_program] Loop in thread: ThreadId(2)
remote: [stage-2] Success, closing connection...
remote: [stage-2] Test passed.
remote: [stage-2] Terminating program
remote: [stage-2] Program terminated successfully
remote:
remote: [stage-3] Running tests for Stage #3: Respond to multiple PINGs
remote: [stage-3] Running program
remote: [stage-3] client-1: Sending ping command...
remote: [your_program] Logs from your program will appear here!
remote: [your_program] new stream: TcpStream { addr: 127.0.0.1:6379, peer: 127.0.0.1:39724, fd: 4 }
remote: [your_program] TcpStream { addr: 127.0.0.1:6379, peer: 127.0.0.1:39724, fd: 4 }
remote: [your_program] Loop in thread: ThreadId(2)
remote: [your_program] 14
remote: [your_program] thread_id=ThreadId(2), loop_count = 2, "8de98ec2877b5b586baf05e56eecfa430ddff6ec70482247a58e724282a697c0"
remote: [your_program] Loop in thread: ThreadId(2)
remote: [stage-3] client-1: Received response.
remote: [stage-3] client-1: Sending ping command...
remote: [your_program] 14
remote: [your_program] thread_id=ThreadId(2), loop_count = 3, "8de98ec2877b5b586baf05e56eecfa430ddff6ec70482247a58e724282a697c0"
remote: [your_program] Loop in thread: ThreadId(2)
remote: [stage-3] client-1: Received response.
remote: [stage-3] client-1: Sending ping command...
remote: [your_program] 14
remote: [your_program] thread_id=ThreadId(2), loop_count = 4, "8de98ec2877b5b586baf05e56eecfa430ddff6ec70482247a58e724282a697c0"
remote: [your_program] Loop in thread: ThreadId(2)
remote: [stage-3] client-1: Received response.
remote: [stage-3] Success, closing connection...
remote: [stage-3] Test passed.
remote: [stage-3] Terminating program
remote: [stage-3] Program terminated successfully
remote:
remote: [stage-4] Running tests for Stage #4: Handle concurrent clients
remote: [stage-4] Running program
remote: [stage-4] client-1: Sending ping command...
remote: [your_program] Logs from your program will appear here!
remote: [your_program] new stream: TcpStream { addr: 127.0.0.1:6379, peer: 127.0.0.1:39730, fd: 4 }
remote: [your_program] TcpStream { addr: 127.0.0.1:6379, peer: 127.0.0.1:39730, fd: 4 }
remote: [your_program] Loop in thread: ThreadId(2)
remote: [your_program] 14
remote: [your_program] thread_id=ThreadId(2), loop_count = 2, "8de98ec2877b5b586baf05e56eecfa430ddff6ec70482247a58e724282a697c0"
remote: [your_program] Loop in thread: ThreadId(2)
remote: [stage-4] client-1: Received response.
remote: [stage-4] client-2: Sending ping command...
remote: [your_program] new stream: TcpStream { addr: 127.0.0.1:6379, peer: 127.0.0.1:39732, fd: 5 }
remote: [your_program] TcpStream { addr: 127.0.0.1:6379, peer: 127.0.0.1:39732, fd: 5 }
remote: [your_program] Loop in thread: ThreadId(3)
remote: [your_program] 14
remote: [your_program] thread_id=ThreadId(3), loop_count = 2, "8de98ec2877b5b586baf05e56eecfa430ddff6ec70482247a58e724282a697c0"
remote: [your_program] Loop in thread: ThreadId(3)
remote: [your_program] Loop in thread: ThreadId(2)
remote: [stage-4] client-2: Received response.
remote: [stage-4] client-1: Sending ping command...
remote: [your_program] 14
remote: [your_program] thread_id=ThreadId(2), loop_count = 4, "8de98ec2877b5b586baf05e56eecfa430ddff6ec70482247a58e724282a697c0"
remote: [your_program] Loop in thread: ThreadId(2)
remote: [your_program] Loop in thread: ThreadId(3)
remote: [stage-4] client-1: Received response.
remote: [stage-4] client-1: Sending ping command...
remote: [your_program] 14
remote: [your_program] thread_id=ThreadId(2), loop_count = 5, "8de98ec2877b5b586baf05e56eecfa430ddff6ec70482247a58e724282a697c0"
remote: [your_program] Loop in thread: ThreadId(2)
remote: [stage-4] timed out, test exceeded 10 seconds
remote: [stage-4] Test failed
remote: [stage-4] Terminating program
remote: [stage-4] client-1: Received response.
remote: [stage-4] client-2: Sending ping command...
remote: [stage-4] Hint: EOF is short for 'end of file'. This usually means that your program either:
remote: [stage-4] (a) didn't send a complete response, or
remote: [stage-4] (b) closed the connection early
remote: [stage-4] Program terminated successfully
所感
「CodeCrafters」、実践的なレベルに到達しているかを試してくれてる感じがして、それ自体はとてもありがたい。しかし、今回チャレンジしたコースでは、Rustで書かれた模範解答コード(ソリューション)が用意されていなかった。Go言語でコード(=ソリューション)を参考にしてくれとのこと。なのでそれは残念。
自分がつまづいたStage4に至っては、トライアル版ではGo言語で書かれたソリューションすら参照できない(課金しなければ答えをまったく見ることができない状態)。なので、お金を払ってコミットするかをかなり考えさせられた。
自分の目的からすると、お金を払ってまでやるかは微妙。明確に腰を据えて再チャレンジはしないかもしれない(気にはなるので答えが思いつけば試行錯誤はする)
良かったことをいくつか書いておく
TCPの仕組みを思い出したり、ソケット建てるやり方を調べる時間が殆ど。おかげで、std::net::{TcpListener,TcpStream}, std::thread::spawnと出会った。
込み入った課題をやる中で構文の理解度もそれなりに試される。(これはよかったがかなり理解不足が見つかった)
Cargo.tomlの記載の仕方。dependenciesに記載するときはハイフン区切り、ソースコード上ではアンスコ区切りなのか。スムーズに書けず。
標準ライブラリを使っている時、戻り値/引数が基本データ型、構造体、Resultか、Optionかなど頭に入っておらず随時躓く。これは練習が必要。ちゃんと練習しないとunwrapが散りばめられた不健全コードになる(今日のコードは少なくともそうなった)
vec!マクロで生成した配列からdigest_hashを生成しようとしたら、型変換できないとエラーになる。固定長配列と動的配列の変換のやり方も理解しなくては。
Rust、整数のインクリメントができないことを今日知った。
moveキーワード is 何?という状態
まとめると
以上、報告終わり。また脱線してしまった。
答え、気になるなー。基礎構文の理解を補って、TCP自作入門でTCPの理解度を上げて、再チャレンジしようかな。
追記
調査した結果なんと改行コード(CRLF)の不足が原因とわかり、stage-4はクリア。スッキリ。仕様はちゃんと読もうな自分!
実行時間が10秒を超えるとタイムアウト扱いになる(CodeCrafters提供のテストコードの仕様)
1回のPING/PONGに3秒かかってる(ログの情報)
stage-4のテストでは3つのクライアントで計6回のPING/PONGの送受信が走る
だから、テスト全体の処理時間が途中で10秒を超えてエラーになった
redis-cliからPINGコマンドを送信し、「本物のRedisサーバ」とRustで書いたサーバで動きを比べた。
ここで 応答したメッセージが認識されていないことに気づく
+PONGの後ろに改行コードが必要(原因)
↓のリンク先が、CodeCraftersが公開している、正否の判定に使われてるテストコード。
ログの方を再確認してみる。
(調査しやすいように、サーバ側のスレッドに、開始時刻からの経過時間(elapsed)を出力するように変えてある)
「timed out, test exceeded 10 seconds」というメッセージ出力。さらに続けて、「[stage-4] Test failed」 「[stage-4] Terminating program」というメッセージ出力
解釈: テストの実行時間が10秒を超えたのでタイムアウトと判断され、その結果テストも失敗とみなされた
ThreadId(2)とThreadId(3)の2つが稼働している。
解釈: ThreadId(2) = client-1 で、ThreadId(3) = client-2と理解
「timed out, test exceeded 10 seconds」の直前のログを確認
「thread_id=ThreadId(2), loop_count = 3, elapsed=9, "eff57f45b75a5829610b3e8f010c2e97178887f673dd8645ef526eec11252cea"」の箇所に注目
解釈: client-1からの3度目のPING受信と理解
解釈: client-1とのやり取りが開始されてから9秒経過したと理解
client-1からの2度目のPING受信に対応するログを探してみる
「thread_id=ThreadId(2), loop_count = 2, elapsed=6, "eff57f45b75a5829610b3e8f010c2e97178887f673dd8645ef526eec11252cea"」に注目
解釈: client-1からの2度目のPING受信の時点で、6秒経過していたようだ。
ってことは、1回のPING/PONGに3秒もかかっているから、テスト全体の処理時間が10秒以上かかってコケたのか!
remote: [stage-4] Running tests for Stage #4: Handle concurrent clients
remote: [stage-4] Running program
remote: [stage-4] client-1: Sending ping command...
remote: [your_program] Logs from your program will appear here!
remote: [your_program] new stream: TcpStream { addr: 127.0.0.1:6379, peer: 127.0.0.1:52746, fd: 4 }
remote: [your_program] JoinHandle { .. }
remote: [your_program] TcpStream { addr: 127.0.0.1:6379, peer: 127.0.0.1:52746, fd: 4 }
remote: [your_program] thread_id=ThreadId(2), loop_count = 1, elapsed=0, "eff57f45b75a5829610b3e8f010c2e97178887f673dd8645ef526eec11252cea"
remote: [your_program] thread_id=ThreadId(2), loop_count = 1, success stream.write size=5, elapsed=0
remote: [stage-4] c: Received response.
remote: [stage-4] client-2: Sending ping command...
remote: [your_program] new stream: TcpStream { addr: 127.0.0.1:6379, peer: 127.0.0.1:52748, fd: 5 }
remote: [your_program] JoinHandle { .. }
remote: [your_program] TcpStream { addr: 127.0.0.1:6379, peer: 127.0.0.1:52748, fd: 5 }
remote: [your_program] thread_id=ThreadId(3), loop_count = 1, elapsed=0, "eff57f45b75a5829610b3e8f010c2e97178887f673dd8645ef526eec11252cea"
remote: [your_program] thread_id=ThreadId(3), loop_count = 1, success stream.write size=5, elapsed=0
remote: [stage-4] client-2: Received response.
remote: [stage-4] client-1: Sending ping command...
remote: [your_program] thread_id=ThreadId(2), loop_count = 2, elapsed=6, "eff57f45b75a5829610b3e8f010c2e97178887f673dd8645ef526eec11252cea"
remote: [your_program] thread_id=ThreadId(2), loop_count = 2, success stream.write size=5, elapsed=6
remote: [stage-4] client-1: Received response.
remote: [stage-4] client-1: Sending ping command...
remote: [your_program] thread_id=ThreadId(2), loop_count = 3, elapsed=9, "eff57f45b75a5829610b3e8f010c2e97178887f673dd8645ef526eec11252cea"
remote: [your_program] thread_id=ThreadId(2), loop_count = 3, success stream.write size=5, elapsed=9
remote: [stage-4] timed out, test exceeded 10 seconds
remote: [stage-4] Test failed
remote: [stage-4] Terminating program
remote: [stage-4] client-1: Received response.
remote: [stage-4] client-2: Sending ping command...
remote: [stage-4] Hint: EOF is short for 'end of file'. This usually means that your program either:
remote: [stage-4] (a) didn't send a complete response, or
remote: [stage-4] (b) closed the connection early
remote: [stage-4] Program terminated successfully
とにかく、今日はこれで終わり。
また次回!