@@ -634,12 +634,25 @@ mod tests {
634634 use crate :: engine:: { DBEngine , SharedWALManager } ;
635635
636636 async fn build_test_engine ( test_name : & str ) -> ( DBEngine , SharedWALManager ) {
637+ build_test_engine_with_memory_limit ( test_name, 128 * 1024 * 1024 ) . await
638+ }
639+
640+ async fn build_test_engine_with_memory_limit (
641+ test_name : & str ,
642+ max_query_memory_bytes : u64 ,
643+ ) -> ( DBEngine , SharedWALManager ) {
637644 let base_path = PathBuf :: from ( "target" ) . join ( test_name) ;
638- if base_path. exists ( ) {
639- tokio:: fs:: remove_dir_all ( & base_path) . await . unwrap ( ) ;
645+ // 동기 `Path::exists()`는 metadata 오류를 `false`로 처리해 정리를 건너뛸 수
646+ // 있으므로, 비동기 `tokio::fs::metadata`로 NotFound만 무시하고 다른 오류는
647+ // 테스트를 실패시킵니다 (CodeRabbit).
648+ match tokio:: fs:: metadata ( & base_path) . await {
649+ Ok ( _) => tokio:: fs:: remove_dir_all ( & base_path) . await . unwrap ( ) ,
650+ Err ( error) if error. kind ( ) == std:: io:: ErrorKind :: NotFound => { }
651+ Err ( error) => panic ! ( "metadata check failed for {base_path:?}: {error}" ) ,
640652 }
641653
642- let config = LaunchConfig :: default_for_base_path ( & base_path) ;
654+ let mut config = LaunchConfig :: default_for_base_path ( & base_path) ;
655+ config. max_query_memory_bytes = max_query_memory_bytes;
643656 tokio:: fs:: create_dir_all ( & config. data_directory )
644657 . await
645658 . unwrap ( ) ;
@@ -821,7 +834,11 @@ mod tests {
821834 . await
822835 . unwrap_or_else ( |error| panic ! ( "{sql} failed: {error}" ) ) ;
823836
824- assert_eq ! ( result. rows. len( ) , expected, "unexpected row count for {sql}" ) ;
837+ assert_eq ! (
838+ result. rows. len( ) ,
839+ expected,
840+ "unexpected row count for {sql}"
841+ ) ;
825842 }
826843 }
827844
@@ -846,7 +863,195 @@ mod tests {
846863 ( "select id from key_value offset 1;" , 2 ) ,
847864 ] {
848865 let result = execute_sql ( & engine, wal. clone ( ) , sql) . await . unwrap ( ) ;
849- assert_eq ! ( result. rows. len( ) , expected, "unexpected row count for {sql}" ) ;
866+ assert_eq ! (
867+ result. rows. len( ) ,
868+ expected,
869+ "unexpected row count for {sql}"
870+ ) ;
850871 }
851872 }
873+
874+ /// 트래킹 상한을 넘기는 크기의 문자열 값을 가진 행을 삽입합니다.
875+ /// `size_bytes` 이상의 문자열이 들어가도록 패딩합니다.
876+ async fn insert_big_string ( engine : & DBEngine , wal : SharedWALManager , size_bytes : usize ) {
877+ let value = "x" . repeat ( size_bytes) ;
878+ engine
879+ . insert (
880+ InsertQuery :: builder ( )
881+ . set_into_table ( TableName :: new (
882+ Some ( "rrdb" . to_string ( ) ) ,
883+ "key_value" . to_string ( ) ,
884+ ) )
885+ . set_columns ( vec ! [ "id" . to_string( ) ] )
886+ . set_values ( vec ! [ InsertValue {
887+ list: vec![ Some ( SQLExpression :: String ( value) ) ] ,
888+ } ] )
889+ . build ( ) ,
890+ wal,
891+ )
892+ . await
893+ . unwrap ( ) ;
894+ }
895+
896+ /// 메모리 예산을 매우 낮게 설정하면, SELECT가
897+ /// 행을 메모리로 로드하는 순간 강제 중단됩니다 (#265).
898+ #[ tokio:: test]
899+ async fn select_over_memory_limit_is_killed ( ) {
900+ let ( engine, wal) = build_test_engine_with_memory_limit ( "test_select_oom_kill" , 1024 ) . await ;
901+
902+ execute_sql ( & engine, wal. clone ( ) , "create database rrdb;" )
903+ . await
904+ . unwrap ( ) ;
905+ execute_sql (
906+ & engine,
907+ wal. clone ( ) ,
908+ "create table key_value (id varchar(65536));" ,
909+ )
910+ . await
911+ . unwrap ( ) ;
912+
913+ // 행 하나만 로드해도 예산(1KB)를 넘는 크기(4KB 문자열).
914+ insert_big_string ( & engine, wal. clone ( ) , 4096 ) . await ;
915+
916+ let result = execute_sql ( & engine, wal, "select id from key_value;" )
917+ . await
918+ . unwrap_err ( ) ;
919+
920+ let message = result. to_string ( ) ;
921+ assert ! (
922+ message. contains( "query memory limit exceeded" ) ,
923+ "expected memory limit error, got: {message}"
924+ ) ;
925+ }
926+
927+ /// `max_query_memory_bytes = 0`이면 OOM killer가 비활성입니다 (#265).
928+ /// 크기의 행을 로드해도 에러 없이 정상 동작합니다.
929+ #[ tokio:: test]
930+ async fn select_with_disabled_memory_limit_succeeds ( ) {
931+ let ( engine, wal) =
932+ build_test_engine_with_memory_limit ( "test_select_oom_disabled" , 0 ) . await ;
933+
934+ execute_sql ( & engine, wal. clone ( ) , "create database rrdb;" )
935+ . await
936+ . unwrap ( ) ;
937+ execute_sql (
938+ & engine,
939+ wal. clone ( ) ,
940+ "create table key_value (id varchar(65536));" ,
941+ )
942+ . await
943+ . unwrap ( ) ;
944+
945+ insert_big_string ( & engine, wal. clone ( ) , 4096 ) . await ;
946+
947+ let result = execute_sql ( & engine, wal, "select id from key_value;" )
948+ . await
949+ . unwrap ( ) ;
950+
951+ assert_eq ! ( result. rows. len( ) , 1 ) ;
952+ }
953+
954+ /// 예산을 넘지 않는 손상된 실행은 정상 동작해야 합니다 (#265).
955+ #[ tokio:: test]
956+ async fn select_within_memory_limit_succeeds ( ) {
957+ let ( engine, wal) =
958+ build_test_engine_with_memory_limit ( "test_select_oom_within" , 1024 * 1024 ) . await ;
959+
960+ execute_sql ( & engine, wal. clone ( ) , "create database rrdb;" )
961+ . await
962+ . unwrap ( ) ;
963+ execute_sql (
964+ & engine,
965+ wal. clone ( ) ,
966+ "create table key_value (id varchar(65536));" ,
967+ )
968+ . await
969+ . unwrap ( ) ;
970+
971+ insert_big_string ( & engine, wal. clone ( ) , 4096 ) . await ;
972+
973+ let result = execute_sql ( & engine, wal, "select id from key_value;" )
974+ . await
975+ . unwrap ( ) ;
976+
977+ assert_eq ! ( result. rows. len( ) , 1 ) ;
978+ }
979+
980+ /// 지정한 테이블에 큰 문자열 행을 삽입합니다.
981+ async fn insert_big_string_into (
982+ engine : & DBEngine ,
983+ wal : SharedWALManager ,
984+ table : & str ,
985+ size_bytes : usize ,
986+ ) {
987+ let value = "x" . repeat ( size_bytes) ;
988+ engine
989+ . insert (
990+ InsertQuery :: builder ( )
991+ . set_into_table ( TableName :: new ( Some ( "rrdb" . to_string ( ) ) , table. to_string ( ) ) )
992+ . set_columns ( vec ! [ "id" . to_string( ) ] )
993+ . set_values ( vec ! [ InsertValue {
994+ list: vec![ Some ( SQLExpression :: String ( value) ) ] ,
995+ } ] )
996+ . build ( ) ,
997+ wal,
998+ )
999+ . await
1000+ . unwrap ( ) ;
1001+ }
1002+
1003+ /// 동시에 실행되는 두 쿼리는 서로의 메모리 예산을 방해하지 않아야 합니다 (#265).
1004+ ///
1005+ /// CodeRabbit #1: 이전 구현(공유 RwLock 슬롯)은 두 `process_query`가 동시에
1006+ /// 실행되면 한쪽이 다른 쪽의 tracker를 덮어쓰거나, 한쪽이 스캔을 마치기 전에
1007+ /// 슬롯을 클리어해서 나머지가 예산 없이 실행될 수 있었습니다.
1008+ ///
1009+ /// task-local로 전환한 후에는 각 쿼리가 자기 tracker를 가지므로,
1010+ /// 큰 쿼리(예산 초과 → 에러)와 작은 쿼리(예산 내 → 성공)를 동시에 실행해도
1011+ /// 서로의 결과에 영향을 주지 않아야 합니다.
1012+ #[ tokio:: test]
1013+ async fn concurrent_queries_do_not_interfere_with_each_others_budget ( ) {
1014+ let ( engine, wal) =
1015+ build_test_engine_with_memory_limit ( "test_select_oom_concurrent" , 2048 ) . await ;
1016+
1017+ execute_sql ( & engine, wal. clone ( ) , "create database rrdb;" )
1018+ . await
1019+ . unwrap ( ) ;
1020+ execute_sql (
1021+ & engine,
1022+ wal. clone ( ) ,
1023+ "create table small_t (id varchar(65536));" ,
1024+ )
1025+ . await
1026+ . unwrap ( ) ;
1027+ execute_sql (
1028+ & engine,
1029+ wal. clone ( ) ,
1030+ "create table big_t (id varchar(65536));" ,
1031+ )
1032+ . await
1033+ . unwrap ( ) ;
1034+
1035+ // small_t: 예산(2KB) 안에 들어오는 작은 값
1036+ // big_t: 예산을 넘기는 큰 값 (4KB 문자열)
1037+ insert_big_string_into ( & engine, wal. clone ( ) , "small_t" , 256 ) . await ;
1038+ insert_big_string_into ( & engine, wal. clone ( ) , "big_t" , 4096 ) . await ;
1039+
1040+ // 동시 실행: big 쿼리는 실패, small 쿼리는 성공해야 함
1041+ let ( big_result, small_result) = tokio:: join!(
1042+ execute_sql( & engine, wal. clone( ) , "select id from big_t;" ) ,
1043+ execute_sql( & engine, wal. clone( ) , "select id from small_t;" ) ,
1044+ ) ;
1045+
1046+ let big_error = big_result. unwrap_err ( ) ;
1047+ assert ! (
1048+ big_error
1049+ . to_string( )
1050+ . contains( "query memory limit exceeded" ) ,
1051+ "big query should be killed, got: {big_error}"
1052+ ) ;
1053+
1054+ let small_rows = small_result. unwrap ( ) ;
1055+ assert_eq ! ( small_rows. rows. len( ) , 1 , "small query should still succeed" ) ;
1056+ }
8521057}
0 commit comments